Skip to main content
Mirai logoDulapah Vibulsanti

Structural and Contextual Styling with Tailwind CSS Arbitrary Variants

Go beyond utility classes and learn how to target children, style elements by parent context, and handle complex structures—without custom CSS or JavaScript.

Content type:Article
Publication date:

Reading time:8 min read

Introduction

Tailwind CSS arbitrary variants allow you to create custom styles that target specific elements in your HTML structure. This is particularly useful when you need to apply styles to child elements based on their type, class, or attributes without adding additional CSS classes or JavaScript.

While the official documentation covers the basics, it doesn't demonstrate how to handle more complex scenarios like targeting deeply nested children, combining multiple selectors, or working with custom attributes.

Use arbitrary variants sparingly as they can reduce code readability and maintainability.

Core Concepts

Understanding the & Symbol

The & character represents the current element (the one with the arbitrary variant class). Think of it as "this element" in your selector.

Canonical Syntax vs Arbitrary Variants

Tailwind CSS provides canonical variants for common combinator and pseudo-class patterns. Prefer these over arbitrary variants when they fit:

  • *: - Targets direct children (replaces [&>*])
  • **: - Targets all descendants (replaces [&_*])
  • has-[...] - Element has matching descendant (replaces [&:has(...)])
  • not-[...] - Negates a selector (replaces [&:not(...)])
  • nth-[N] - Matches :nth-child(N) (replaces [&:nth-child(N)])
  • in-[...] - Matches when inside a parent (replaces [..._&])

Reach for arbitrary variants like [&>tag] or [&_.class] only when you need a structural filter the canonical variants can't express — for example, a direct child of a specific tag, or a specific descendant path.

Direct Children vs All Descendants

This is the most important distinction to understand:

  • *: or [&>...] - Targets only direct children (immediate descendants)
  • **: or [&_...] - Targets all descendants at any nesting level
<div className="[&>p]:text-red-500 [&_span]:text-blue-500">
  <p>Direct child - will be red</p>
  <div>
    <p>Nested child - won't be red (not direct)</p>
    <span>Nested span - will be blue (all descendants)</span>
  </div>
  <span>Direct span - will be blue</span>
</div>

Direct child - will be red

Nested child - won't be red (not direct)

Nested span - will be blue (all descendants)
Direct span - will be blue

Targeting Children by Tag

When you need to target direct children by their tag, you can use the & selector followed by the tag name.

<div className="[&>p]:text-red-600">
  <span>Black text</span>
  <p>Red text</p>
</div>
Black text

Red text

Targeting Children by Class

When you need to target direct children by their class, you can use the & selector with the class name prefixed by a dot.

This also works with className attributes in React components.

<div className="[&>.child2]:text-red-600">
  <div className="child1">Black text</div>
  <div className="child2">Red text</div>
</div>
Black text
Red text

You can also add additional classes to the selector.

<div className="[&>div.child.foo]:text-red-600">
  <div className="child">Black text</div>
  <div className="child foo">Red text</div>
</div>
Black text
Red text

Targeting Children by Data Attributes

When you need to target elements based on their data attributes, you can use the *: variant for direct children combined with the data-[attribute="value"] syntax.

This also works with other attributes, not just data-*, for example, role, aria-*, or custom attributes.

Target immediate children with specific data attributes using the *:data-[attribute="value"] pattern.

<div className="*:data-[type='special']:text-red-600">
  <div data-type="normal">Black text</div>
  <div data-type="special">Red text</div>
</div>
Black text
Red text

You can also target nested elements with data attributes using the **: variant for all descendants.

<div className="**:data-[type='special']:text-red-600">
  <div>
    <div data-type="special">Red text</div>
  </div>
</div>
Red text

When you need to target elements based on both their attributes and classes, you can combine them in the selector.

<div className="[&_[data-type='a'].red]:text-red-500 [&_[data-type='a'].blue]:text-blue-500">
  <p data-type="a" className="red">Red text</p>
  <p data-type="a" className="blue">Blue text</p>
</div>

Red text

Blue text

Targeting Nested Child Elements

When you need to target deeply nested child elements, you can use the & selector with a path that describes the hierarchy.

<div className="[&>div>div>span]:text-red-600">
  <div>
    <div>
      <span>Deep Child</span>
    </div>
  </div>
</div>
Deep Child

Conditional Styling Based on Parent Tag

When you need to style child elements based on the tag of their parent, you can use the in-[tag] variant to check if the element is inside a specific parent tag.

<p>
  <span className="in-[p]:text-red-600">Styled if parent is p</span>
</p>
<div>
  <span className="in-[p]:text-red-600">Styled if parent is p</span>
</div>

Styled if parent is p

Styled if parent is p

Conditional Styling Based on Parent Class

When you need to style child elements based on the class of their parent, you can use the in-[.classname] variant to check if the element is inside a parent with a specific class.

<div className="foo">
  <div>
    <div className="in-[.foo]:text-red-600">Child 1</div>
    <div className="in-[.bar]:text-red-600">Child 2</div>
  </div>
</div>
Child 1
Child 2

Conditional Styling Based on Parent Attribute

When you need to style child elements based on a specific attribute of their parent, you can use the in-data-[attribute="value"] variant to check if the element is inside a parent with that data attribute.

<div data-type="special">
  <p className="in-data-[type='special']:text-red-600">Styled if parent has data-type="special"</p>
</div>
<div>
  <p className="in-data-[type='special']:text-red-600">Not styled</p>
</div>

Styled if parent has data-type="special"

Not styled

Targeting by nth-child

You can use the & selector with :nth-child(n) to target specific children based on their position within the parent.

<div className="*:nth-[3]:text-red-600">
  <p>One</p>
  <p>Two</p>
  <p>Three</p>
</div>

One

Two

Three

To combine multiple nth-child selectors to target different children, stack a separate variant per match.

<div className="*:first:text-red-600 *:nth-[3]:text-red-600">
  <p>One</p>
  <p>Two</p>
  <p>Three</p>
</div>

One

Two

Three

Advanced Combinations

Target second child inside .foo only

When you want to target a specific child element inside a parent with a specific class, you can use the & selector with :nth-child(n).

<div className="[&>div.foo>*:nth-child(2)]:text-red-600">
  <div className="foo">
    <p>1</p>
    <p>2</p>
    <p>3</p>
  </div>
  <div className="bar">
    <p>Not affected</p>
  </div>
</div>

1

2

3

Not affected

Conditional nth-child with Exclusion

When you need to exclude certain elements from a universal styling rule, you can use the :not() pseudo-class.

<div className="*:not-[.excluded]:text-red-600">
  <div>
    <div>Not Excluded</div>
  </div>
  <div className="excluded">
    <div>Excluded</div>
  </div>
</div>
Not Excluded
Excluded
<div className="*:not-[[excluded]]:text-red-600">
  <div>
    <div>Not Excluded</div>
  </div>
  <div excluded>
    <div>Excluded</div>
  </div>
</div>
Not Excluded
Excluded

Pseudo + Tag + Descendant + Modifier

When you need to style elements based on a combination of pseudo-classes, tags, and modifiers, compose the canonical has-[...] variant with an arbitrary descendant filter.

<div className="has-[span]:[&>p]:text-red-600">
  <span>Trigger</span>
  <p>Styled paragraph</p> <!-- Styled because parent has span -->
</div>
<div className="has-[span]:[&>p]:text-red-600">
  <div>Trigger</div>
  <p>Styled paragraph</p>
</div>
Trigger

Styled paragraph

Trigger

Styled paragraph

Conclusion

Tailwind CSS arbitrary variants provide powerful tools for targeting child elements, styling based on parent context, and handling complex structures without the need for custom CSS or JavaScript. However, they should be used with care to maintain readability and code maintainability.

Cheat Sheet

SelectorUse Case
*:Target all direct children
**:Target all descendants
[&>tag]Target direct child elements by tag
[&_tag]Target all descendant elements by tag
[&>.class]Target direct children by class
[&_.class]Target all descendants by class
[&>div.class1.class2]Target direct children with multiple classes
*:data-[x="y"]Target direct children with data attributes
**:data-[x="y"]Target any nested element with data attributes
[&_[data-type='a'].class]Combine attributes and classes
[&>div>div>span]Target deeply nested children with specific path
in-[tag]Style based on parent's tag
in-[.parent]Style based on parent's class
in-data-[x="y"]Style based on parent's attributes
*:nth-[N]Target the Nth direct child
*:first / *:last / *:odd / *:evenCommon position shortcuts
*:not-[.excluded]Exclude direct children by class
*:not-[[excluded]]Exclude direct children by attribute
has-[selector]Element has matching descendant (canonical for :has())
has-[span]:[&>p]Style direct children when element contains a specific descendant