is-* variants
is-[…], group-is-* and peer-is-* for :is() selectors.
On this page
The is-[…] variant applies a utility when the element matches the selector list in the brackets, through CSS :is(). group-is-[…] does the same for the nearest group ancestor and peer-is-[…] for a preceding peer sibling, so state classes that a script toggles (.active, .is-open) can drive styles without custom CSS.
Quick referenceLink to this section
| Variant | Applies | Note |
|---|---|---|
is-[.active]: | &:is(.active) | The element matches the selector in brackets. |
group-is-[.active]: | &:is(:where(.group):is(.active) *) | The closest group ancestor matches the selector. |
peer-is-[:checked]: | &:is(:where(.peer):is(:checked) ~ *) | A preceding peer sibling matches the selector. |
Basic usageLink to this section
is-[.active]:font-bold styles the element while it has the active class. Any selector :is() accepts goes in the brackets, including a comma-separated list: is-[:hover,:focus-visible]:underline matches either state. Click the tabs below; the script only moves the active class, and the variant does the rest:
import { useState } from 'react'
const tabs = ['Overview', 'Pricing', 'Reviews']
export default function Demo() {
const [current, setCurrent] = useState('Overview')
return (
<div className="flex gap-1 rounded-lg border border-(--color-foundations-surface-border) p-1">
{tabs.map((tab) => (
<button
key={tab}
type="button"
aria-pressed={tab === current}
onClick={() => setCurrent(tab)}
className={`rounded-md px-4 py-2 color-body is-[:hover,:focus-visible]:underline is-[.active]:bg-(--color-foundations-primary-bg) is-[.active]:text-(--color-foundations-primary-text) ${tab === current ? 'active' : ''}`}
>
{tab}
</button>
))}
</div>
)
}<button class="rounded-md px-4 py-2 is-[.active]:bg-(--color-foundations-primary-bg) active">
Overview
</button>Group and peerLink to this section
group-is-[…] styles a child by the state of an ancestor marked group. That fits components whose state lives on the root, such as an open menu or an expanded panel:
Orders ship within two working days.
import { useState } from 'react'
export default function Demo() {
const [open, setOpen] = useState(false)
return (
<div
className={`group w-full max-w-[24rem] rounded-lg border border-(--color-foundations-surface-border) ${open ? 'open' : ''}`}
>
<button
type="button"
aria-expanded={open}
onClick={() => setOpen(!open)}
className="flex w-full items-center justify-between px-4 py-3 color-heading"
>
Shipping details
<svg
aria-hidden="true"
focusable="false"
viewBox="0 0 16 16"
className="h-4 w-4 transition-transform group-is-[.open]:rotate-180"
>
<path d="M3 6l5 5 5-5" fill="none" stroke="currentColor" strokeWidth="1.5" />
</svg>
</button>
<p className="hidden px-4 pb-4 text-small color-body group-is-[.open]:block">
Orders ship within two working days.
</p>
</div>
)
}peer-is-[…] styles an element by the state of an earlier sibling marked peer. Type a malformed address and leave the field: the hint appears without a line of script.
Enter a valid email address.
export default function Demo() {
return (
<div className="flex w-full max-w-[24rem] flex-col gap-2">
<label htmlFor="is-demo-email" className="form-label">
Email
</label>
<input
id="is-demo-email"
type="email"
placeholder="you@example.com"
className="peer form-input"
/>
<p className="hidden text-small text-(--color-foundations-destructive) peer-is-[:invalid:not(:placeholder-shown)]:block">
Enter a valid email address.
</p>
</div>
)
}Both take Tailwind's named form when groups or peers nest: mark the ancestor group/menu and write group-is-[.open]/menu:block; mark the sibling peer/email and write peer-is-[:invalid]/email:block.
Responsive and state variantsLink to this section
The is-* variants stack with breakpoint prefixes and with each other: md:is-[.active]:bg-base-100 applies from md up, mm-sm:group-is-[.open]:block only on phones (see Breakpoints and ranges).
They pair well with the state classes Systhema's own listeners write: the header menu toggles is-open, scroll listeners write scroll-on-top and is-scrolling-down on <body>, and revealed elements get animated. For example, group-is-[.scroll-on-top]:shadow-none on a header child with group on <body> (see Listeners).
CustomizingLink to this section
There is nothing to configure: the selector is whatever you put in the brackets. Spaces are not allowed inside a class name, so write them as underscores, as in any Tailwind arbitrary value: is-[.card_>_.title]: matches .card > .title.