zfb
GitHub repository

Type to search...

to open search from anywhere

Coming from Tailwind

Map Tailwind CSS constructs to zudo-wind tokens, variants, authored CSS, and explicit source inputs.

Search the class compatibility inventory for exact implemented and missing upstream registrations. It identifies configuration requirements, syntax limits, source evidence, and separate browser verification. A source-inspected mapping does not establish published-release support.

Read the compatibility result

  • Absent preset value: bg-gray-500 needs a project color named gray-500 if you choose to keep that spelling. Wind never supplies a palette implicitly.

  • Configuration required: sm:block needs a configured sm breakpoint; nonzero numeric spacing such as p-4 needs an explicit spacingUnit.

  • Missing native utility: a ring-2 or line-clamp-2 root has no Wind catalog implementation. Write the needed declaration in authored CSS and reserve the complete class where necessary.

  • Intentional syntax exclusion: arbitrary properties and !important class spellings receive diagnostics. Write those declarations in authored CSS.

Structural classes such as block, hidden, inline-flex, and mx-auto work without tokens. hidden sets display: none; bare none is not its utility class. Spacing, colors, and named typography use project values. The exact class table records source-inspected mappings and differences; read its verification column before treating a row as tested support.

Default theme variables

Tailwind's default theme namespace includes --color-*, --font-* (including --font-sans, --font-mono, and --font-serif), --font-weight-*, --text-*, --tracking-*, --leading-*, --breakpoint-*, --container-*, --spacing and --spacing-*, --radius-*, --shadow-*, --inset-shadow-*, --drop-shadow-*, --blur-*, --perspective-*, --aspect-*, --ease-*, and --animate-*. Those variables leave with Tailwind's import; zudo-wind does not provide them implicitly. A missing variable is not a zudo-wind build error.

To keep an authored var() reference, choose one of these remedies:

  • Declare the custom property yourself in authored CSS.

  • When its value is a configured token, use the emitted --zw-<category>-<key> variable. See the category-to-prefix table in Tokens for the exact variable name.

  • Add a fallback such as var(--x, <literal>).

Where authored CSS formerly referenced var(--color-neutral-900), update it to var(--zw-color-neutral-900), the emitted token variable, when neutral-900 is configured as a color token. zfb wind audit reports ZW016 as audit information for a fallback-less reference to a Tailwind default-theme variable that no scanned stylesheet declares; it is a warning under wind.strict: true. This is a heuristic: it scans the entry stylesheet and discovered imports only. CSS Modules, linked stylesheets, inline styles, and unresolved imports are outside that scan; with wind: false, ZW016 is not reported. Any declaration of the referenced variable in the scanned set suppresses the row.

Migrate in a short pass

The docs site hosting this guide still uses Tailwind for its own chrome. This checklist and its compiled samples describe zudo-wind v1 in your project.

  1. Remove Tailwind imports and engine directives from the entry stylesheet and its imports. Keep ordinary authored CSS and @layer. Search for affected default-theme references with:

    grep -rnoE 'var\(--(color|font|text|tracking|leading|breakpoint|container|spacing|radius|shadow|inset-shadow|drop-shadow|blur|perspective|aspect|ease|animate)[-)]' src pages
  2. Move engine values into the top-level wind key. Set spec, choose a reset, and configure the tokens and breakpoints your candidates use.

  3. Keep finite candidates in source, use safelist for app candidates absent from source, and publish package candidates through a versioned manifest.

  4. Audit the build plan. Fix supported candidates, reserve exact authored names, and replace or implement foreign utilities before enabling strict mode.

import { defineConfig } from "zfb/config";

export default defineConfig({
  wind: {
    spec: 1,
    reset: "owned-v1",
    strict: false,
    tokens: {
      spacingUnit: "0.25rem",
      colors: { panel: "var(--app-panel)" }
    },
    breakpoints: { sm: { minWidthPx: 640 } }
  }
});

Declare referenced custom properties in authored CSS. Then audit and explain the complete candidates:

zfb wind audit --plan build
zfb wind explain --config zfb.config.ts bg-panel
zfb wind audit --plan build --fail-on warning

The preview uses the token and numeric spacing in compiled zudo-wind rules:

Move a theme value into wind.tokens
HTML
<article class="wind-demo-card bg-panel p-4"><h2>Workspace panel</h2><p>Token-backed color and numeric spacing.</p></article>
CSS
@layer zw-reset, zw-tokens, zfb-hi, base, components;
@layer zw-reset {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
    border-width: 0;
    border-style: solid;
    border-color: currentColor;
  }
  html {
    line-height: 1.5;
    font-family: ui-sans-serif, system-ui, sans-serif;
    -webkit-text-size-adjust: 100%;
    tab-size: 4;
  }
  body {
    line-height: inherit;
  }
  h1,
  h2,
  h3,
  h4,
  h5,
  h6 {
    font-size: inherit;
    font-weight: inherit;
  }
  a {
    color: inherit;
    text-decoration: inherit;
  }
  b,
  strong {
    font-weight: bolder;
  }
  code,
  kbd,
  samp,
  pre {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 1em;
  }
  ol,
  ul,
  menu {
    list-style: none;
  }
  button,
  input,
  optgroup,
  select,
  textarea {
    font: inherit;
    letter-spacing: inherit;
    color: inherit;
  }
  button,
  input:where([type="button"], [type="reset"], [type="submit"]) {
    background-color: transparent;
    background-image: none;
  }
  textarea {
    resize: vertical;
  }
  img,
  svg,
  video,
  canvas,
  audio,
  iframe,
  embed,
  object {
    display: block;
    vertical-align: middle;
  }
  img,
  video {
    max-width: 100%;
    height: auto;
  }
  table {
    border-collapse: collapse;
    border-color: inherit;
    text-indent: 0;
  }
  summary {
    display: list-item;
  }
  hr {
    height: 0;
    border-top-width: 1px;
  }
}
@layer zw-tokens {
  :root {
    --zw-color-panel: #eaf2ff;
    --zw-spacing-unit: .25rem;
  }
}
.wind-demo-card {
  max-width: 24rem;
  border: 1px solid #b6c4d2;
  border-radius: .5rem;
  color: #172b4d;
}

.wind-demo-card h2 {
  margin: 0 0 .5rem;
  font-size: 1.1rem;
}

.wind-demo-card p {
  margin: 0;
}
.p-4 {
  padding-top: 1rem;
  padding-right: 1rem;
  padding-bottom: 1rem;
  padding-left: 1rem;
}
.bg-panel {
  background-color: var(--zw-color-panel);
}
Configuration for this example
{
  "wind": {
    "spec": 1,
    "reset": "owned-v1",
    "strict": true,
    "tokens": {
      "spacingUnit": "0.25rem",
      "colors": {
        "panel": "#eaf2ff"
      }
    },
    "breakpoints": {},
    "defaultTransitionTimingFunction": "ease",
    "authoredClasses": {
      "wind-demo-card": true
    },
    "safelist": {
      "docs-preview": [
        "bg-panel",
        "p-4"
      ]
    }
  }
}
Authored demonstration CSS
.wind-demo-card { max-width: 24rem; border: 1px solid #b6c4d2; border-radius: 0.5rem; color: #172b4d; }
.wind-demo-card h2 { margin: 0 0 0.5rem; font-size: 1.1rem; }
.wind-demo-card p { margin: 0; }

A remaining Tailwind directive is a hard ZW009 error, including when wind is false. This typed fixture shows the rejected entry stylesheet:

@tailwind utilities;
<p>Legacy entry check</p>

Expected diagnostics: ZW009 (error)

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

A Tailwind import is rejected by the same ZW009 stylesheet check:

@import "tailwindcss/utilities";
<p>Legacy entry check</p>

Expected diagnostics: ZW009 (error)

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

An old line-clamp-2 class is unsupported and produces ZW014. Keep it as a warning while inventorying lost styles, or make it an error at class positions with strict mode:

<p class="line-clamp-2">Two-line label</p>

Expected diagnostics: ZW014 (warning)

Configuration for this example
{
  "wind": {
    "spec": 1,
    "reset": "none",
    "strict": false,
    "tokens": {
      "spacingUnit": "0.25rem"
    },
    "breakpoints": {},
    "defaultTransitionTimingFunction": "ease",
    "authoredClasses": {},
    "safelist": {}
  }
}
Authored demonstration CSS

Write each unported selector or declaration in authored CSS or a CSS Module. The reference below retains exact reset differences, specificity, placement ties, and unsupported syntax alternatives.

Technical reference

What changes in one minute

  • zudo-wind is a versioned utility language. V1 is identified by spec: 1; familiar spellings do not promise compatibility with another engine.

  • There are no utility plugins. The accepted catalog is closed for each language version.

  • Configuration is data in the top-level wind key of zfb.config.ts or zfb.config.json. CSS directives do not configure the engine.

  • No palette, spacing unit, named scale, breakpoint, font, radius, shadow, or named easing token is supplied implicitly. Transition utilities use ease by default; set wind.defaultTransitionTimingFunction to choose another project default.

  • The source scanner needs complete class candidates. A prefix joined to a runtime value does not become a utility.

  • A project class with no matching utility root stays ordinary class text. A name that starts with a catalog root is resolved against the longest matching root and validated as a utility, so missing tokens receive diagnostics. Reserve a complete authored candidate with wind.authoredClasses when CSS owns a name that overlaps a utility root.

  • Keep declarations and selectors outside the catalog in authored CSS or CSS Modules.

Directives

Wind configuration and candidate discovery live outside CSS. Remove Tailwind-only directives when migrating a stylesheet. If one remains in the entry sheet or any imported sheet, zfb reports ZW009 with its spelling and source location and fails the pass, even when wind: false disables utility generation.

Tailwind constructzudo-wind disposition
@import "tailwindcss"Remove it. Wind supplies its own layer prelude and emits the selected reset, configured tokens, and resolved utilities. A remaining Tailwind import is a ZW009 error.
@import "tailwindcss/preflight"Remove it and choose an explicit wind.reset. The reset choices are listed below; an import left behind is a ZW009 error.
@import "tailwindcss/utilities"Remove it. Utilities are generated from complete candidates and the closed catalog; a remaining import is a ZW009 error.
@tailwind base, @tailwind components, or @tailwind utilitiesThere is no CSS expansion directive. Choose a reset and write component rules in authored CSS; use complete candidates for catalog utilities. A leftover directive is a ZW009 error.
@themePut validated values in wind.tokens. Keep CSS-only properties as ordinary custom properties in authored CSS, then point tokens at those properties when a utility needs them.
var(--<default theme variable>) in authored CSSTailwind's default theme variables are not provided by zudo-wind. The namespaces include --color-*, --font-* (including --font-sans, --font-mono, and --font-serif), --font-weight-*, --text-*, --tracking-*, --leading-*, --breakpoint-*, --container-*, --spacing and --spacing-*, --radius-*, --shadow-*, --inset-shadow-*, --drop-shadow-*, --blur-*, --perspective-*, --aspect-*, --ease-*, and --animate-*. Declare the property, use a configured token's emitted variable (see the category-to-prefix table), or provide a var() fallback. See Default theme variables.
@sourceCSS does not select scan roots. zfb's source plan owns scanned files and package roots; see Sources and candidates.
@source inline(...)Use an owner-keyed wind.safelist array for app-owned candidates. Put package-owned candidates in an explicit versioned wind.manifests file filtered to complete v1 utilities or markers; every manifest entry is strict, so an invalid or unknown entry fails. There is no pattern, brace expansion, or runtime interpolation syntax.
@source not(...)There is no CSS negation directive. Use the source plan's root and exclusion rules; see Sources and candidates.
@custom-variantOnly the built-in dark variant is configurable. Set its attribute and value under wind.dark; write other selector conditions in authored CSS.
@applyThere is no directive that expands utility names into declarations. Put declarations in the authored selector that needs them.
@utilityCustom utilities do not extend the language. Define a class in authored CSS and, if its exact complete candidate appears in markup, reserve that spelling with wind.authoredClasses.
@variantThere is no stylesheet variant-expansion directive. Use a supported complete candidate in markup or write the selector and condition in authored CSS.
@pluginPlugins are not loaded. Use the built-in catalog or authored CSS.
@configMove configuration data to the project's wind key; CSS does not load a utility config file.
@referenceThere is no utility reference import for stylesheet expansion. Keep CSS Modules and global CSS authored, and configure utility values under wind.tokens.

The scanner also rejects the legacy declaration-value functions theme(), --spacing(), --alpha(), and --value() with ZW009. It ignores comments and string contents. Ordinary @layer statements and CSS imports that explicitly name a layer remain valid. See Diagnostics and tools for the error contract.

Configuration

The old top-level tailwind key has no zudo-wind meaning. A leftover key is a hard configuration error with a named migration message. Replace it with wind. A missing wind key enables the empty v1 configuration after cutover; that means generation is enabled, reset is none, and the token maps start empty.

import { defineConfig } from "zfb/config";

export default defineConfig({
  wind: {
    spec: 1,
    reset: "owned-v1",
    defaultTransitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
    tokens: {
      spacingUnit: "0.25rem",
      colors: { panel: "var(--app-panel)" }
    },
    breakpoints: {
      sm: { minWidthPx: 640 }
    },
    dark: { attribute: "data-theme", value: "dark" }
  }
});

The old opt-out tailwind: { enabled: false } becomes wind: false. That disables wind generation, token validation, manifest loading, and candidate scanning. Authored global CSS, CSS Modules, and syntax highlighting still run, and the leftover-directive check still runs. For all fields and merge rules, see Configuration.

Theme and tokens

V1 ships no color palette or spacing scale. It also supplies no named sizes, font families or weights, font sizes, line heights, tracking, radii, shadows, z-index values, easing values, or breakpoints. The constants 0, px, and catalog keywords do not imply a theme.

Tailwind's --text-<n> and --text-<n>--line-height variables map to a fontSizes entry with the same key, size, and optional lineHeight:

Tailwind variable pairzudo-wind token
--text-<n> + --text-<n>--line-heightfontSizes: { <n>: { size, lineHeight } }

Move the values that utilities need into wind.tokens. Numeric spacing such as p-4 multiplies the configured spacingUnit; named values come from their family map. For example, this binds both color utilities to a project-owned property:

{
  "wind": {
    "tokens": {
      "spacingUnit": "0.25rem",
      "colors": {
        "panel": "var(--app-panel)"
      }
    }
  }
}
:root {
  --app-panel: #f4f1ec;
}

Then bg-panel and text-panel resolve from colors.panel. Token entries are validated data, not executable CSS or JavaScript. A referenced custom property remains authored CSS; the generated token variable points to it. Missing tokens and missing numeric spacing units are reported at strict candidate origins instead of being repaired with defaults. See Tokens for the supported maps and value rules.

For authored CSS that still references a Tailwind default-theme variable, apply one of the remedies in Default theme variables.

Uppercase DEFAULT is invalid. Use the lowercase key default only in maps that define that meaning: radii.default supplies bare rounded, and shadows.default supplies bare shadow. It is not a fallback for colors or spacing.

Reset

The default reset is none, which emits no reset declarations. Select a reset for the assumptions a page makes about browser defaults.

ResetRelevant behavior
noneNo reset bytes. Lists, heading sizes, border defaults, and controls keep the browser's normal styles unless authored CSS changes them.
minimal-v1Sets border-box sizing, removes the body margin, and makes form controls inherit the font. It does not remove list markers or normalize heading sizes.
owned-v1Sets border-box sizing and clears margins, padding, and default border width while making borders solid. It removes list markers, gives headings inherited size and weight, and normalizes control fonts and button backgrounds.

With owned-v1, add list markers back where the design calls for them; the scaffold's .prose rules explicitly restore disc and decimal lists. Headings do not retain browser heading sizes, so set them in authored CSS or with configured font-size utilities. Bare border utilities set their own solid border under every reset choice. Controls inherit font styling, and the reset leaves native focus outlines and appearance alone. Wind utilities are unlayered and, by default, follow authored global CSS: they beat normal declarations in named layers, while an unlayered authored rule can still win by specificity or later order. See Utility placement and ties for the opposite order. The reset is an explicit zudo-wind choice, not a promise to reproduce another preflight byte for byte. See Cascade and reset for the complete declarations and layer order.

Differences from Tailwind preflight

The baseline is Tailwind CSS 4.2.0 preflight vs owned-v1. The earlier recipe migrations were measured in Chromium 147 and 151 and WebKit 26.4. The supplemental #3451 fixture ran in Chromium 151 and WebKit 26.5 at 375px and 1280px; neither matrix is exhaustive. In the measured projects, Tailwind's theme resolves --default-font-family from --font-sans and --default-mono-font-family from --font-mono.

SelectorTailwind 4.2.0 preflightowned-v1 and effect
html font familyTheme --default-font-family / --font-sansFixed ui-sans-serif, system-ui, sans-serif; descendants can inherit another face.
html tap highlight-webkit-tap-highlight-color: transparentNo rule; the browser's tap color is inherited by everything.
code, kbd, samp, pre font familyTheme --default-mono-font-family / --font-monoFixed ui-monospace, SFMono-Regular, Menlo, monospace; macOS screenshots can hide a change when both stacks resolve to Menlo.
smallfont-size: 80%No rule; the UA size applies.
sub, supfont-size: 75%; line-height: 0; position: relative; vertical-align: baseline, with sub { bottom: -0.25em } and sup { top: -0.5em }No rules; the line box can grow and shift layout.
hrcolor: inheritNo color rule; the UA color can affect the border.
button, button/reset/submit inputsappearance: buttonNo appearance rule; native appearance can differ.
button, input, select, optgroup, textareaborder-radius: 0; background-color: transparent; opacity: 1owned-v1 already uses transparent backgrounds for buttons and button-type inputs; a text input keeps its UA white background in Chromium. Add the common-control rule below to match these declarations.
::placeholderopacity: 1 and, in supported browsers, color: color-mix(in oklab, currentcolor 50%, transparent)No rule; UA placeholder color and opacity apply (grey in measured Chromium).
::file-selector-buttonBorderless, transparent, inherited text styling, 4px end margin, appearance: buttonNo pseudo-element rule; the native bevelled file button remains.
[hidden]:where(:not([hidden="until-found"]))display: none !importantOnly the UA hidden rule applies; a display utility can make an element with hidden visible silently.

The original 0 differing elements result was limited to its fixture: it omitted a standalone text input, abbr[title], progress, and :-moz-focusring. The #3451 fixture found one difference before the common-control rule below: the text input had the UA white background under owned-v1 and a transparent background under Tailwind. After adding the rule, every standard computed-style property on the fixture elements and both pseudo-elements matched in Chromium 151 and WebKit 26.5 at both widths; all four screenshot pairs were pixel-identical. Chromium also retained a visible keyboard focus ring on the link after tabbing to it. This matrix still does not cover every control, font installation, or platform. A per-element getComputedStyle diff does not inspect ::placeholder or ::file-selector-button; query each explicitly and compare pixels where appearance matters.

For projects that want these measured preflight behaviors, add the following to authored CSS. Replace the sample font stacks with your project's stacks if they differ. The project must declare --font-sans and --font-mono in authored CSS, as this block does, or substitute literal stacks at both font-family declarations: v3 supplies no implicit theme.

@layer base {
  :root {
    --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
    --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  }
  html { font-family: var(--font-sans); -webkit-tap-highlight-color: transparent; }
  code, kbd, samp, pre { font-family: var(--font-mono); }
  small { font-size: 80%; }
  sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
  sub { bottom: -0.25em; }
  sup { top: -0.5em; }
  hr { color: inherit; }
  button, input:where([type="button"], [type="reset"], [type="submit"]) { appearance: button; }
  button, input, select, optgroup, textarea {
    border-radius: 0;
    background-color: transparent;
    opacity: 1;
  }
  ::placeholder { opacity: 1; color: color-mix(in oklab, currentcolor 50%, transparent); }
  ::file-selector-button {
    box-sizing: border-box;
    margin: 0;
    margin-inline-end: 4px;
    padding: 0;
    border: 0 solid;
    border-radius: 0;
    background-color: transparent;
    font: inherit;
    font-feature-settings: inherit;
    font-variation-settings: inherit;
    letter-spacing: inherit;
    color: inherit;
    opacity: 1;
    appearance: button;
  }
  /* Optional: keep hidden elements hidden even when a display utility is present. */
  [hidden]:where(:not([hidden="until-found"])) { display: none !important; }
}

The common-control rule restores Tailwind's border radius, background, and opacity declarations. The file-selector rule is separate because the button rule does not style that pseudo-element. In the Chromium 151 image-gallery recipe, adding the measured file-selector rule removed the pixel and pseudo-element differences on both tested pages in light and dark themes. The [hidden] rule is optional; its !important overrides display utilities, as Tailwind preflight does. The layer prelude orders zw-reset before authored base, so these declarations can override the reset. Keep keyboard focus indicators and review each rule you adopt rather than pasting a broader reset blindly. Restoring reset declarations does not restore utility placement or relation-selector specificity; that migration difference is tracked in #3386.

Variants and grammar

The table names the v1 disposition for each construct. Supported chains use the order responsive, dark, pointer, relation, state, pseudo-element, utility, with no repeated category.

ConstructSpec disposition and rule
Breakpoints, including max-*Adopt changed (W02). Configure each name with a positive integer minWidthPx. sm is a minimum-width query; max-sm is the exclusive range below that width. Widths are pixels and do not follow changes to the root font size.
darkAdopt changed (W09). Dark mode is off until configured with an attribute and value. The condition may match the element or an ancestor and adds zero specificity.
State variantsAdopt (W03). V1 admits hover, focus, focus-visible, active, disabled, checked, focus-within, open, first, and last. Every hover condition is guarded by a hover-capability media query.
first and lastAdopt (W03). They target :first-child and :last-child, respectively.
group-* and peer-*, including named formsAdopt changed (W04). Unnamed group and peer markers work with the admitted states; a group is an ancestor and a peer is a preceding sibling. Markers must stand alone and emit no CSS by themselves. Named markers and forms such as group/menu or group-hover/menu:block are rejected.
Pseudo-element variantsAdopt (W05). before, after, marker, placeholder, backdrop, and selection are admitted. before and after do not add a content declaration; author it when needed.
aria-* and data-[...] variantsReject with diagnostic (W06). Attribute variants are ZW004. Put the selector in authored CSS.
Arbitrary-selector variantsReject with diagnostic (W07). A selector such as [&_a]:underline is ZW004. Write the selector in authored CSS.
Stacked variantsAdopt changed (W08). Use at most one responsive, dark, pointer, relation, state, and pseudo-element variant, in that order. For example, sm:dark:hover:bg-panel is canonical when each feature and token is configured. Duplicate or out-of-order chains are diagnosed.
Color opacity modifierAdopt (W10). A color suffix is an integer from 0 to 100 and mixes the configured color in oklab against transparent; bg-panel/40 is valid. This modifier does not apply to spacing or sizing utilities.
FractionsAdopt changed (W11). Positive integer ratios are accepted by eligible sizing, translate, and seven inset roots, and as aspect ratios. w-1/2 means half the available sizing basis; aspect-4/3 emits aspect-ratio: 4 / 3. Aspect numerator and denominator must each be 1–1,000,000; use an arbitrary value such as aspect-[1.5/2.5] for decimal parts. left-1/2 emits left: calc(100% * 1 / 2); negative inset fractions also work. A sizing or inset slash never means color opacity.
TransitionsCurrent v1 behavior. transition-shadow sets box-shadow, and transition-colors includes outline-color. Transition utilities except transition-none set a 150ms duration and use wind.defaultTransitionTimingFunction, which defaults to ease. Set it to cubic-bezier(0.4, 0, 0.2, 1) to keep Tailwind 4's default timing. Explicit ease-* utilities take precedence in the same variant bucket.
Arbitrary valuesAdopt changed (W12). A supported utility root may carry a balanced bracket value that passes CSS property validation. Write spaces as underscores; an escaped underscore stays literal. Tailwind spaced calc() operators for you; zudo-wind does not, so write w-[calc(100vw_-_2rem)], and the error suggests that spelling for w-[calc(100vw-2rem)]. Data-type hints such as text-[color:var(--x)] are not supported; use a named token for an ambiguous variable. url() is rejected, as are values that inject declarations or other syntax.
Arbitrary propertiesReject with diagnostic (W13). A token such as [overflow-wrap:anywhere] is ZW004. Write the declaration in authored CSS or a CSS Module.
!important spellingsReject with diagnostic (W14). Leading and trailing forms are both rejected with ZW004. Put an intentional important declaration in authored CSS.
Negative utilitiesAdopt (W15). Only catalog families that permit negatives accept the sign, with the family-defined sign applied to its value. For example, -mt-2 is admitted while -p-2 is invalid.
space-* and divide-*Adopt changed (W20). The utilities target later visible direct-child siblings, filtered by the hidden attribute. Spacing sets the relevant margin; divide sets a solid left or top border. There are no reverse flags, and pseudo-element variants cannot combine with these child utilities.

pointer-coarse and pointer-fine need no config. any-pointer-*, motion-*, print, portrait, and arbitrary [@media(...)] remain unsupported (ZW002/R04; an arbitrary selector reports ZW004/R09). checked:, group-checked:, peer-checked:, and max-sm:peer-checked:visible are admitted; indeterminate, required, and invalid remain unsupported. selection: is admitted, without creating content. The standard focus-visible:outline focus-visible:outline-2 idiom resolves with a computed 2px outline width.

Decoration thickness/style use decoration-<integer>, nonnegative decoration-[<length>], and exact decoration-solid|double|dotted|dashed|wavy. Negative and slash thickness/style forms, decoration-auto, and decoration-from-font are unsupported. Configured full color tokens take precedence over style names: colors: { wavy: "#123456", "dotted-muted": "#123456" } makes decoration-wavy and decoration-dotted-muted/40 emit text-decoration-color. Without the token, decoration-wavy emits text-decoration-style. The names solid, double, dotted, and dashed were already reserved as color-token names; wavy was not. To get both declarations under a colliding name, author .decorated { text-decoration-style: wavy; text-decoration-color: var(--zw-color-wavy); } and reserve decorated in wind.authoredClasses if audit diagnoses it.

The candidate grammar balances brackets, parentheses, and quotes before splitting separators, so a colon or slash inside a bracket value is not treated as a variant or modifier separator. See Variants, Utility grammar, and the utility reference for the full catalog.

Utility placement and ties

A Tailwind v4 entry usually imports the utilities before its own CSS, so an authored rule with the same specificity as a utility won by coming later. zudo-wind places utilities after authored global CSS by default, which flips those ties:

/* authored */
.card {
  color: red;
}
<p class="card text-accent">Card</p>
SetupOrderColor
Tailwind v4, utilities imported firstutilities, .cardred
zudo-wind default (after-authored).card, utilitiesaccent
zudo-wind before-authoredutilities, .cardred

To keep Tailwind's order, set the placement:

wind: {
  utilities: { placement: "before-authored" }
}

The emitted stylesheet is then: the layer prelude, hoisted external imports, the highlight block, the reset and token layers, @property registrations, the unlayered utilities, the authored global CSS, and finally CSS Modules. Placement decides only ties of equal specificity between unlayered rules. Under either setting a more specific authored selector wins, an authored rule inside a layer loses to every utility, and an authored !important declaration wins. Breakpoint and dark variants add no specificity, so they follow the same tie rule. Emitting utilities into a named layer is not available.

Relation variant specificity

Tailwind emits group-hover:text-accent as .group-hover\:text-accent:is(:where(.group):hover *), with specificity (0,2,0). zudo-wind emits :where(.group:hover) .group-hover\:text-accent, with specificity (0,1,0), and peer-* variants likewise. Placement does not change any selector, so it does not restore that specificity. An authored (0,2,0) or (0,1,1) rule that used to lose to a group or peer state now wins under either placement:

/* (0,2,0): now beats group-hover:text-accent */
.panel .title {
  color: red;
}

Choose one of these per rule:

  • Lower the authored selector to the utility's specificity, for example :where(.panel) .title. The state then wins by order under the default placement.

  • Move the authored rule into a layer, such as @layer components { .panel .title { color: red; } }, so every utility beats it.

  • State the hover style in authored CSS too: .panel:hover .title { color: var(--zw-color-accent); }.

Classes that used to emit nothing

Not every class-shaped string is meant to create a utility rule. A project name such as site-header has no matching utility root, so it remains ordinary class text in markup and wind emits no rule for it. A name such as text-link does match the text root and is validated as a utility; if its required token is missing, it receives ZW006. This strict classification preserves typo detection instead of treating every unresolved name as ordinary. A standalone group or peer marker also emits no rule, while still supplying relation metadata to matching candidates.

A Tailwind utility zudo-wind does not implement, such as line-clamp-2, order-1, basis-1/2, or container, is not treated as ordinary text. It is reported as ZW014 with the declaration to author instead, as a warning by default and an error under wind.strict: true. Names with underscores, such as card__title, remain ordinary.

Candidate outcomeWhat happens
Resolved utilityWind emits its rule.
Ordinary class or markerA class-position name with no matching utility root remains in markup and produces no utility rule. The class can still be styled in authored CSS.
Recognized but invalid utilityA strict class, safelist, or manifest origin receives a diagnostic; lower-confidence source text is audit information. A missing token is not filled in automatically.
Malformed, unsupported, or unknown explicit utility inputThe pass fails with a diagnostic. Unknown explicit safelist or manifest entries fail; unrelated class-position names remain ordinary.

Run zfb wind audit to list ordinary class-position names, missing or dead utility candidates, dynamic fragments, and property conflicts within a class-list literal. Audit helps distinguish an intentionally authored class from a misspelled utility; it does not invent a replacement. To use an authored class whose name starts with a utility root, reserve the exact complete candidate spelling in wind.authoredClasses, for example { "text-link": true }. Include the variant chain when reserving a candidate such as hover:text-link. A match suppresses utility validation, diagnostics, and generated CSS for that candidate at every origin. Otherwise, correct the utility spelling or declare its missing token. See Diagnostics and tools.

ZW014 vocabulary v4 identifies logical ms-*/me-*, ps-*/pe-*, start-*/end-*, border-s/border-e for authored margin-inline-*, padding-inline-*, inset-inline-*, and border-inline-* declarations. It also identifies isolation, float, clear, word-break/break-*, transform origin, filter, backdrop-filter, text-shadow, and gradient families. Use authored CSS and reserve each exact retained class in wind.authoredClasses; no composed CSS is generated. Names such as origin-story, float-label, and clear-fix stay authored. An actual catalog/token utility resolves first. Gradient spellings include bg-gradient-to-*, bg-linear-to-*, integer-angle bg-linear-*, bg-radial and bracketed values, bg-conic and integer angles, and from-*/via-*/to-* with a declared color, current, transparent, or bracketed color/position. Author background-image. Bare stop percentages (from-10%, via-30%, to-90%) are parser-rejected; bracketed from-[60%] reaches ZW014.

A worked migration

The basic-blog scaffold's callout uses actual candidates such as my-6, rounded-md, border-l-4, px-4, py-3, dark:bg-sky-950/40, and group-hover:text-accent. The migration table keeps these spellings. Adoption means they resolve only after the scaffold explicitly configures their tokens and variants.

The old stylesheet begins with the Tailwind import and a custom dark variant, and declares three project colors through @theme:

@import "tailwindcss";
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));

@theme {
  --color-accent: oklch(0.53 0.21 258);
  --color-code-bg: #2b303b;
  --color-code-fg: #c0c5ce;
}

The callout source supplies these tone strings:

tone: "border-sky-500 bg-sky-50 dark:bg-sky-950/40",
accent: "text-sky-700 dark:text-sky-300",

Remove the two Tailwind directives. Move utility values to config and keep the CSS-only theme properties authored:

import { defineConfig } from "zfb/config";

export default defineConfig({
  wind: {
    spec: 1,
    reset: "owned-v1",
    tokens: {
      spacingUnit: "0.25rem",
      colors: {
        accent: "var(--color-accent)",
        "sky-50": "#f0f9ff",
        "sky-300": "#7dd3fc",
        "sky-500": "#0ea5e9",
        "sky-700": "#0369a1",
        "sky-950": "#082f49"
      },
      radii: { md: "0.375rem" }
    },
    breakpoints: { sm: { minWidthPx: 640 } },
    dark: { attribute: "data-theme", value: "dark" },
    authoredClasses: { prose: true }
  }
});

This config excerpt covers the shown callout tokens. A complete scaffold config also declares every color and scale used by the remaining candidates and authored CSS; nothing fills those maps implicitly.

Merge the theme properties into the existing authored base layer, retaining its color-scheme and selection rules:

@layer base {
  :root {
    --color-accent: oklch(0.53 0.21 258);
    --color-code-bg: #2b303b;
    --color-code-fg: #c0c5ce;
    color-scheme: light;
  }

  [data-theme="dark"] {
    --color-accent: oklch(0.79 0.13 254);
    color-scheme: dark;
  }
}

Keep the existing base and component layers; wind's prelude establishes their order. The authored .prose class remains in the stylesheet and is entered exactly in authoredClasses. As described in Default theme variables, update the old authored var(--color-neutral-900) reference to the emitted var(--zw-color-neutral-900) token variable when the color token is configured. The configured accent token points back to the authored --color-accent property, so the theme toggle can continue changing that value. The scaffold reset is owned-v1; other projects still default to none. These are language and stylesheet changes only; the full ordered project upgrade is in Migrating to v3.

What is not supported, and what to do instead

  • No implicit palette or spacing scale: declare project values under wind.tokens.

  • No shadow-color composition: shadow-<color> and valid shadow-<color>/<opacity> forms (/0–/100) do not apply colors tokens to a shadow. Fold the tint into a shadows token or author the complete box-shadow in CSS and reserve the class. A bare shadow-<name> with matching colors and shadows tokens resolves as the shadow token; slash modifiers remain unsupported. shadow-none and complete arbitrary values such as shadow-[0_1px_2px_#000] remain supported.

  • No ring, animation, scale, transform-string, or arbitrary-property utility: write the declarations in authored CSS or a CSS Module.

  • No attribute or arbitrary-selector variant and no named group or peer: use an authored selector.

  • No plugin-defined classes or @apply expansion: use catalog candidates for supported utilities and keep custom declarations in CSS.

  • No runtime-built candidate or regex safelist: write a finite literal map, an owner-keyed safelist, or a strict versioned package manifest.

Use the utility reference for the v1 catalog and Diagnostics and tools to inspect candidates and migration errors.

Revision History

CreatedUpdated