Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make dark and rtl/ltr variants insensitive to DOM order #10766

Merged
merged 3 commits into from
Mar 10, 2023
Merged

Conversation

adamwathan
Copy link
Member

This PR tweaks the rtl/ltr variants and the dark variant (when using the class strategy) to use the :is() pseudo-class to make it possible to stack these variants, while declaring both the dir attribute and the .dark parent class on the same element, or on parent elements in any order.

Prior to this PR, stacking variants like rtl:dark:italic would create a selector like this:

[dir="rtl"] .dark .rtl\:dark\:italic {
  font-style: italic;
}

With this selector, only HTML using this structure would work:

<div dir="rtl">
  <div class="dark">
    <p class="rtl:dark:italic">Hello world.</p>
  </div>
</div>

This is inconvenient because what you really want to do is put both markers right on the html tag, like this:

<html dir="rtl" class="dark">
  <!-- ... -->
    <p class="rtl:dark:italic">Hello world.</p>
  <!-- ... -->
</html>

...but there was no easy way for us to make that work with how variants compose, other than to generate every combination of selectors like this:

[dir="rtl"] .dark .rtl\:dark\:italic,
.dark [dir="rtl"] .rtl\:dark\:italic,
.dark[dir="rtl"] .rtl\:dark\:italic {
  font-style: italic;
}

Once this PR is merged, that same class would generate this CSS instead:

:is([dir="rtl"] :is(.dark .rtl\:dark\:italic)) {
  font-style: italic;
}

The :is() pseudo-class is the perfect solution here as it handles this sort of thing by design. Using :is(), all three of these DOM structures just work:

<html dir="rtl" class="dark">
  <!-- ... -->
    <p class="rtl:dark:italic">Hello world.</p>
  <!-- ... -->
</html>

<div dir="rtl">
  <div class="dark">
    <p class="rtl:dark:italic">Hello world.</p>
  </div>
</div>

<div class="dark">
  <div dir="rtl">
    <p class="rtl:dark:italic">Hello world.</p>
  </div>
</div>

Browser support for :is() is very good at this point, with support back to Safari 14 which is close to three years old.

Tailwind already uses CSS features that rely on even newer browsers (like native aspect-ratio support, as well as :where() in Preflight), so it feels reasonable to me to start relying on :is() in core as well.

@adamwathan adamwathan merged commit e40b73a into master Mar 10, 2023
@adamwathan adamwathan deleted the dark-rtl-is branch March 10, 2023 17:03
@MichaelAllenWarner
Copy link
Contributor

This feels a bit soon -- Safari 13 still has a lot of users, and many devs are still obligated to support it.

With :is(), though, one can still include a :matches() fallback-rule to cover Safari 13. I wonder if it might be possible to include such a fallback here (on an opt-in basis, say). I've been doing this with custom variants of my own, for precisely the same reason (stacking multiple variants that use the descendant combinator). Just a thought.

@7iomka
Copy link

7iomka commented May 19, 2023

This feels a bit soon -- Safari 13 still has a lot of users, and many devs are still obligated to support it.

With :is(), though, one can still include a :matches() fallback-rule to cover Safari 13. I wonder if it might be possible to include such a fallback here (on an opt-in basis, say). I've been doing this with custom variants of my own, for precisely the same reason (stacking multiple variants that use the descendant combinator). Just a thought.

please tell me what tool do you use to create a fallback in the form of :matches?

@MichaelAllenWarner
Copy link
Contributor

@7iomka

I made a custom PostCSS plugin. Looks like this:

/*
  This custom PostCSS plugin makes `:matches()` fallback rules for every
  rule that uses `:is()` (mainly to support Safari < 14).
*/

/** @returns {import('postcss').Plugin} */
module.exports = () => {
  return {
    postcssPlugin: 'postcss-matches-fallback-for-is',

    OnceExit(root) {
      root.walkRules((rule) => {
        if (!rule.selector.includes(':is(')) return;

        const clone = rule.clone();
        clone.selectors = clone.selectors.map((selector) =>
          selector.replaceAll(':is(', ':matches(')
        );

        rule.before(clone);
      });
    },
  };
};
module.exports.postcss = true;

(I'm not a PostCSS expert, but this seems to work.)

@7iomka
Copy link

7iomka commented May 20, 2023

@7iomka

I made a custom PostCSS plugin. Looks like this:

/*
  This custom PostCSS plugin makes `:matches()` fallback rules for every
  rule that uses `:is()` (mainly to support Safari < 14).
*/

/** @returns {import('postcss').Plugin} */
module.exports = () => {
  return {
    postcssPlugin: 'postcss-matches-fallback-for-is',

    OnceExit(root) {
      root.walkRules((rule) => {
        if (!rule.selector.includes(':is(')) return;

        const clone = rule.clone();
        clone.selectors = clone.selectors.map((selector) =>
          selector.replaceAll(':is(', ':matches(')
        );

        rule.before(clone);
      });
    },
  };
};
module.exports.postcss = true;

(I'm not a PostCSS expert, but this seems to work.)

Thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants