> For the complete documentation index, see [llms.txt](https://analog.groupthought.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://analog.groupthought.com/reference/utilities/variants.md).

# Custom variants

Custom variants for input method, JavaScript availability, Alpine load state, viewport intersection, and feature support.

Custom variants for input method, JavaScript availability, Alpine load state, viewport intersection, and feature support.

| Variant                               | What it matches                                                                                                                           |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `keyboard-active:`                    | `[data-keyboard-active] &` while the shopper is navigating by keyboard. The `keyboardActive` Alpine plugin sets the attribute on `<body>` |
| `keyboard-focus:`                     | `[data-keyboard-active] &:focus` while the element has keyboard focus                                                                     |
| `keyboard-focus-within:`              | `[data-keyboard-active] &:focus-within` while a descendant has keyboard focus                                                             |
| `touch:`                              | `@media (pointer: coarse) and (hover: none)` for touch devices                                                                            |
| `mouse:`                              | `@media (pointer: fine) and (hover: hover)` for devices with a hover-capable pointer                                                      |
| `fine:` / `coarse:`                   | `@media (pointer: fine)` / `(pointer: coarse)` for the primary pointer                                                                    |
| `any-fine:` / `any-coarse:`           | `@media (any-pointer: fine)` / `(any-pointer: coarse)` for any attached pointer                                                           |
| `js:` / `nojs:`                       | `@media (scripting: enabled)` / `(scripting: none)`                                                                                       |
| `prealpine:`                          | `[data-alpine-loading] &` before Alpine initializes                                                                                       |
| `intersected:`                        | `[data-has-intersected='true']` on the element or an ancestor after it enters the viewport once                                           |
| `intersecting:`                       | `[data-is-intersecting='true']` on the element or an ancestor while it is in the viewport                                                 |
| `popover-open:` / `not-popover-open:` | `&:popover-open` / `&:not(:popover-open)`                                                                                                 |
| `placeholder-shown:`                  | `&:placeholder-shown` while the input is empty and showing its placeholder                                                                |
| `closed:`                             | `&:not([open])` on a closed `<dialog>` or `<details>`                                                                                     |
| `thumb:` / `track:`                   | The thumb / track pseudo-elements of `<input type="range">`, across browser engines                                                       |
| `supports-{feature}:`                 | `@supports` for a named feature: `subgrid`, `container`, `color-mix`, `oklch-from`, `popover`, `svh`, `aspect`                            |
| `no-{feature}:`                       | `@supports not` for the same feature names                                                                                                |
| `children-[n]:`                       | The element has exactly `n` direct children. Also `[>n]` (more than), `[<n]` (fewer than), and `[n-m]` (between)                          |
| `group-children-[n]:`                 | Same counts, tested on the nearest `.group` ancestor                                                                                      |

The theme adds no motion variants. Reduced-motion styling uses Tailwind's own `motion-safe:` and `motion-reduce:` variants.

## Examples

### Keyboard focus

Use `keyboard-focus:` to adjust the focus outline only when the shopper is tabbing:

```html
<button role="tab" class="keyboard-focus:outline-offset-[-2px] flex-row-center h-btn">
  {{ label }}
</button>
```

The global focus ring already appears only under `[data-keyboard-active]`. Use `keyboard-focus:` for per-element overrides, such as moving the outline inside a clipped tab. Use `keyboard-focus:!outline-none` on an input whose wrapper draws the ring.

### Touch and mouse devices

Use `touch:` and `mouse:` to swap controls between input methods:

```html
<button class="touch:hidden flex flex-center rounded-pill px-btn" x-slideshow:prev>…</button>
```

The slideshow hides hover arrows on touch devices since there is no hover to reveal them. Shoppers can swipe instead. Product cards use the same variants to replace hover quick add with a tap target.

### No JavaScript

Use `nojs:` to keep content usable when JavaScript is off:

```html
<!-- Collapsed accordion content stays readable -->
<div x-collapse.duration.200ms class="w-full nojs:!block">…</div>

<!-- Stepper buttons need JS, so hide them -->
<button class="nojs:hidden" @click.prevent="$stepper.decrement()">…</button>
```

### Before Alpine loads

Use `prealpine:` to hold a stable state until Alpine initializes:

```html
<div class="prealpine:[&>:not(:first-of-type)]:!hidden">
  {{ tab_panels }}
</div>
```

Tabs and accordions show only their first panel until Alpine takes over, so the page does not flash every panel at once during load.

### Scrolled into view

Use `intersected:` with a paused animation to start it when the element enters the viewport:

```html
<span class="animate-highlight-wipe-in !paused intersected:!running">
  {{ text }}
</span>
```

The section intersection manager sets `data-has-intersected` on the section once and never removes it, so the animation runs exactly once. `data-is-intersecting` toggles as the section scrolls in and out. Use `intersecting:` for styles that should reverse.

### Feature support

Use `supports-{feature}:` and `no-{feature}:` to branch on browser features named in the config:

```html
<div class="supports-subgrid:grid-cols-subgrid supports-subgrid:col-[fullbleed-start/fullbleed-end]">…</div>

<div class="no-aspect:min-h-[--card-fallback-height] aspect-[--card-aspect-ratio]">…</div>
```

Tailwind's arbitrary form also works for one-off checks: `supports-[field-sizing:content]:…`.

### Child counts

Use `children-[n]:` to style a container by how many children it has:

```html
<ul class="children-[1]:[--columns:1] children-[2]:[--columns:2] children-[3]:[--columns:3] children-[>8]:[--columns:4]">
  {{ menu_columns }}
</ul>
```

The megamenu grid picks its column count from how many columns the merchant added.

## Customizing

Each variant is defined in a small plugin in `tailwind/` (`keyboard-active.ts`, `pointer-media.ts`, `alpine.ts`, `intersection.ts`, `popover.ts`, `no-support.ts`) or in `@groupthought/assembly-ui/tailwind` (`js:`/`nojs:`, `closed:`, `thumb:`/`track:`). The feature names behind `supports-*` and `no-*` live in `theme.supports` in `tailwind.config.ts`. `children-[…]:` comes from the `tailwindcss-quantity-queries` package.
