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

Add * variant for targeting direct children #12551

Merged
merged 5 commits into from Dec 9, 2023
Merged

Conversation

adamwathan
Copy link
Member

This PR adds a new * variant for targeting direct children of an element:

<nav class="*:underline">
  <a href="#">One</a>
  <a href="#">Two</a>
  <a href="#">Three</a>
</nav>

This can also be combined with other variants, such as hover:

<nav class="hover:*:underline">
  <a href="#">One</a>
  <a href="#">Two</a>
  <a href="#">Three</a>
</nav>

The above example would only add an underline to each link when each link is hovered, as per our documentation on stacked variants/modifiers.

Known limitations

One thing you'd probably expect to work that won't currently work is overriding a child selector style with a utility directly on the child itself:

<nav class="*:underline">
  <a href="#">One</a>
  <a href="#" class="no-underline">Two</a> <!-- Will still be underlined -->
  <a href="#">Three</a>
</nav>

This is because the generated selector for a child variant has the same specificity as a regular utility class, but appears later in the CSS file so it takes precedence.

We can reduce the specificity of this selector using :where:

:where(.\*\:underline) > * {
  text-decoration: underline
}

However this reduces the specificity 0,0,0 which is lower than the styles in Preflight, which means Preflight styles would now defeat child variant styles, which is no good.

This can be solved by taking advantage of the new proper CSS @layer rule, which ensures that rules in later layers always take precedence over rules in earlier layers, regardless of their specificity.

We don't want to do that in the v3.* series of Tailwind because it could be considered a breaking change due to the fact that Tailwind would stop working in browsers it currently works in, but we do intend to make this change for v4. So in v4, we can introduce the use of both :where() in the child variant, and native @layer rules which will make it possible to override child variants with utilities the way you'd expect.

We originally resisted adding this feature because of this limitation and because we didn't know what the best solution for it would be, but now that the correct solution is clear we feel comfortable introducing this feature despite the limitation because the path forward will not require drastically re-imagining the API or how the feature should work.

@louiss0
Copy link

louiss0 commented Dec 8, 2023

I prefer the word direct-children. The star is weird or dir-children.

@AhmedBaset
Copy link

>:underline is more indicative of direct children thant *:... which is used for "All". or as @louiss0 said children:.....

@dano2906
Copy link

dano2906 commented Dec 8, 2023

  • is confuse

@unlocomqx
Copy link

>:underline is more indicative of direct children thant *:... which is used for "All". or as @louiss0 said children:.....

I fear that one day, they will want to add a variant to target all children but * is already taken

@adamwathan
Copy link
Member Author

>:underline is more indicative of direct children thant *:... which is used for "All". or as @louiss0 said children:.....

I fear that one day, they will want to add a variant to target all children but * is already taken

Thought about this pretty hard before deciding on this API and I just don't see myself ever wanting to target all children ever. If that's ever needed though you can use an arbitrary variant [&_*] 👍

@IanMitchell
Copy link

Excited to see this, wanted something like this for a while :D.

Thought about this pretty hard before deciding on this API and I just don't see myself ever wanting to target all children ever. If that's ever needed though you can use an arbitrary variant [&_*] 👍

My two cents on the syntax - I think intuitively, if I was introduced to this selector while reading a codebase, I would see * and assume it was all children as opposed to direct children because of its meaning in CSS (and knowing Tailwind usually tries to match that pretty closely). Without knowing this syntax was an intentional choice, I'd likely initially think I had discovered a bug rather than question my understanding of it (until I read the docs a little more thouroughly).

Either way, stoked to see this ship!

@louiss0
Copy link

louiss0 commented Dec 8, 2023

>:underline is more indicative of direct children thant *:... which is used for "All". or as @louiss0 said children:.....

I fear that one day, they will want to add a variant to target all children but * is already taken

Thought about this pretty hard before deciding on this API and I just don't see myself ever wanting to target all children ever. If that's ever needed though you can use an arbitrary variant [&_*] 👍

I think you should do a poll. Please do the more I look at the star the more I cringe.
Seeing it in between hover like this is weird. hover:*:underline 👎

<nav class="hover:*:underline"></nav>

@brandonmcconnell
Copy link
Contributor

>:underline is more indicative of direct children thant *:... which is used for "All". or as @louiss0 said children:.....

I fear that one day, they will want to add a variant to target all children but * is already taken

Thought about this pretty hard before deciding on this API and I just don't see myself ever wanting to target all children ever. If that's ever needed though you can use an arbitrary variant [&_*]

@adamwathan I promise I’m not just trying to pick a fight here lol — imo using * to target direct children doesn’t feel super CSS-intuitive to me either. Even if you don’t think you’d ever need to target all children, I still feel that using > would be the more intuitive choice.

Just my 2¢ — a great value-add regardless 🙂

@adamwathan
Copy link
Member Author

For those questioning * and suggesting that implies all descendants, here's the selector for direct children:

.my-class > * {
  /* ... */
}

...and here's the one for all children:

.my-class * {
  /* ... */
}

The * isn't the part that determines if you are targeting all descendants or direct children — the combinator decides that, which is > for direct children and for all children:

https://developer.mozilla.org/en-US/docs/Web/CSS/Child_combinator
https://developer.mozilla.org/en-US/docs/Web/CSS/Descendant_combinator

So while it's definitely ambiguous, * is no more correct for all descendants than it is for direct children.

I'm not super keen on adding this to core unless we can keep the name really short, because even children is much longer than [&>*] which already works today. Personally don't love > because it almost looks like a mistake in your HTML because of how many other angle brackets there are and how angle brackets are generally balanced in HTML:

<div class=">:underline">

Just looks like a bug to my eyes, even though of course it's fine in reality.

@AlexVipond
Copy link
Contributor

AlexVipond commented Dec 9, 2023

TL;DR

  • Cool feature
  • I won't be using it because there are existing solutions that don't break code colocation the way this does
  • There's an opportunity to make the implementation even simpler using @scope (and waiting even longer for browser support 😅)

* is 100% the right choice for syntax here.

That said, there are downsides. This feature gives us our very first first-class way to break code colocation in Tailwind. (I don't consider arbitrary variants to be first-class, they're more clearly an escape hatch.) A two-character sequence *: will detach a complex CSS rule from the element it's added to, and attach its styling logic to direct children.

This also gives us our first variant (again, excluding arbitrary variants) that has nothing to do with element state. All other variants apply styles to an element based its state, or the state of an ancestor or sibling. This variant's behavior is arguably similar to after: and before: but since the only way to style ::after and ::before is via their "parent" element, the comparison isn't that close.

This is nitpicky and negligible for smaller use cases like the one in the original PR comment, but for larger cases, I'm hoping the community continues to reach for solutions that don't break code colocation:

  • Loops, OR
  • Extract a React/Vue/whatever component to keep the class string in one place, OR
  • Create a Tailwind component class to apply the set of styles, OR
  • Store repetitive class strings in variables that can be easily concatenated into multiple elements' class strings. Use editor features to easily view variable contents.

Here's an idea of how to implement this feature with @scope:

/*
Per the spec, this is 1 specificity point, although the current
Chrome Canary implementation seems to give it 2 specificity points.
*/
@scope (.\*\:text-blue-600) {
  :is(:scope > *) {
    @apply text-blue-600;
  }
}

@brandonmcconnell
Copy link
Contributor

@adamwathan & @AlexVipond Great thoughts all around. I appreciate the thoughts behind * and to me, it actually makes sense not as the * CCS uses but as a wildcard found in many languages and frameworks, including RegEx and soon Svelte.

I'm with it. 🚀

I'm not worried about the collocation concerns here, as we use [&>*] a ton in our codebase, and one could also argue that unnamed groups via the group variant also introduces a similar risk, but I think someone who knows what they're doing can greatly benefit from both.

@adamwathan adamwathan merged commit cb9c64a into master Dec 9, 2023
10 checks passed
@adamwathan adamwathan deleted the child-variant branch December 9, 2023 14:11
@MichaelAllenWarner
Copy link
Contributor

Thought about this pretty hard before deciding on this API and I just don't see myself ever wanting to target all children ever. If that's ever needed though you can use an arbitrary variant [&_*] 👍

I target all descendants in just about every project! Can give some use-cases if you're interested.

I also target all direct-children in just about every project, so I'm very used to both [&_*]: and [&>*]: at this point. As such, I have to agree with some of the other responses that >: would be the better choice for direct-children, and *: for all descendants.

@brandonmcconnell
Copy link
Contributor

@MichaelAllenWarner I was in the same party, but I couldn't come up with any practical examples for targeting all descendants. I would love to see some. 🙂

@MichaelAllenWarner
Copy link
Contributor

MichaelAllenWarner commented Dec 11, 2023

@brandonmcconnell

Most common use-case is when I'm setting up a background image that will involve a real img element. In this scenario, I need to account for the possibility that the CMS's image-mechanism may include wrapping elements, like a picture or even several div layers (ever work with Drupal?). So in my template, I'll usually do something like this to ensure that the img gets sized correctly:

<div class="absolute inset-0 [&_*]:w-full [&_*]:h-full [&_img]:object-cover">
  {{ imageMaybeWithSomeWrappers }}
</div>

(though I suppose I could just do [&_img]:absolute [&_img]:inset-0 [&_img]:object-cover). Sometimes there are other rules that I need to ensure get applied to each wrapper, too, but usually it's just the full-size.

Another one I often use is break-inside-avoid [&_*]:break-inside-avoid (for column layouts), because I've found that break-inside-avoid alone doesn't always suffice in Safari.

If I've got a component with multiple elements that have to transition simultaneously on hover, then I might do [&_*]:duration-150, say (if the needed value differs from what I've configured as the default).

Those are the main examples I can think of right now. There are some other non-inherited properties that I do target all descendants for (like text-underline-offset, or scroll-margin-top if I've got a sticky header), but usually these are global rules that go in my Tailwind config instead of in-template utility classes.

thecrypticace pushed a commit that referenced this pull request Dec 11, 2023
* add `*` as child variant

* add `*` as allowed variant character

* update test to reflect Lightning CSS output

* add `childVariant` test

* Update changelog

---------

Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Co-authored-by: Gregor Kaczmarczyk <github@aggreggator.de>
@gukj
Copy link

gukj commented Dec 14, 2023

Not sure I'm missing something, but what's wrong with just using children:, direct-children:, all-children: or even direct:? *: looks very non-tailwind to my eyes.

@brandonmcconnell
Copy link
Contributor

brandonmcconnell commented Dec 14, 2023

@gukj I think all of those, even direct longer than even (longer than [&>*]), where sometimes you need to attach several styles to all children, so the short-form * makes this easier, e.g.: *:inline-flex *:items-center *:justify-start

This is just my guess

@gukj
Copy link

gukj commented Dec 14, 2023

@brandonmcconnell I think it's more about trying to look like the css it's representing. Otherwise (I would argue) it makes zero sense for something as uncommon as this to get *: and for something as common as a11y handling transitions to use prefers-reduced-motion:.

If the aim was for tailwind to be short rather than readable, I would maybe agree.

@brandonmcconnell
Copy link
Contributor

@gukj Fair enough, though like some here, myself included, have pointed out – if the goal was for this to look like the logic in CSS it represented, it would have used >: 🤷🏻‍♂️

Seems like it's more than that

@gukj
Copy link

gukj commented Dec 15, 2023

@brandonmcconnell True, true. If the reasoning really is saving a couple characters in case someone needs to apply multiple classes to children, I would much rather see some kind of multi-target syntax. Something like modifier:(class1 class2 class3) instead of modifier:class1 modifier:class2 modifier:class3 would save so much typing.

> I'm against mainly because it'll mess with vim / to have a bunch of trailing brackets. Sorry if I'm coming across grumpy here – I promise I'm not 😅

@brandonmcconnell
Copy link
Contributor

@gukj Hence https://github.com/brandonmcconnell/multitool-for-tailwindcss 😉
(only meant to be used in rare scenarios with unique utilities so as to not inflate CSS file sizes)

@gukj
Copy link

gukj commented Dec 15, 2023

@brandonmcconnell Where have you been all my life 😍

@brandonmcconnell
Copy link
Contributor

brandonmcconnell commented Dec 15, 2023

@gukj spamming tailwind labs with pull requests and crafting custom TailwindCSS plugins 😆

thecrypticace pushed a commit that referenced this pull request Dec 18, 2023
* add `*` as child variant

* add `*` as allowed variant character

* update test to reflect Lightning CSS output

* add `childVariant` test

* Update changelog

---------

Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Co-authored-by: Gregor Kaczmarczyk <github@aggreggator.de>
thecrypticace pushed a commit that referenced this pull request Dec 18, 2023
* add `*` as child variant

* add `*` as allowed variant character

* update test to reflect Lightning CSS output

* add `childVariant` test

* Update changelog

---------

Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
Co-authored-by: Gregor Kaczmarczyk <github@aggreggator.de>
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.

None yet