Skip to content

Commit b7a108c

Browse files
FreekHeijtingclaude
andcommitted
feat(skill): tailwind-syntax-functional-utilities
v4-only syntax skill covering the four CSS functions used inside @Utility directives: --value() for theme-namespace / bare-type / literal / arbitrary parameter resolution, --modifier() for the slash-suffix with optional --default(), --alpha() for color-mix- based opacity composition, --spacing() for the spacing-scale calc. Includes decision trees, ten patterns, full type-hint catalogue, v3-matchUtilities-to-v4-@Utility migration, and ten anti-patterns including the v3 backport silent-failure mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f71a3dc commit b7a108c

4 files changed

Lines changed: 1265 additions & 0 deletions

File tree

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
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

Comments
 (0)