functional @utility

[check: …]
📖 Pair this live Tailwind demo with the companion guide (.md) — this page is the rendered ground truth. ↗ Sibling to arbitrary values (one-off inline values) — functional @utility is the REUSABLE, variant-friendly counterpart for values you use as a family.

static @utility → functional @utility: the upgrade

A static @utility is one CSS rule for one fixed class. A functional @utility ends in -* and is a template: the * is a value slot, and the special --value() function resolves whatever you put there. This is exactly how every built-in utility (w-4, p-8, text-cyan-500) is authored internally — so your custom utilities inherit the full variant system (hover:, lg:, dark:) for free.

/* STATIC — one rule, one value */ @utility tab-4 { tab-size: 4; } /* ↑ only `.tab-4` exists. Want tab-8? Write another block. */ /* FUNCTIONAL — a template with a value slot */ @utility tab-* { tab-size: --value(integer, --default(4)); } /* ↑ `.tab-2`, `.tab-4`, `.tab-8`, `.tab-76` ALL compile on demand. Bare `.tab` resolves to 4 via --default(4). */
conceptstatic @utilityfunctional @utility
name shape @utility tab-4 (literal) @utility tab-* (ends in -*)
value source hard-coded in the block resolved by --value() from the class
how many classes exactly one (.tab-4) infinitely many — one rule per used value, JIT-generated
variant support yes — hover:tab-4 yes — hover:tab-8, lg:tab-4 (identical)
when to use a single one-off property (content-auto) a family of related values (sizes, steps, scales)

1 · tab-* — the canonical example (gold-check target)

This is the example from the official Tailwind docs. --value(integer) accepts any whole number; --default(4) makes the bare class tab resolve to tab-size: 4. The three <pre> blocks below have real tab characters — notice how the same source indent renders at different widths because the integer flows through --value().

@utility tab-* { tab-size: --value(integer, --default(4)); }
class="tab-2"
def greet(name):
	return f"hi {name}"
		if cond:
			nested()
class="tab-4" (default scale)
def greet(name):
	return f"hi {name}"
		if cond:
			nested()
class="tab-8"
def greet(name):
	return f"hi {name}"
		if cond:
			nested()
class="tab" (bare → default 4)
def greet(name):
	return f"hi {name}"
		if cond:
			nested()

tab-2: — · tab-4: — · tab-8: — · tab(bare): —

The gold-check asserts tab-4tab-size: 4 and tab-8tab-size: 8. The second assertion is the important one: it proves the integer is actually read from the class and plumbed through --value(integer) — not a hardcoded fallback.

2 · text-shadow-* — integer plumbed into calc()

Functional utilities aren't limited to pass-through. Here the integer drives a calc() expression, building a glow scale from a single number. Gotcha: Tailwind v4.3 does ship a built-in text-shadow-* (preset scale: text-shadow-sm, text-shadow-lg…). Your custom @utility text-shadow-* adds the numeric branch (text-shadow-6) — the resolver picks the right branch per value. If you only need the presets, don't reinvent.

@utility text-shadow-* { text-shadow: 0 0 calc(--value(integer) * 0.3rem) oklch(0.75 0.22 195 / 0.9); }
glow · 3
glow · 6
glow · 10

text-shadow-3: — · text-shadow-6: — · text-shadow-10: —

3 · grid-auto-fill-* — one utility, many declarations

A functional utility body can set multiple properties (just like the built-in line-clamp-*, which sets four). Here the integer becomes a minimum card width via minmax(), and the grid auto-fills as many cards as fit. Click a preset — the CDN's MutationObserver sees the class change and compiles the new rule on the fly.

@utility grid-auto-fill-* { display: grid; gap: 0.75rem; grid-template-columns: repeat(auto-fill, minmax(calc(--value(integer) * 4rem), 1fr)); }
card 1
card 2
card 3
card 4
card 5
card 6
card 7
card 8

class: — · grid-template-columns: —

the --value() resolver — every form

--value() is the bridge between the class suffix and your CSS. Stacking multiple --value() declarations (or comma-separated arguments) lets one utility accept theme keys, bare values, and arbitrary values — each branch that fails to resolve is silently dropped.

formsyntaxmatchesexample class
theme key --value(--tab-size-*) keys in @theme tab-github--tab-size-github: 8
bare value --value(integer) whole numbers (also number, ratio, percentage) tab-4
literal --value("inherit", "unset") exact keyword strings (quoted) tab-inherit
arbitrary value --value([integer]) bracketed value of a type (length, color, image, *…) tab-[76]
default value --value(integer, --default(4)) bare class with no suffix → default tabtab-size: 4
all-in-one --value(--tab-size-*, integer, [integer]) theme + bare + arbitrary, resolved left-to-right tab-github / tab-4 / tab-[76]
modifier --modifier(integer, --default(1)) the part after / (e.g. line-height) tab-2/3 → modifier 3

Heads-up: the older "--value(integer)? with a ? to make it optional" shorthand you may see in old blog posts does not exist in v4. The real mechanism is the --default() function.

when: functional @utility vs arbitrary value vs @theme

Three tools, three frequencies. Pick by how often the value recurs. See arbitrary values for the one-off escape hatch and arbitrary properties for one-off CSS properties Tailwind doesn't cover.

you need…useexample
a value once, never again arbitrary value top-[117px]
a CSS property Tailwind lacks, once arbitrary property [mask-type:luminance]
one design token reused everywhere @theme + generated utility --color-brandbg-brand
a family of utilities from a formula/scale functional @utility tab-*, grid-auto-fill-*, text-shadow-*
variants on a one-off value (hover:[mask-type:alpha]) arbitrary property (variants work)
variants on a reusable scale functional @utility (variants work identically) hover:tab-8, lg:grid-auto-fill-6

intent → pattern

intentpatternwhy
register a parameterized family @utility tab-* { tab-size: --value(integer); } the -* marks a value slot; JIT emits a rule per used value
make the bare class work too --value(integer, --default(4)) tab (no number) resolves to the default
accept theme keys + numbers + brackets --value(--tab-size-*, integer, [integer]) comma list = left-to-right resolution; misses drop silently
set several properties from one value multiple declarations in the block mimics built-in line-clamp-* (4 declarations)
negative values separate @utility -inset-* with * -1 v4 has no sign flag — register the negative form by hand
read the /modifier part --modifier(integer, --default(1)) like built-in text-* reads leading-* after /
use it with variants hover:tab-8, dark:text-shadow-6 automatic — functional utilities live in the utilities layer