|
| 1 | +--- |
| 2 | +name: tailwind-syntax-functional-utilities |
| 3 | +description: > |
| 4 | + Use when authoring custom Tailwind utilities in v4 with the `@utility` |
| 5 | + directive that need to accept parameters: a theme-namespace key (like |
| 6 | + `tab-github`, `tab-2`, `tab-4`), a bare integer/number/ratio/percentage, |
| 7 | + a literal keyword, an arbitrary bracketed value, or a slash-modifier |
| 8 | + (like `text-base/relaxed`). |
| 9 | + Prevents the common v3-to-v4 plugin-authoring mistakes: writing a |
| 10 | + `matchUtilities()` JS plugin where a native `@utility name-* { ... }` |
| 11 | + block would do, attempting to use `--value()` / `--modifier()` / |
| 12 | + `--alpha()` / `--spacing()` inside v3 (silently ignored as |
| 13 | + unrecognised CSS functions), and forgetting that `--value()` accepts |
| 14 | + multiple type forms simultaneously so users can pick theme key |
| 15 | + OR bare integer OR arbitrary bracket on the same utility. |
| 16 | + Covers: the four v4-only CSS functions `--value()`, `--modifier()`, |
| 17 | + `--alpha()`, `--spacing()`; type-hint syntax (`integer`, `[length]`, |
| 18 | + `--namespace-*`, literal strings, `[*]`); default-value syntax via |
| 19 | + `--default(<value>)`; combining theme keys with bare and arbitrary |
| 20 | + values in a single utility definition; the migration path from |
| 21 | + v3 `matchUtilities()` plugin to v4 `@utility` directive. |
| 22 | + v4 ONLY: these functions do not exist in v3 and silently fail there. |
| 23 | + Keywords: functional utilities, custom utility, @utility directive, |
| 24 | + --value, --modifier, --alpha, --spacing, theme namespace, |
| 25 | + bare values, arbitrary values, type hints, integer, length, |
| 26 | + ratio, percentage, color, custom plugin, matchUtilities, |
| 27 | + how do I make a custom utility, v4 plugin authoring, |
| 28 | + tab-size utility, --tab-size-*, --default, default value, |
| 29 | + modifier slash, text-base/6, line-height pairing, |
| 30 | + color-mix opacity, alpha compose, spacing calc. |
| 31 | +license: MIT |
| 32 | +compatibility: "Designed for Claude Code. Requires Tailwind CSS v4.0+." |
| 33 | +metadata: |
| 34 | + author: OpenAEC-Foundation |
| 35 | + version: "1.0" |
| 36 | +--- |
| 37 | + |
| 38 | +# Tailwind CSS : Functional Utilities (v4 only) |
| 39 | + |
| 40 | +Native CSS functions that turn `@utility name-* { ... }` blocks into parameterised utilities. Replaces v3 `matchUtilities()` JS plugin authoring with CSS-first directives. NEVER use these in v3 : the CSS functions are unrecognised tokens and the build silently emits broken CSS. |
| 41 | + |
| 42 | +## Quick Reference |
| 43 | + |
| 44 | +### Four functions |
| 45 | + |
| 46 | +| Function | Purpose | Returns | |
| 47 | +|----------|---------|---------| |
| 48 | +| `--value(<type-or-namespace>)` | Resolve the suffix of a `name-*` utility against a type, literal list, theme namespace, or arbitrary bracket | The matched value | |
| 49 | +| `--modifier(<type-or-namespace>)` | Resolve the part after `/` in `name-X/Y` against the same type system | The matched value, or `--default(...)` if none | |
| 50 | +| `--alpha(var(--color-*) / <percentage>)` | Compose an alpha value into a colour via `color-mix()` | A colour with alpha applied | |
| 51 | +| `--spacing(<number>)` | Multiply the spacing scale base by a number | `calc(var(--spacing) * <number>)` | |
| 52 | + |
| 53 | +### Three invariants |
| 54 | + |
| 55 | +1. ALWAYS define functional utilities with `@utility <name>-* { ... }` in v4. NEVER reach for `matchUtilities()` in a v4 project unless backward-compatibility with a v3 JS plugin is required. |
| 56 | +2. ALWAYS list multiple `--value(...)` lines in the same `@utility` block to accept multiple value forms (theme key + bare + arbitrary). The CSS cascade picks the first match. |
| 57 | +3. ALWAYS use `--alpha()` over `color-mix()` directly for clarity, and `--spacing()` over hand-written `calc(var(--spacing) * n)` for the same reason. |
| 58 | + |
| 59 | +### Minimal canonical example |
| 60 | + |
| 61 | +```css |
| 62 | +@theme { |
| 63 | + --tab-size-github: 8; |
| 64 | +} |
| 65 | + |
| 66 | +@utility tab-* { |
| 67 | + tab-size: --value(--tab-size-*); /* theme key form : tab-github */ |
| 68 | + tab-size: --value(integer); /* bare form : tab-2, tab-4 */ |
| 69 | + tab-size: --value([integer]); /* arbitrary form : tab-[1], tab-[76] */ |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +All three usages compile : |
| 74 | +- `tab-github` resolves via the theme key `--tab-size-github = 8`. |
| 75 | +- `tab-2`, `tab-4` resolve via the bare integer type. |
| 76 | +- `tab-[1]`, `tab-[76]` resolve via the arbitrary integer bracket. |
| 77 | + |
| 78 | +## Decision Tree 1 : When to use `@utility` + `--value()` |
| 79 | + |
| 80 | +``` |
| 81 | +Q1. Does the project use Tailwind v4? |
| 82 | + no → STOP. Use v3 `matchUtilities()` JS plugin instead. See |
| 83 | + [tailwind-impl-plugins-custom]. |
| 84 | + yes → Q2 |
| 85 | + |
| 86 | +Q2. Does the utility need a parameter (suffix or modifier)? |
| 87 | + no → ALWAYS use a static `@utility name { ... }` block (no `-*` |
| 88 | + suffix, no `--value()` call). For example `@utility no-scrollbar { ... }`. |
| 89 | + yes → Q3 |
| 90 | + |
| 91 | +Q3. Should the parameter resolve against a theme namespace |
| 92 | + (so consumers can extend by adding `--namespace-foo: ...` in `@theme`)? |
| 93 | + yes → ALWAYS include `--value(--namespace-*)` as a line. |
| 94 | + no → Q4 |
| 95 | + |
| 96 | +Q4. Should the parameter accept a bare numeric or keyword value |
| 97 | + (without bracket syntax)? |
| 98 | + yes → ALWAYS include `--value(integer)` (or `number`, `ratio`, |
| 99 | + `percentage`, or `"literal1", "literal2"`). |
| 100 | + no → Q5 |
| 101 | + |
| 102 | +Q5. Should the parameter accept arbitrary bracketed values? |
| 103 | + yes → ALWAYS include `--value([type])` (or `[*]` for unrestricted). |
| 104 | + no → STOP. The utility has no value source ; consider whether it |
| 105 | + really needs `-*` suffix at all. |
| 106 | +``` |
| 107 | + |
| 108 | +## Decision Tree 2 : Choosing the right type hint |
| 109 | + |
| 110 | +``` |
| 111 | +Parameter is... |
| 112 | + |
| 113 | + → an integer (e.g., 2, 4, 76) ALWAYS `--value(integer)` + `[integer]` |
| 114 | + → a fractional number (e.g., 1.5, 2.25) ALWAYS `--value(number)` + `[number]` |
| 115 | + → a ratio (e.g., 16/9) ALWAYS `--value(ratio)` + `[ratio]` |
| 116 | + → a percentage (e.g., 50%) ALWAYS `--value(percentage)` + `[percentage]` |
| 117 | + → a length (e.g., 12px, 1.5rem) ALWAYS `--value([length])` (length is bracket-only) |
| 118 | + → a colour ALWAYS `--value([color])` (bracket) + `--value(--color-*)` (theme) |
| 119 | + → an angle (e.g., 45deg) ALWAYS `--value([angle])` |
| 120 | + → a URL (e.g., url('/x.png')) ALWAYS `--value([url])` |
| 121 | + → a literal keyword set ALWAYS `--value("a", "b", "c")` |
| 122 | + → anything (escape hatch) ALWAYS `--value([*])` (matches every arbitrary) |
| 123 | +``` |
| 124 | + |
| 125 | +## Decision Tree 3 : Should I use `--modifier()`? |
| 126 | + |
| 127 | +``` |
| 128 | +Q1. Does the utility need a slash-modifier (`name-X/Y`)? |
| 129 | + no → STOP. Do not call `--modifier()`. |
| 130 | + yes → Q2 |
| 131 | + |
| 132 | +Q2. Should the modifier default to a value when omitted? |
| 133 | + yes → ALWAYS use `--modifier(<type>, --default(<value>))`. |
| 134 | + no → ALWAYS use `--modifier(<type>)` and accept that the |
| 135 | + modifier-less form yields the empty value. |
| 136 | +``` |
| 137 | + |
| 138 | +## Patterns |
| 139 | + |
| 140 | +### Pattern 1 : Theme-namespace-driven utility |
| 141 | + |
| 142 | +```css |
| 143 | +@theme { |
| 144 | + --tab-size-github: 8; |
| 145 | + --tab-size-default: 4; |
| 146 | +} |
| 147 | + |
| 148 | +@utility tab-* { |
| 149 | + tab-size: --value(--tab-size-*); |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +Usage : `class="tab-github"`, `class="tab-default"`. NEW token values can be added to `@theme` without touching the `@utility` definition. ALWAYS pair theme namespace + matching utility namespace : `--tab-size-*` ↔ `tab-*`. |
| 154 | + |
| 155 | +### Pattern 2 : Bare-value utility |
| 156 | + |
| 157 | +```css |
| 158 | +@utility tab-* { |
| 159 | + tab-size: --value(integer); |
| 160 | +} |
| 161 | +``` |
| 162 | + |
| 163 | +Usage : `class="tab-2"`, `class="tab-4"`, `class="tab-8"`. v4 dynamically accepts any integer literal in markup; no need to enumerate. |
| 164 | + |
| 165 | +### Pattern 3 : Arbitrary-value utility |
| 166 | + |
| 167 | +```css |
| 168 | +@utility tab-* { |
| 169 | + tab-size: --value([integer]); |
| 170 | +} |
| 171 | +``` |
| 172 | + |
| 173 | +Usage : `class="tab-[1]"`, `class="tab-[76]"`. The bracket syntax in markup is mandatory; this form does NOT accept `tab-1` (use Pattern 2 for that). |
| 174 | + |
| 175 | +### Pattern 4 : Combined (theme + bare + arbitrary) |
| 176 | + |
| 177 | +ALWAYS combine all three forms in one block when the utility is exposed to users : |
| 178 | + |
| 179 | +```css |
| 180 | +@utility tab-* { |
| 181 | + tab-size: --value(--tab-size-*); /* tab-github */ |
| 182 | + tab-size: --value(integer); /* tab-2 */ |
| 183 | + tab-size: --value([integer]); /* tab-[76] */ |
| 184 | +} |
| 185 | +``` |
| 186 | + |
| 187 | +The CSS cascade resolves the first matching form. Users get full flexibility without separate utility names. |
| 188 | + |
| 189 | +### Pattern 5 : Literal-keyword utility |
| 190 | + |
| 191 | +```css |
| 192 | +@utility tab-* { |
| 193 | + tab-size: --value("inherit", "initial", "unset", "revert"); |
| 194 | +} |
| 195 | +``` |
| 196 | + |
| 197 | +Usage : `class="tab-inherit"`, `class="tab-initial"`. NEVER mix literal-keyword `--value` with bare-`integer` form on the same line; split into two lines : |
| 198 | + |
| 199 | +```css |
| 200 | +@utility tab-* { |
| 201 | + tab-size: --value("inherit", "initial", "unset"); |
| 202 | + tab-size: --value(integer); |
| 203 | +} |
| 204 | +``` |
| 205 | + |
| 206 | +### Pattern 6 : Functional utility with modifier |
| 207 | + |
| 208 | +```css |
| 209 | +@theme { |
| 210 | + --text-base: 1rem; |
| 211 | + --text-lg: 1.125rem; |
| 212 | + --leading-relaxed: 1.625; |
| 213 | + --leading-tight: 1.25; |
| 214 | +} |
| 215 | + |
| 216 | +@utility text-* { |
| 217 | + font-size: --value(--text-*, [length]); |
| 218 | + line-height: --modifier(--leading-*, [length], [*]); |
| 219 | +} |
| 220 | +``` |
| 221 | + |
| 222 | +Usage : |
| 223 | +- `text-base` : font-size 1rem, line-height unset (no modifier present). |
| 224 | +- `text-base/relaxed` : font-size 1rem, line-height 1.625 (modifier matches `--leading-relaxed`). |
| 225 | +- `text-base/6` : font-size 1rem, line-height matched against any-length bracket. |
| 226 | +- `text-[18px]/[1.5]` : both font-size and line-height arbitrary. |
| 227 | + |
| 228 | +The `--modifier(...)` reads everything after the `/` and resolves it against the listed type hints. |
| 229 | + |
| 230 | +### Pattern 7 : Modifier with default value |
| 231 | + |
| 232 | +```css |
| 233 | +@utility text-* { |
| 234 | + font-size: --value(--text-*, [length]); |
| 235 | + line-height: --modifier(--leading-*, [length], --default(1)); |
| 236 | +} |
| 237 | +``` |
| 238 | + |
| 239 | +When no modifier present (`text-base`), line-height resolves to `1`. The `--default(...)` value is emitted as a final fallback. |
| 240 | + |
| 241 | +### Pattern 8 : `--alpha()` for colour-with-opacity composition |
| 242 | + |
| 243 | +```css |
| 244 | +@theme { |
| 245 | + --color-lime-300: oklch(0.87 0.18 142); |
| 246 | +} |
| 247 | + |
| 248 | +@utility highlight { |
| 249 | + background-color: --alpha(var(--color-lime-300) / 50%); |
| 250 | +} |
| 251 | +``` |
| 252 | + |
| 253 | +Compiles to : |
| 254 | + |
| 255 | +```css |
| 256 | +.highlight { |
| 257 | + background-color: color-mix(in oklab, var(--color-lime-300) 50%, transparent); |
| 258 | +} |
| 259 | +``` |
| 260 | + |
| 261 | +ALWAYS use `--alpha()` over hand-writing `color-mix()` for readability and forward-compat. NEVER apply this to a colour that is not registered in `@theme` or accessed via `var(...)` ; the function does not accept raw hex. |
| 262 | + |
| 263 | +### Pattern 9 : `--spacing()` for spacing-scale calc |
| 264 | + |
| 265 | +```css |
| 266 | +.gutter { |
| 267 | + padding-block: --spacing(4); |
| 268 | +} |
| 269 | +``` |
| 270 | + |
| 271 | +Compiles to : |
| 272 | + |
| 273 | +```css |
| 274 | +.gutter { |
| 275 | + padding-block: calc(var(--spacing) * 4); |
| 276 | +} |
| 277 | +``` |
| 278 | + |
| 279 | +Useful in arbitrary-value brackets that mix spacing with other arithmetic : |
| 280 | + |
| 281 | +```html |
| 282 | +<div class="py-[calc(--spacing(4)-1px)]">...</div> |
| 283 | +``` |
| 284 | + |
| 285 | +This produces `padding-block: calc(var(--spacing) * 4 - 1px);` : a hair shorter than the canonical `py-4` for pixel-fine layout adjustments. |
| 286 | + |
| 287 | +NEVER use `--spacing()` outside `calc()` for plain values ; `py-4` is shorter and clearer. |
| 288 | + |
| 289 | +### Pattern 10 : Migration from v3 `matchUtilities()` |
| 290 | + |
| 291 | +v3 plugin authoring (JavaScript) : |
| 292 | + |
| 293 | +```js |
| 294 | +const plugin = require('tailwindcss/plugin') |
| 295 | +module.exports = plugin(function({ matchUtilities, theme }) { |
| 296 | + matchUtilities( |
| 297 | + { tab: (value) => ({ tabSize: value }) }, |
| 298 | + { values: theme('tabSize'), type: 'integer' } |
| 299 | + ) |
| 300 | +}) |
| 301 | +``` |
| 302 | + |
| 303 | +v4 equivalent (CSS, no JS plugin needed) : |
| 304 | + |
| 305 | +```css |
| 306 | +@theme { |
| 307 | + --tab-size-github: 8; |
| 308 | +} |
| 309 | + |
| 310 | +@utility tab-* { |
| 311 | + tab-size: --value(--tab-size-*); |
| 312 | + tab-size: --value(integer); |
| 313 | + tab-size: --value([integer]); |
| 314 | +} |
| 315 | +``` |
| 316 | + |
| 317 | +ALWAYS prefer the v4 CSS form. NEVER ship a JS plugin to v4 projects unless the plugin needs Node-only logic (e.g., reading external files at build time). |
| 318 | + |
| 319 | +## Anti-patterns (summary; full list in references/anti-patterns.md) |
| 320 | + |
| 321 | +NEVER do these : |
| 322 | + |
| 323 | +1. NEVER use `--value()`, `--modifier()`, `--alpha()`, `--spacing()` in v3. The CSS functions are unrecognised; the build emits the raw token, browsers ignore the declaration, the utility silently does nothing. |
| 324 | +2. NEVER write `@utility name-* { property: --value(--namespace-foo); }` with a specific suffix. ALWAYS use `--value(--namespace-*)` with the wildcard to match against the whole namespace. |
| 325 | +3. NEVER omit the type-hint when calling `--value()`. The function REQUIRES a type or namespace; an empty `--value()` is a parse error. |
| 326 | +4. NEVER combine `--value([length])` and bare numeric `--value(number)` expecting unitless `mt-1.5` to match `[length]`. Bracket-form ALWAYS requires the bracket; unitless numbers go to bare-numeric form. |
| 327 | +5. NEVER use `--alpha()` to apply opacity to a non-themed colour. The function expects `var(--color-*)`. For arbitrary colours, use `color-mix()` directly. |
| 328 | + |
| 329 | +## Reference Links |
| 330 | + |
| 331 | +- [references/methods.md](references/methods.md) : complete type-hint catalogue, function signatures, `--default()` semantics, compilation rules |
| 332 | +- [references/examples.md](references/examples.md) : multi-form utilities, slash-modifier patterns, real plugin migrations |
| 333 | +- [references/anti-patterns.md](references/anti-patterns.md) : v3 backport attempts, missing type hints, bracket-vs-bare confusion |
| 334 | + |
| 335 | +## Cross-references |
| 336 | + |
| 337 | +- [tailwind-impl-config-v4](../../tailwind-impl/tailwind-impl-config-v4/SKILL.md) : the `@theme`, `@utility`, `@variant`, `@custom-variant`, `@source` family of v4 CSS directives |
| 338 | +- [tailwind-impl-plugins-custom](../../tailwind-impl/tailwind-impl-plugins-custom/SKILL.md) : v3 `matchUtilities()` JS plugin authoring and the migration to v4 `@utility` |
| 339 | +- [tailwind-core-architecture](../../tailwind-core/tailwind-core-architecture/SKILL.md) : utility-first doctrine that motivates custom utilities |
| 340 | +- [tailwind-syntax-arbitrary-values](../tailwind-syntax-arbitrary-values/SKILL.md) : `[<value>]` bracket syntax in markup |
| 341 | +- [tailwind-core-v3-vs-v4](../../tailwind-core/tailwind-core-v3-vs-v4/SKILL.md) : the JIT to Oxide engine shift that enables CSS-first functional utilities |
| 342 | + |
| 343 | +## Sources |
| 344 | + |
| 345 | +- https://tailwindcss.com/docs/adding-custom-styles (`@utility` directive, `--value()`, `--modifier()`) |
| 346 | +- https://tailwindcss.com/docs/functions-and-directives (`--alpha()`, `--spacing()`, `theme()` deprecation) |
| 347 | +- https://tailwindcss.com/blog/tailwindcss-v4 (v4 dynamic utility values rationale) |
| 348 | +- https://tailwindcss.com/docs/theme (theme namespaces that `--value(--namespace-*)` reads from) |
| 349 | + |
| 350 | +Verified 2026-05-19. |
0 commit comments