Docs
Next

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

VariantAppliesNote
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:

is-[.active]
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:

group-is-[.open]
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.

peer-is-[…]
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.