Tailwind CSS 41 🎨 Custom Boolean and Value Data Attributes Variants (data-*)
Data attributes are the bridge between JavaScript-managed state and CSS styling. A component’s state — whether a menu is open, whether a tab is active, whether a form is submitting — often lives in a JavaScript variable or a framework’s internal state. The data attribute makes that state visible to CSS. data-state="open" on a dropdown element is a signal that the dropdown is open, and Tailwind’s data-* variants read that signal and apply the corresponding styles. The attribute is the contract between the imperative world of JavaScript and the declarative world of utility classes.
Tailwind v4 simplified the syntax for these variants. In v3, a data attribute variant required brackets: data-[state=open]:bg-blue-500. In v4, the brackets are optional when the value is a bare word. data-state=open is written as data-open:bg-blue-500, and data-size=large is written as data-large:p-8 . The variant matches the attribute name and the value, and the compiler generates the selector [data-open] or [data-size=large]. Boolean attributes — those that exist or do not exist — are written without a value at all. data-active:font-bold applies the font weight when the element has a data-active attribute, regardless of its value .
The value-based variants are the ones that matter for component state. A tab interface has data-state="active" on the active tab and data-state="inactive" on the others. A button has data-variant="primary" or data-variant="secondary". A loading state has data-loading="true" or just data-loading. The variant syntax mirrors the attribute: data-active:bg-white, data-variant=primary:bg-blue-500, data-loading:opacity-50. Each variant is a conditional class, and the condition is the presence or value of the data attribute. Tailwind’s compiler generates the selector, and the browser applies the style when the attribute matches .
This chapter covers three areas. First, why data attribute variants exist — the problem of styling JavaScript-managed state and the contract between the DOM attribute and the CSS selector. Second, how Boolean and value variants work — the data-* syntax, the bare value shorthand, the difference between presence checks and value checks, and the custom variant shortcuts for repeated patterns. Third, how to use them in real components — tabs, dropdowns, loading states, and the group-data-* and peer-data-* patterns for parent and sibling state. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the attribute-variant flow.
Key point: data-* variants style elements based on their data attributes. Boolean variants like data-active: apply when the attribute is present. Value variants like data-variant=primary: apply when the attribute has a specific value. Tailwind v4 allows bare values (data-active: instead of data-[active]:), and custom shortcuts can be defined in @theme for repeated patterns.
Why data attribute variants exist
The JavaScript-state problem. CSS has no direct access to JavaScript variables. A component’s state — isOpen, isActive, isLoading — lives in JavaScript, and the styles need to change when the state changes. The traditional solutions are to toggle a class or to inline the styles. Toggling a class works, but the class name is arbitrary and the CSS must be written to match. Inline styles work, but they cannot be expressed as utility classes, and they bypass the entire Tailwind system. Data attributes solve this by making the state visible to CSS as a first-class attribute. The JavaScript sets data-state="open", and the CSS reads it. The attribute is the interface, and the styling is declarative .
The value-check problem. A class toggle is binary: the class is present or absent. A data attribute can carry a value. data-state="loading" and data-state="error" are different states, and the styles can differ. The data-* variant supports both presence checks and value checks. data-loading: applies when the attribute is present; data-state=loading: applies when the value matches. This makes a single attribute a state machine, with each value corresponding to a different style. The alternative — one class per state — would require multiple classes and multiple toggles, and the CSS would need to handle the combinations .
The component-variant problem. A button component has variants: primary, secondary, danger, ghost. The variant is a prop, and it needs to be reflected in the styles. The data attribute is the natural place to store it: data-variant="primary". The data-variant=primary: variant applies the primary styles, and data-variant=secondary: applies the secondary styles. The component’s markup is the same; only the attribute changes. The styles are declarative, and the variant is a value, not a class name. This pattern is common in headless component libraries, where the component provides the behavior and the consumer provides the styling .
The bare-value shorthand. In Tailwind v3, the data attribute variant required brackets: data-[state=open]:. The brackets were the signal to the compiler that the content was an arbitrary value. In v4, the brackets are optional when the value is a bare word — a sequence of characters that does not contain spaces or special characters. data-state=open is written as data-state=open, and data-size=large is written as data-size=large. The compiler recognizes the pattern and generates the selector. The shorthand reduces the visual noise and makes the variants easier to read .
The custom-shortcut problem. Some data attribute patterns are repeated across a project. The state attribute data-state with values active, loading, error appears in many components. Writing data-state=active: every time is verbose. Tailwind allows custom shortcuts to be defined in the @theme block. A variable named --data-state-active with the value [data-state='active'] creates a data-state-active: variant. The shortcut is shorter, and the selector is defined in one place. The trade-off is a small amount of configuration for a significant reduction in repetition .
The trade-off. Data attribute variants are a low-level tool. They require the developer to set the attribute in JavaScript and to write the variant in the class list. The attribute name and the variant name must match, and a typo in either produces a silent failure — the style does not apply, and there is no error. The tool is also more verbose than a simple class toggle. The alternative — a single class like .is-loading — is shorter, but it is not composable with Tailwind’s utility system. The trade-off is between the flexibility of a value-carrying attribute and the simplicity of a boolean class.
a. Boolean data attribute variants
A Boolean data attribute is one that is present or absent. The value does not matter; the presence is the signal. In HTML, a Boolean attribute can be written as data-active or data-active="". The data-* variant matches both forms .
<div data-active class="data-active:bg-blue-500 data-active:text-white">
Active content
</div>
The data-active: variant applies the styles when the element has the data-active attribute. The attribute is set by JavaScript, and the styles change when the attribute is added or removed. The variant is a presence check, and the value — if present — is irrelevant.
The syntax in v4 is the bare name: data-active:. In v3, the equivalent was data-[active]: with the brackets. The v4 compiler recognizes the bare name and generates the selector [data-active]. The selector matches any element with the attribute, regardless of the value .
Boolean attributes are the right choice for binary state: a menu is open or closed, a tab is active or inactive, a form is submitting or not. The attribute is a flag, and the styles are conditional on the flag. The JavaScript sets the attribute with element.setAttribute('data-active', '') or with a framework’s attribute binding. The CSS reads it, and the styles apply.
b. Value data attribute variants
A value data attribute carries a string. The variant matches the value, not just the presence. The syntax is data-attribute=value:. The compiler generates the selector [data-attribute=value] .
<div data-state="loading" class="data-state=loading:opacity-50">
Loading...
</div>
The data-state=loading: variant applies the opacity when the data-state attribute equals loading. If the attribute is data-state="error", the variant does not apply. The value is the condition, and the styles are specific to that value.
The value can be any bare word: primary, secondary, active, loading, large, small. The variant name is the attribute name plus the value, separated by =. The compiler parses the value, and the selector is generated. For values that contain spaces or special characters, the arbitrary value syntax with brackets is still available: data-[size=extra-large]:. The brackets are required when the value is not a bare word .
The value variants are the right choice for component variants and state machines. A button with data-variant="primary" uses data-variant=primary:bg-blue-500. A tab with data-state="active" uses data-state=active:border-b-2. The attribute is the state, and the value is the specific state. The styles are declarative, and the component’s logic is in JavaScript.
c. Custom shortcuts in @theme
The @theme block can define shortcuts for repeated data attribute patterns. A variable named --data-* creates a variant with the corresponding name. The value is the selector, written as a CSS attribute selector .
@import "tailwindcss";
@theme {
--data-state-active: [data-state='active'];
--data-state-loading: [data-state='loading'];
--data-orientation-vertical: [data-orientation='vertical'];
}
The --data-state-active variable creates a data-state-active: variant. The --data-state-loading variable creates data-state-loading:, and --data-orientation-vertical creates data-orientation-vertical:. The shortcuts are used like any other variant:
<div class="data-state-active:bg-white data-state-loading:opacity-50">
Content
</div>
The shortcut is shorter than data-state=active:, and the selector is defined once in the theme. If the attribute name or the value changes, the theme is updated, and every usage is updated. The shortcuts are a form of abstraction: the repeated pattern is named, and the name is used in the markup. The trade-off is the configuration overhead, which is justified when the pattern is used in many places .
The shortcuts can also be used with group-* and peer-*. The group-data-state-active: variant applies when an ancestor has data-state="active", and the peer-data-state-active: variant applies when a sibling has the attribute. The shortcuts compose with the existing variants, so the pattern is consistent.
Complete Example Session
<!-- ============================================
PART 1: BOOLEAN DATA ATTRIBUTE
============================================ -->
<div data-active class="data-active:bg-blue-500 data-active:text-white">
Active content
</div>
<!-- The data-active attribute is present.
The data-active: variant applies. -->
<!-- ============================================
PART 2: BOOLEAN ABSENT
============================================ -->
<div class="data-active:bg-blue-500 data-active:text-white">
Inactive content
</div>
<!-- The data-active attribute is absent.
The data-active: variant does not apply. -->
<!-- ============================================
PART 3: VALUE DATA ATTRIBUTE
============================================ -->
<div data-state="loading" class="data-state=loading:opacity-50">
Loading...
</div>
<!-- The data-state attribute equals "loading".
The data-state=loading: variant applies. -->
<!-- ============================================
PART 4: VALUE MISMATCH
============================================ -->
<div data-state="error" class="data-state=loading:opacity-50">
Error message
</div>
<!-- The data-state attribute equals "error".
The data-state=loading: variant does not apply. -->
<!-- ============================================
PART 5: COMPONENT VARIANTS
============================================ -->
<button
data-variant="primary"
class="data-variant=primary:bg-blue-500 data-variant=primary:text-white"
>
Primary Button
</button>
<button
data-variant="secondary"
class="data-variant=secondary:bg-gray-100 data-variant=secondary:text-gray-900"
>
Secondary Button
</button>
<!-- ============================================
PART 6: TAB INTERFACE
============================================ -->
<div class="flex gap-2">
<button
data-state="active"
class="data-state=active:border-b-2 data-state=active:border-blue-500"
>
Tab 1
</button>
<button
data-state="inactive"
class="data-state=active:border-b-2 data-state=active:border-blue-500"
>
Tab 2
</button>
</div>
<!-- The active tab has the border.
The inactive tab does not. -->
<!-- ============================================
PART 7: GROUP DATA ATTRIBUTE
============================================ -->
<div data-state="open" class="group">
<button class="group-data-state=open:bg-gray-100">
Toggle
</button>
<div class="group-data-state=open:block hidden">
Dropdown content
</div>
</div>
<!-- The parent has data-state="open".
The group-data-state=open: variant applies to children. -->
<!-- ============================================
PART 8: PEER DATA ATTRIBUTE
============================================ -->
<div>
<input type="checkbox" data-checked class="peer" />
<label class="peer-data-checked:text-green-600">
Checked
</label>
</div>
<!-- The input has data-checked.
The peer-data-checked: variant applies to the sibling label. -->
<!-- ============================================
PART 9: CUSTOM SHORTCUTS
============================================ -->
<style type="text/tailwindcss">
@import "tailwindcss";
@theme {
--data-state-active: [data-state='active'];
--data-state-loading: [data-state='loading'];
--data-state-error: [data-state='error'];
}
</style>
<div
data-state="active"
class="data-state-active:bg-white data-state-loading:opacity-50 data-state-error:bg-red-50"
>
Content
</div>
<!-- The shortcuts are defined in @theme.
The variant names are shorter and reusable. -->
<!-- ============================================
PART 10: THE COMPLETE COMPONENT
============================================ -->
<style type="text/tailwindcss">
@import "tailwindcss";
@theme {
--data-variant-primary: [data-variant='primary'];
--data-variant-secondary: [data-variant='secondary'];
--data-size-sm: [data-size='sm'];
--data-size-lg: [data-size='lg'];
--data-loading: [data-loading];
}
</style>
<button
data-variant="primary"
data-size="lg"
data-loading
class="
rounded-lg font-semibold transition
data-variant-primary:bg-blue-500 data-variant-primary:text-white
data-variant-secondary:bg-gray-100 data-variant-secondary:text-gray-900
data-size-sm:px-3 data-size-sm:py-1.5 data-size-sm:text-sm
data-size-lg:px-6 data-size-lg:py-3 data-size-lg:text-base
data-loading:opacity-60 data-loading:cursor-not-allowed
"
>
Submit
</button>
<!-- The component has three data attributes.
Each variant applies when the corresponding attribute matches.
The styles are declarative, and the component logic is in JavaScript. -->
The ten parts show a Boolean attribute present, a Boolean attribute absent, a value attribute match, a value mismatch, component variants, a tab interface, a group data attribute, a peer data attribute, custom shortcuts, and a complete component.
Quick Reference
Boolean vs Value Variants
| Type | Syntax | Applies when |
|---|---|---|
| Boolean | data-active: | Attribute is present |
| Value | data-state=open: | Attribute equals the value |
| Arbitrary value | data-[size=extra-large]: | Value contains spaces or special chars |
Common Variants
| Variant | Selector |
|---|---|
data-active: | [data-active] |
data-state=open: | [data-state=open] |
data-variant=primary: | [data-variant=primary] |
data-size=large: | [data-size=large] |
data-loading: | [data-loading] |
Group and Peer
| Variant | Applies when |
|---|---|
group-data-state=open: | Ancestor with data-state=open |
peer-data-checked: | Sibling with data-checked |
group-data-active: | Ancestor with data-active |
Custom Shortcuts
| Variable | Variant | Selector |
|---|---|---|
--data-state-active | data-state-active: | [data-state='active'] |
--data-state-loading | data-state-loading: | [data-state='loading'] |
--data-orientation-vertical | data-orientation-vertical: | [data-orientation='vertical'] |
Best Practices
✅ Do This:
<!-- Use Boolean variants for binary state -->
<div data-active class="data-active:bg-blue-500"> <!-- ✅ -->
<!-- Use value variants for component variants -->
<button data-variant="primary" class="data-variant=primary:bg-blue-500"> <!-- ✅ -->
<!-- Use group-data for parent state -->
<div data-state="open" class="group">
<div class="group-data-state=open:block hidden"> <!-- ✅ -->
<!-- Use peer-data for sibling state -->
<input data-checked class="peer" />
<label class="peer-data-checked:text-green-600"> <!-- ✅ -->
<!-- Define shortcuts for repeated patterns -->
@theme { --data-state-active: [data-state='active']; } <!-- ✅ -->
❌ Don’t Do This:
<!-- Don't use brackets for bare values in v4 -->
<div class="data-[active]:bg-blue-500"> <!-- use data-active: --> <!-- ❌ -->
<!-- Don't forget the attribute on the element -->
<div class="data-active:bg-blue-500"> <!-- no data-active attribute --> <!-- ❌ -->
<!-- Don't use spaces in bare values -->
<div data-size="extra large" class="data-size=extra large:p-8"> <!-- ❌ -->
<!-- Don't use a value variant for a Boolean check -->
<div data-active class="data-active=true:bg-blue-500"> <!-- use data-active: --> <!-- ❌ -->
<!-- Don't define shortcuts inside @layer -->
@layer base { --data-state-active: ...; } <!-- use @theme --> <!-- ❌ -->
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Variant not applying | Attribute missing or wrong value | Check the DOM attribute |
| Typo in variant name | Attribute name mismatch | Match the variant to the attribute |
| Brackets required | Value has spaces | Use data-[size=extra large]: |
| Shortcut not working | Defined outside @theme | Define in @theme block |
| Group variant not applying | Parent missing group class | Add class="group" to parent |
| Peer variant not applying | Sibling missing peer class | Add class="peer" to sibling |
| Boolean value irrelevant | Expecting value to matter | Presence is the signal |
Real-World Examples
1. Boolean Active
<div data-active class="data-active:bg-blue-500">
2. Value State
<div data-state="loading" class="data-state=loading:opacity-50">
3. Component Variant
<button data-variant="primary" class="data-variant=primary:bg-blue-500">
4. Tab Interface
<button data-state="active" class="data-state=active:border-b-2">
5. Group Data
<div data-state="open" class="group">
<div class="group-data-state=open:block hidden">
6. Peer Data
<input data-checked class="peer" />
<label class="peer-data-checked:text-green-600">
7. Custom Shortcut
@theme { --data-state-active: [data-state='active']; }
8. Arbitrary Value
<div class="data-[size=extra-large]:p-8">
9. Loading State
<div data-loading class="data-loading:opacity-60">
10. Complete Button
<button data-variant="primary" data-loading class="data-variant=primary:bg-blue-500 data-loading:opacity-60">
Visual
The Attribute-Variant Flow
┌──────────────────────────────────────────────────────────────┐
│ ATTRIBUTE-VARIANT FLOW │
│ │
│ JavaScript sets the attribute: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ element.setAttribute('data-state', 'open') │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ DOM: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <div data-state="open" │ │
│ │ class="data-state=open:bg-gray-100"> │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ CSS selector: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ [data-state=open] { background-color: #f3f4f6; } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Browser applies the style. │
│ │
│ The attribute is the interface. │
│ The variant is the condition. │
│ The style is the result. │
│ │
└──────────────────────────────────────────────────────────────┘
Boolean vs Value
┌──────────────────────────────────────────────────────────────┐
│ BOOLEAN vs VALUE │
│ │
│ BOOLEAN: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ data-active: │ │
│ │ │ │
│ │ <div data-active class="data-active:bg-blue-500"> │ │
│ │ → [data-active] { ... } │ │
│ │ │ │
│ │ Applies when the attribute is present. │ │
│ │ The value does not matter. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ VALUE: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ data-state=open: │ │
│ │ │ │
│ │ <div data-state="open" class="data-state=open:..."> │ │
│ │ → [data-state=open] { ... } │ │
│ │ │ │
│ │ Applies when the attribute equals the value. │ │
│ │ A different value does not apply. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
Group and Peer
┌──────────────────────────────────────────────────────────────┐
│ GROUP AND PEER │
│ │
│ GROUP: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <div data-state="open" class="group"> │ │
│ │ <button class="group-data-state=open:bg-gray-100">│ │
│ │ <div class="group-data-state=open:block hidden"> │ │
│ │ </div> │ │
│ │ │ │
│ │ The parent has the attribute. │ │
│ │ The children react to it. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ PEER: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ <input data-checked class="peer" /> │ │
│ │ <label class="peer-data-checked:text-green-600"> │ │
│ │ │ │
│ │ The sibling has the attribute. │ │
│ │ The label reacts to it. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
Custom Shortcuts
┌──────────────────────────────────────────────────────────────┐
│ CUSTOM SHORTCUTS │
│ │
│ WITHOUT SHORTCUT: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ data-state=active:bg-white │ │
│ │ data-state=loading:opacity-50 │ │
│ │ data-state=error:bg-red-50 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ WITH SHORTCUT: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ @theme { │ │
│ │ --data-state-active: [data-state='active']; │ │
│ │ --data-state-loading: [data-state='loading']; │ │
│ │ --data-state-error: [data-state='error']; │ │
│ │ } │ │
│ │ │ │
│ │ data-state-active:bg-white │ │
│ │ data-state-loading:opacity-50 │ │
│ │ data-state-error:bg-red-50 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The shortcut is shorter and the selector is in one place. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
data-* variant | Styles based on data attributes |
| Boolean variant | data-active: applies when attribute is present |
| Value variant | data-state=open: applies when value matches |
| Bare value shorthand | data-active: instead of data-[active]: |
| Arbitrary value | data-[size=extra large]: for spaces |
| Group variant | group-data-state=open: for parent state |
| Peer variant | peer-data-checked: for sibling state |
| Custom shortcut | --data-* in @theme |
| v4 requirement | Bare values need v4; v3 used brackets |
Key takeaways:
data-*variants style elements based on their data attributes. The attribute is set by JavaScript, and the variant reads it. The attribute is the contract between the imperative state and the declarative styling .- Boolean variants check for presence.
data-active:applies when the element has adata-activeattribute, regardless of its value. This is the right choice for binary state: open/closed, active/inactive, loading/not-loading . - Value variants check for a specific value.
data-state=open:applies when thedata-stateattribute equalsopen. This is the right choice for component variants and state machines: primary/secondary, loading/error/success . - Tailwind v4 allows bare values. The brackets are optional when the value is a bare word.
data-state=open:is written asdata-state=open:. The compiler generates the selector. For values with spaces or special characters, the brackets are still required . group-data-*andpeer-data-*extend the pattern. The group variant applies when an ancestor has the attribute; the peer variant applies when a sibling has the attribute. This is the standard pattern for parent-child and sibling state .- Custom shortcuts reduce repetition. The
@themeblock defines--data-*variables that create named variants. The shortcut is shorter than the full value variant, and the selector is defined once. Use it when the pattern is repeated across the project . - The attribute name and the variant name must match. A typo in either produces a silent failure. The style does not apply, and there is no error. This is the most common pitfall with data attribute variants.
- Data attribute variants are for JavaScript-managed state. If the state is static or controlled by CSS, a class toggle or a built-in variant is simpler. The data attribute is the bridge for state that lives in JavaScript and needs to be reflected in the styling.
Remember: data-* variants are the bridge between JavaScript-managed state and Tailwind’s utility system. The attribute is the interface: JavaScript sets it, CSS reads it, and the variant applies the style. Boolean variants check for presence; value variants check for a specific value. The bare-value shorthand in v4 makes the syntax cleaner, and the @theme shortcuts make repeated patterns reusable. The group-data-* and peer-data-* variants extend the pattern to ancestors and siblings. Use data attribute variants for state that JavaScript controls: component variants, loading states, open/closed flags, and active/inactive tabs. The attribute is the contract, and the variant is the condition.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!