zfb
GitHub repository

Type to search...

to open search from anywhere

Variants

Apply the configured responsive, dark, relation, state, and pseudo-element variants in canonical order.

Apply a variant to a task

Variants add a condition to a complete utility candidate. zudo-wind has no default breakpoints or dark mode; configure both before using them:

{
  "wind": {
    "breakpoints": { "sm": { "minWidthPx": 640 } },
    "dark": { "attribute": "data-theme", "value": "dark" }
  }
}

Use the controls on each compiled preview to resize it, hover or focus the control, and compare the light and dark surfaces. The preview configuration is explicit and its CSS comes from the workspace compiler.

Change layout at a configured width

An unprefixed candidate applies at every width; sm:grid-cols-2 adds the second column from the configured 640px threshold.

Resize the responsive grid
HTML
<div class="wind-demo-board grid grid-cols-1 gap-3 sm:grid-cols-2">
  <div class="wind-demo-card">One column below 640px</div>
  <div class="wind-demo-card">Two columns from 640px</div>
  <div class="wind-demo-card">Resize the preview</div>
</div>
CSS
@layer zw-reset, zw-tokens, zfb-hi, base, components;
@layer zw-tokens {
  :root {
    --zw-spacing-unit: .25rem;
  }
}
.wind-demo-board {
  font: 14px / 1.5 system-ui, sans-serif;
}

.wind-demo-card {
  min-height: 3rem;
  text-align: center;
  border: 1px solid #8797aa;
  background: #f4f1ec;
}
.grid {
  display: grid;
}
.grid-cols-1 {
  grid-template-columns: repeat(1,minmax(0,1fr));
}
.gap-3 {
  column-gap: 0.75rem;
  row-gap: 0.75rem;
}
@media (min-width: 640px) {
  .sm\:grid-cols-2 {
    grid-template-columns: repeat(2,minmax(0,1fr));
  }
}
Configuration for this example
{
  "wind": {
    "spec": 1,
    "reset": "none",
    "strict": true,
    "tokens": {
      "spacingUnit": "0.25rem"
    },
    "breakpoints": {
      "sm": {
        "minWidthPx": 640
      }
    },
    "defaultTransitionTimingFunction": "ease",
    "authoredClasses": {
      "wind-demo-board": true,
      "wind-demo-card": true
    },
    "safelist": {
      "docs-preview": [
        "grid",
        "grid-cols-1",
        "gap-3",
        "sm:grid-cols-2"
      ]
    }
  }
}
Authored demonstration CSS
.wind-demo-board { font: 14px/1.5 system-ui, sans-serif; }
.wind-demo-card { min-height: 3rem; text-align: center; border: 1px solid #8797aa; background: #f4f1ec; }

Scope a rule to dark mode

dark: matches the configured attribute on the element itself or any ancestor. This example marks one card with data-theme="dark" so both modes remain visible together.

Compare configured light and dark values
HTML
<div class="wind-demo-dark-grid">
  <div class="wind-demo-color-card bg-paper text-ink p-4">Light surface</div>
  <div class="wind-demo-color-card bg-paper text-ink dark:bg-night dark:text-night-ink p-4" data-theme="dark">Dark surface from this element’s configured attribute</div>
</div>
CSS
@layer zw-reset, zw-tokens, zfb-hi, base, components;
@layer zw-tokens {
  :root {
    --zw-color-ink: #253047;
    --zw-color-night: #263449;
    --zw-color-night-ink: #f5f7fb;
    --zw-color-paper: #f5f7fb;
    --zw-spacing-unit: .25rem;
  }
}
.wind-demo-dark-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: .75rem;
  font: 14px / 1.5 system-ui, sans-serif;
}

.wind-demo-color-card {
  border: 1px solid #8797aa;
}
.p-4 {
  padding-top: 1rem;
  padding-right: 1rem;
  padding-bottom: 1rem;
  padding-left: 1rem;
}
.text-ink {
  color: var(--zw-color-ink);
}
.bg-paper {
  background-color: var(--zw-color-paper);
}
.dark\:text-night-ink:where([data-theme="dark"], [data-theme="dark"] *) {
  color: var(--zw-color-night-ink);
}
.dark\:bg-night:where([data-theme="dark"], [data-theme="dark"] *) {
  background-color: var(--zw-color-night);
}
Configuration for this example
{
  "wind": {
    "spec": 1,
    "reset": "none",
    "strict": true,
    "tokens": {
      "spacingUnit": "0.25rem",
      "colors": {
        "paper": "#f5f7fb",
        "ink": "#253047",
        "night": "#263449",
        "night-ink": "#f5f7fb"
      }
    },
    "breakpoints": {},
    "defaultTransitionTimingFunction": "ease",
    "dark": {
      "attribute": "data-theme",
      "value": "dark"
    },
    "authoredClasses": {
      "wind-demo-dark-grid": true,
      "wind-demo-color-card": true
    },
    "safelist": {
      "docs-preview": [
        "bg-paper",
        "text-ink",
        "p-4",
        "dark:bg-night",
        "dark:text-night-ink"
      ]
    }
  }
}
Authored demonstration CSS
.wind-demo-dark-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem; font: 14px/1.5 system-ui, sans-serif; }
.wind-demo-color-card { border: 1px solid #8797aa; }

Respond to an element state

State variants target the utility's own element. The first button changes on hover and focus-visible; the disabled button shows a state that does not depend on pointer input.

Try hover, focus, and disabled states
HTML
<button class="wind-demo-button bg-accent text-paper px-4 py-2 hover:bg-accent-strong focus-visible:bg-accent-strong disabled:opacity-50">Hover or focus this button</button>
<button class="wind-demo-button bg-accent text-paper px-4 py-2 disabled:opacity-50" disabled>Disabled state</button>
CSS
@layer zw-reset, zw-tokens, zfb-hi, base, components;
@layer zw-tokens {
  :root {
    --zw-color-accent: #5b5bd6;
    --zw-color-accent-strong: #38389a;
    --zw-color-paper: #fff;
    --zw-spacing-unit: .25rem;
  }
}
.wind-demo-button {
  margin: .25rem;
  border: 0;
  border-radius: 9999px;
  font: 600 14px / 1.4 system-ui, sans-serif;
  cursor: pointer;
}
.px-4 {
  padding-left: 1rem;
  padding-right: 1rem;
}
.py-2 {
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
}
.text-paper {
  color: var(--zw-color-paper);
}
.bg-accent {
  background-color: var(--zw-color-accent);
}
@media (hover: hover) {
  .hover\:bg-accent-strong:hover {
    background-color: var(--zw-color-accent-strong);
  }
}
.focus-visible\:bg-accent-strong:focus-visible {
  background-color: var(--zw-color-accent-strong);
}
.disabled\:opacity-50:disabled {
  opacity: 0.5;
}
Configuration for this example
{
  "wind": {
    "spec": 1,
    "reset": "none",
    "strict": true,
    "tokens": {
      "spacingUnit": "0.25rem",
      "colors": {
        "accent": "#5b5bd6",
        "accent-strong": "#38389a",
        "paper": "#ffffff"
      }
    },
    "breakpoints": {},
    "defaultTransitionTimingFunction": "ease",
    "authoredClasses": {
      "wind-demo-button": true
    },
    "safelist": {
      "docs-preview": [
        "bg-accent",
        "text-paper",
        "px-4",
        "py-2",
        "hover:bg-accent-strong",
        "focus-visible:bg-accent-strong",
        "disabled:opacity-50"
      ]
    }
  }
}
Authored demonstration CSS
.wind-demo-button { margin: .25rem; border: 0; border-radius: 9999px; font: 600 14px/1.4 system-ui, sans-serif; cursor: pointer; }

Place an unnamed group marker on an ancestor or an unnamed peer marker on a preceding sibling. Relation variants style a descendant or later sibling when that marker has the requested state.

Try group and preceding-peer relations
HTML
<div class="wind-demo-relations">
  <div class="wind-demo-link group text-ink">
    <span class="group-hover:text-accent">Hover this group to color its child</span>
  </div>
  <label class="wind-demo-choice">
    <input class="peer" type="checkbox" />
    <span class="peer-focus:text-accent">Tab to the preceding peer control</span>
  </label>
</div>
CSS
@layer zw-reset, zw-tokens, zfb-hi, base, components;
@layer zw-tokens {
  :root {
    --zw-color-accent: #5b5bd6;
    --zw-color-ink: #253047;
    --zw-spacing-unit: .25rem;
  }
}
.wind-demo-relations {
  display: grid;
  gap: .75rem;
  font: 14px / 1.5 system-ui, sans-serif;
}

.wind-demo-link {
  display: block;
  padding: .75rem;
  border: 1px solid #8797aa;
}

.wind-demo-choice {
  display: flex;
  align-items: center;
  gap: .5rem;
}
.text-ink {
  color: var(--zw-color-ink);
}
@media (hover: hover) {
  :where(.group:hover) .group-hover\:text-accent {
    color: var(--zw-color-accent);
  }
}
:where(.peer:focus) ~ .peer-focus\:text-accent {
  color: var(--zw-color-accent);
}
Configuration for this example
{
  "wind": {
    "spec": 1,
    "reset": "none",
    "strict": true,
    "tokens": {
      "spacingUnit": "0.25rem",
      "colors": {
        "ink": "#253047",
        "accent": "#5b5bd6"
      }
    },
    "breakpoints": {},
    "defaultTransitionTimingFunction": "ease",
    "authoredClasses": {
      "wind-demo-relations": true,
      "wind-demo-link": true,
      "wind-demo-choice": true
    },
    "safelist": {
      "docs-preview": [
        "group",
        "text-ink",
        "group-hover:text-accent",
        "peer",
        "peer-focus:text-accent"
      ]
    }
  }
}
Authored demonstration CSS
.wind-demo-relations { display: grid; gap: .75rem; font: 14px/1.5 system-ui, sans-serif; }
.wind-demo-link { display: block; padding: .75rem; border: 1px solid #8797aa; }
.wind-demo-choice { display: flex; align-items: center; gap: .5rem; }

Add a pseudo-element rule

before and after do not create content. Authored CSS supplies the pseudo-element content and geometry here; zudo-wind supplies its display and color utilities.

Style an authored pseudo-element
HTML
<p class="wind-demo-mark before:block before:bg-accent">The scaffold supplies pseudo-element content and size.</p>
CSS
@layer zw-reset, zw-tokens, zfb-hi, base, components;
@layer zw-tokens {
  :root {
    --zw-color-accent: #5b5bd6;
    --zw-spacing-unit: .25rem;
  }
}
.wind-demo-mark {
  display: flex;
  align-items: center;
  font: 14px / 1.5 system-ui, sans-serif;
}

.wind-demo-mark:before {
  content: "";
  inline-size: .75rem;
  block-size: .75rem;
  border-radius: 50%;
  margin-inline-end: .5rem;
}
.before\:block::before {
  display: block;
}
.before\:bg-accent::before {
  background-color: var(--zw-color-accent);
}
Configuration for this example
{
  "wind": {
    "spec": 1,
    "reset": "none",
    "strict": true,
    "tokens": {
      "spacingUnit": "0.25rem",
      "colors": {
        "accent": "#5b5bd6"
      }
    },
    "breakpoints": {},
    "defaultTransitionTimingFunction": "ease",
    "authoredClasses": {
      "wind-demo-mark": true
    },
    "safelist": {
      "docs-preview": [
        "before:block",
        "before:bg-accent"
      ]
    }
  }
}
Authored demonstration CSS
.wind-demo-mark { display: flex; align-items: center; font: 14px/1.5 system-ui, sans-serif; }
.wind-demo-mark::before { content: ""; inline-size: .75rem; block-size: .75rem; border-radius: 50%; margin-inline-end: .5rem; }
Inspect variant rejection examples

Variant groups have a canonical order. hover:sm:block is rejected; put the configured responsive variant first.

<div class="hover:sm:block">hover:sm:block</div>

Expected diagnostics: ZW003 (error)

Configuration for this example
{
  "wind": {
    "spec": 1,
    "reset": "none",
    "strict": true,
    "tokens": {
      "spacingUnit": "0.25rem"
    },
    "breakpoints": {
      "sm": {
        "minWidthPx": 640
      }
    },
    "defaultTransitionTimingFunction": "ease",
    "authoredClasses": {},
    "safelist": {
      "docs-preview": [
        "hover:sm:block"
      ]
    }
  }
}
Authored demonstration CSS

An unconfigured dark variant is unknown to this compiler invocation.

<div class="dark:block">dark:block needs configured dark mode.</div>

Expected diagnostics: ZW002 (error)

Configuration for this example
{
  "wind": {
    "spec": 1,
    "reset": "none",
    "strict": true,
    "tokens": {
      "spacingUnit": "0.25rem"
    },
    "breakpoints": {},
    "defaultTransitionTimingFunction": "ease",
    "authoredClasses": {},
    "safelist": {
      "docs-preview": [
        "dark:block"
      ]
    }
  }
}
Authored demonstration CSS

Named group and peer forms are outside v1.

<div class="group-hover/menu:block">group-hover/menu:block</div>

Expected diagnostics: ZW004 (error)

Configuration for this example
{
  "wind": {
    "spec": 1,
    "reset": "none",
    "strict": true,
    "tokens": {
      "spacingUnit": "0.25rem"
    },
    "breakpoints": {},
    "defaultTransitionTimingFunction": "ease",
    "authoredClasses": {},
    "safelist": {
      "docs-preview": [
        "group-hover/menu:block"
      ]
    }
  }
}
Authored demonstration CSS

Attribute variants stay in authored CSS.

<button class="aria-pressed:block">aria-pressed:block</button>

Expected diagnostics: ZW004 (error)

Configuration for this example
{
  "wind": {
    "spec": 1,
    "reset": "none",
    "strict": true,
    "tokens": {
      "spacingUnit": "0.25rem"
    },
    "breakpoints": {},
    "defaultTransitionTimingFunction": "ease",
    "authoredClasses": {},
    "safelist": {
      "docs-preview": [
        "aria-pressed:block"
      ]
    }
  }
}
Authored demonstration CSS

The same restriction applies to data-* attributes.

<div class="data-[state=open]:block">data-[state=open]:block</div>

Expected diagnostics: ZW004 (error)

Configuration for this example
{
  "wind": {
    "spec": 1,
    "reset": "none",
    "strict": true,
    "tokens": {
      "spacingUnit": "0.25rem"
    },
    "breakpoints": {},
    "defaultTransitionTimingFunction": "ease",
    "authoredClasses": {},
    "safelist": {
      "docs-preview": [
        "data-[state=open]:block"
      ]
    }
  }
}
Authored demonstration CSS

Arbitrary-selector variants are unsupported in v1. Write the selector in authored CSS instead.

<p class="[_a]:underline">[_a]:underline</p>

Expected diagnostics: ZW004 (error)

Configuration for this example
{
  "wind": {
    "spec": 1,
    "reset": "none",
    "strict": true,
    "tokens": {
      "spacingUnit": "0.25rem"
    },
    "breakpoints": {},
    "defaultTransitionTimingFunction": "ease",
    "authoredClasses": {},
    "safelist": {
      "docs-preview": [
        "[_a]:underline"
      ]
    }
  }
}
Authored demonstration CSS

A pseudo-element subject cannot be combined with child-space or divide utilities.

<div class="before:divide-y">before:divide-y</div>

Expected diagnostics: ZW005 (error)

Configuration for this example
{
  "wind": {
    "spec": 1,
    "reset": "none",
    "strict": true,
    "tokens": {
      "spacingUnit": "0.25rem"
    },
    "breakpoints": {},
    "defaultTransitionTimingFunction": "ease",
    "authoredClasses": {},
    "safelist": {
      "docs-preview": [
        "before:divide-y"
      ]
    }
  }
}
Authored demonstration CSS

For the full ordering, selector behavior, and variant limits, open technical details. Continue with the token guide, utility grammar, configuration, or complete utility index.

Variant ordering and selector details

Admitted variants

V1 admits configured responsive breakpoints, optional dark mode, unnamed group and peer relations, ten element states including checked, six pseudo-elements, and pointer conditions. At most one variant from each category may appear in a candidate. The canonical order is:

responsive → dark → pointer → relation → state → pseudo-element → utility

Use one responsive, one dark, and one pointer variant at most. A candidate may contain one relation and one self state; the relation variant itself names a state on its group or peer. No variant may repeat. There is no combined minimum and maximum breakpoint form.

Responsive and dark variants

A breakpoint is a configured name whose minWidthPx is a positive integer. sm at 640 means a minimum-width media condition of 640 CSS pixels. max-sm uses the exclusive condition width < 640px; it does not subtract an epsilon. Pixel widths stay fixed regardless of the root font size.

Dark mode is disabled by default. Configure its lowercase attribute name and nonempty value to enable dark. The condition accepts that attribute on the element or any ancestor. The generated selector uses :where(...), so dark mode adds zero specificity. The value is escaped as a CSS string.

Pointer conditions

pointer-coarse and pointer-fine need no configuration and add zero selector specificity. They emit (pointer: coarse) and (pointer: fine) media conditions, combined with responsive and hover conditions using and. Use at most one: repeats or a coarse/fine conflict report ZW003/R05, and an out-of-order chain reports ZW003/R06 with its canonical suggestion.

Relation and state variants

The supported self states, in their canonical rank order, are:

RankVariantSelector behavior
1first:first-child
2last:last-child
3open:is([open], :popover-open)
4focus-within:focus-within
5hover:hover
6focus:focus
7focus-visible:focus-visible
8active:active
9disabled:disabled
10checked:checked

State rank 0 is the absence of a state; the table lists ranks 1 through 10. The other state selectors use the state name as a pseudo-class. Every hover state, including group and peer hover, is guarded by @media (hover: hover).

A group marker is an ancestor; a peer marker must be a preceding sibling. A group state uses :where(.group:S) C; a peer state uses :where(.peer:S) ~ C, where C is the escaped candidate selector and S is the selected state. Relation selectors contribute zero specificity. Relation rank 0 is no relation; group states use ranks 1 through 10, followed by peer states at ranks 11 through 20, each in state order. Named markers and named relation forms such as group/menu and group-hover/menu:block are unsupported.

Pseudo-elements

The admitted pseudo-elements are before, after, marker, placeholder, backdrop, and selection. They are appended after state selectors with ranks 1 through 6 in that order; rank 0 means no pseudo-element. before and after do not insert a content declaration; add the needed declaration in authored CSS.

checked:, group-checked:, peer-checked:, and max-sm:peer-checked:visible are admitted; checked has no hover guard. selection: creates no content. In supporting browsers ::selection accepts color, background-color, text-decoration properties, text-shadow, and -webkit-text-stroke properties; other resolved utilities may have no visual effect. selection:space-* and selection:divide-* still report R23.

Order and rejection

A full chain uses responsive, dark, pointer, relation, state, pseudo-element, then utility order. For example, sm:dark:hover:bg-panel is canonical if sm, dark mode, and colors.panel are configured. hover:sm:block is out of order and returns ZW003 with the sorted suggestion sm:hover:block. A duplicate or an unknown variant has no automatic reorder suggestion.

The same selector is escaped from the original complete candidate. With dark configured as data-theme="dark", the selector for dark:hover:bg-panel is:

.dark:hover:bg-panel:where([data-theme="dark"], [data-theme="dark"] *):hover

Responsive and hover conditions are combined with and in one media wrapper; the dark condition stays in the selector. first and last map to child positions, and open also recognizes popover-open elements.

V1 rejects aria-* and data-* attribute variants with ZW004, and arbitrary-selector variants with ZW004. When dark mode is disabled, dark is an unknown or unconfigured variant and returns ZW002. Use authored CSS for unsupported attribute and arbitrary selectors, or configure dark mode before using dark. A candidate with a pseudo-element variant and child spacing or divide utilities is rejected as R23/ZW005 because the child selector would target children of the pseudo-element.

See also

Revision History

CreatedUpdated