Variants in Tailwind CSS allow you to apply styles conditionally based on state, viewport size, or other conditions. Tailwind v4 includes a comprehensive set of built-in variants and provides APIs for creating custom ones.
Built-in Variants
Tailwind includes many variants out of the box:
Pseudo-class Variants
Interactive States
Form States
Positional
< button class = "hover:bg-blue-600 focus:ring-2 active:scale-95" >
Interactive button
</ button >
Pseudo-element Variants
< p class = "first-line:uppercase first-letter:text-7xl" >
Styled text
</ p >
< div class = "before:content-['→'] after:content-['←']" >
Content with pseudo-elements
</ div >
< input class = "placeholder:italic placeholder:text-gray-400" placeholder = "Search..." />
Responsive
Dark Mode
Reduced Motion
< div class = "text-sm md:text-base lg:text-lg" >
Responsive text
</ div >
Variant Categories
State Variants
Applied on hover (with @media (hover: hover) wrapper) < button class = "hover:bg-blue-600" > Hover me </ button >
Applied when element has focus < input class = "focus:ring-2 focus:ring-blue-500" />
Applied when element is being activated < button class = "active:scale-95" > Press me </ button >
Applied to visited links < a class = "visited:text-purple-600" > Link </ a >
Applied when element is disabled < button class = "disabled:opacity-50" disabled > Disabled </ button >
<!-- Validation states -->
< input class = "valid:border-green-500 invalid:border-red-500" />
<!-- Checked states -->
< input type = "checkbox" class = "checked:bg-blue-600" />
<!-- Indeterminate state -->
< input type = "checkbox" class = "indeterminate:bg-gray-600" />
<!-- Required/Optional -->
< input class = "required:border-red-500 optional:border-gray-300" />
Positional Variants
< ul >
< li class = "first:pt-0" > First item </ li >
< li class = "last:pb-0" > Last item </ li >
< li class = "only:border-0" > Only item </ li >
< li class = "odd:bg-gray-100" > Odd items </ li >
< li class = "even:bg-white" > Even items </ li >
</ ul >
Group & Peer Variants
Apply styles based on parent or sibling state:
< div class = "group" >
< img class = "group-hover:opacity-75" />
< h3 class = "group-hover:text-blue-600" > Title </ h3 >
</ div >
Responsive Variants
The default breakpoints:
-- breakpoint - sm : 40 rem /* 640px */
-- breakpoint - md : 48 rem /* 768px */
-- breakpoint - lg : 64 rem /* 1024px */
-- breakpoint - xl : 80 rem /* 1280px */
-- breakpoint - 2 xl : 96 rem /* 1536px */
Usage:
< div class = "text-sm sm:text-base md:text-lg lg:text-xl" >
Responsive sizing
</ div >
Custom Variants with Plugins
Create custom variants using the plugin API:
Static Variant
Add a simple static variant:
import type { Config } from 'tailwindcss'
import plugin from 'tailwindcss/plugin'
export default {
plugins: [
plugin (({ addVariant }) => {
// Add a `third` variant
addVariant ( 'third' , '&:nth-child(3)' )
// Add a `hocus` variant (hover or focus)
addVariant ( 'hocus' , [ '&:hover' , '&:focus' ])
// Add an `optional` variant for optional form elements
addVariant ( 'optional' , '&:optional' )
// Add a `supports-grid` variant
addVariant ( 'supports-grid' , '@supports (display: grid)' )
}),
] ,
} satisfies Config
Usage:
< div class = "third:bg-blue-500" > Third child is blue </ div >
< button class = "hocus:ring-2" > Hover or focus for ring </ button >
Functional Variant
Create variants that accept values:
import plugin from 'tailwindcss/plugin'
export default {
plugins: [
plugin (({ matchVariant }) => {
// Add an `nth` variant
matchVariant (
'nth' ,
( value ) => `&:nth-child( ${ value } )` ,
)
// Add a `data` variant with values
matchVariant (
'data' ,
( value ) => `&[data- ${ value } ]` ,
{
values: {
checked: 'checked' ,
active: 'active' ,
disabled: 'disabled' ,
},
},
)
}),
] ,
}
Usage:
< div class = "nth-3:bg-blue-500" > Third child </ div >
< div class = "nth-[2n]:bg-gray-100" > Even children </ div >
< div class = "data-active:bg-green-500" data-active > Active state </ div >
Compound Variants
Create variants that modify other variants:
import plugin from 'tailwindcss/plugin'
export default {
plugins: [
plugin (({ addVariant }) => {
// Add a custom group variant
addVariant ( 'group-optional' , ':merge(.group):optional &' )
// Add a custom peer variant
addVariant ( 'peer-optional' , ':merge(.peer):optional ~ &' )
}),
] ,
}
Functional Variants
Some variants accept arbitrary values:
< div class = "nth-3:bg-blue-500" > 3rd child </ div >
< div class = "nth-[2n+1]:bg-gray-100" > Odd children </ div >
Stacking Variants
Combine multiple variants:
<!-- Responsive + hover -->
< button class = "md:hover:bg-blue-600" > Desktop hover </ button >
<!-- Dark mode + hover + focus -->
< button class = "dark:hover:focus:ring-4" > Complex state </ button >
<!-- Group + responsive + hover -->
< div class = "group-hover:md:scale-110" > Scales on group hover (desktop) </ div >
Variant Order
Variants are applied in a specific order (roughly):
Responsive variants (sm:, md:, etc.)
State variants (hover:, focus:, etc.)
Compound variants (group-*:, peer-*:)
Pseudo-elements (before:, after:, etc.)
The order of variants in your class name doesn’t matter - Tailwind handles ordering automatically based on CSS specificity rules.
Advanced Variant Examples
Custom Dark Mode
Implement a custom dark mode strategy:
export default {
darkMode: [ 'variant' , '&:is(.dark *)' ] ,
}
RTL Support
import plugin from 'tailwindcss/plugin'
export default {
plugins: [
plugin (({ addVariant }) => {
addVariant ( 'rtl' , '&:is([dir="rtl"] *)' )
addVariant ( 'ltr' , '&:is([dir="ltr"] *)' )
}),
] ,
}
Usage:
< div class = "ml-4 rtl:mr-4 rtl:ml-0" >
RTL-aware margin
</ div >
Container Queries
Use container query variants:
< div class = "@container" >
< div class = "@md:text-lg @lg:text-xl" >
Responsive to container size
</ div >
</ div >
Not Variant
Negate other variants:
< div class = "not-first:mt-4" > Margin except first </ div >
< div class = "not-last:border-b" > Border except last </ div >
< div class = "not-hover:opacity-75" > Opacity when not hovered </ div >
Variant Modifiers
Some variants support modifiers:
<!-- Named group -->
< div class = "group/card" >
< div class = "group-hover/card:opacity-100" >
Only affected by .group/card
</ div >
</ div >
<!-- Named peer -->
< input class = "peer/email" type = "email" />
< span class = "peer-invalid/email:visible" > Invalid email </ span >
Best Practices
Use semantic variants - hover:, focus:, etc. are more maintainable than arbitrary selectors
Leverage responsive variants - Build mobile-first with progressive enhancement
Group related states - Use group and peer for parent/sibling relationships
Be mindful of specificity - More variants = higher specificity
Stacking too many variants can create overly specific selectors that are hard to override. Consider extracting complex variant combinations into component classes.