Tailwind Properties: What 58 @property Rules Reveal in a v4 Build
Tailwind v4 classes set CSS properties and register --tw-* variables with @property. Read from a real 84 KB build: 58 rules, how translate and shadow compose.
A Tailwind utility is a named bundle of one or more CSS properties, and since v4 it also writes custom properties of its own: --tw-* variables that are registered with @property so the browser knows their type and initial value. Compiled for this storefront, the stylesheet is 84,090 bytes (14,993 gzipped), declares 58 @property rules, and uses 61 distinct --tw- names.
Most "Tailwind properties" tutorials stop at a cheat sheet of classes. This one reads the compiled output instead, so you can see which CSS properties a class actually emits, why shadow-sm and translate-x-1/2 look different in DevTools than they did in v3, and where to look when a utility does not do what its name implies.
What "Tailwind properties" can mean
The phrase gets used for three different things, and mixing them up is the source of most confusion.
| You are asking about | What it is | Where you see it |
|---|---|---|
| Utility → CSS property mapping | Which CSS property a class sets (rounded-lg → border-radius) | The generated .rounded-lg{...} rule |
| Theme variables | Design tokens declared in @theme and emitted as --color-*, --spacing, --radius-* | src/app/globals.css |
Internal --tw-* properties | Plumbing variables that let several utilities compose into one CSS property | @property --tw-shadow{...} in the build output |
The first is what a cheat sheet covers. The second is covered in Tailwind design tokens. The third is the one that changed most in v4 and the one almost nobody documents, so it gets most of the space here.
Where the numbers come from
This repo pins "tailwindcss": "^4" and "@tailwindcss/postcss": "^4" in package.json; the installed version is 4.3.1. The whole pipeline is one PostCSS plugin:
// postcss.config.mjs
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
There is no tailwind.config.js. Tokens live in the @theme block at the top of src/app/globals.css (brand --color-primary: #465fff, the gray-25…950 scale, two custom breakpoints), and the compiler scans the source for class names. npm run build on Next.js 16.2.9 (Turbopack) produces one CSS chunk, and everything below was read from it:
F=.next/static/chunks/451v46_b247e_.css # the hashed name changes per build
wc -c $F # 84090
gzip -c $F | wc -c # 14993
grep -o '@property --[a-z0-9-]*' $F | sort -u | wc -l # 58
grep -o -- '--tw-[a-z0-9-]*' $F | sort -u | wc -l # 61
grep -o '@layer [a-z, ]*' $F | sort | uniq -c # 5 layers
The five layers are theme, base, components, utilities and properties. That last one is new in the sense that matters here: it is where the fallbacks for the --tw-* variables live.
A class is not one property
Take three utilities this site really uses and look at what they compile to.
| Class | Used in | Compiled output |
|---|---|---|
rounded-lg | many cards | border-radius:var(--radius-lg) |
border | cards, inputs | border-style:var(--tw-border-style);border-width:1px |
-translate-x-1/2 | HeroBackground.tsx, Testimonials.tsx | --tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y) |
rounded-lg is the simple case: one class, one property, one theme variable. border is already two properties, and one of them reads a --tw- variable so that border-dashed can change the style without redefining the width.
-translate-x-1/2 is the interesting one. It does not write transform: translateX(-50%). It writes a custom property and then sets the standalone translate property from two of them. That is why -translate-x-1/2 and -translate-y-1/2 can be stacked on one element: each class owns one variable, and both feed the same translate: declaration. The centred hero glow in src/components/molecules/HeroBackground.tsx relies on exactly that: left-1/2 … -translate-x-1/2 puts the element's left edge at the middle of its parent and then pulls it back by half of its own width.
Why @property is in the output at all
If translate: reads var(--tw-translate-y) and no class set --tw-translate-y, the declaration would be invalid at computed-value time. The browser would drop the whole translate, and the horizontal shift would silently disappear. Registering the variable gives it a guaranteed starting value:
@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}
@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}
Three things to read off these lines:
syntax:"*"means the value is untyped, so Tailwind is not asking the browser to validate it. It is using@propertyfor the other two descriptors.inherits:falsestops a--tw-shadowset on a card from leaking into every descendant. Without it, an inner element with no shadow class would inherit its ancestor's shadow value through thebox-shadowcomposition.initial-valueis the default every element starts from, which is what makes an unset--tw-translate-yresolve to0instead of being invalid.
Counting the 58 registrations by family is a quick way to see what Tailwind composes. The backdrop-* and filter families (blur, brightness, contrast, grayscale, hue-rotate, invert, saturate, sepia) account for a large share; then shadows and rings; then the individual transform axes (translate-x/y/z, scale-x/y/z, rotate-x/y/z, skew-x/y).
The composed property: shadow-sm
Shadows show the same pattern at larger scale:
.shadow-sm{
--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);
box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow),
var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)
}
One class writes --tw-shadow and then sets box-shadow from five variables. That is how shadow-sm ring-1 ring-gray-200 combine into a single box-shadow value rather than the second class overwriting the first. It is also why an unlayered hand-written box-shadow beats the utility completely: your declaration replaces the whole composition, not one layer of it.
transition lists properties too
transition is a property list, and the compiled list is long:
.transition{transition-property:color,background-color,border-color,outline-color,
text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,
opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,
backdrop-filter,display,content-visibility,overlay,pointer-events; ...}
Two details matter if you animate. translate, scale and rotate are listed separately from transform, which is what makes a transition on -translate-x-1/2 work given that the class sets translate. And --tw-gradient-from/via/to are listed because gradient colour stops are custom properties; whether they animate smoothly depends on the browser's @property support for those names.
The fallback layer
The first rule inside @layer properties in this build is:
@layer properties{
@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline)))
or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){
*,:before,:after,::backdrop{
--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;
--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1; ...
It is a feature query that only matches certain older Safari and Firefox engines. On those, the same defaults that @property would have supplied are written onto every element, ::before, ::after and ::backdrop instead. Modern browsers skip the block. The point for you: you do not write fallbacks for --tw-* yourself. The v4 migration guide covers the browser floor this implies.
Property-name lookup: when a class does nothing
| Symptom | Likely cause | Check |
|---|---|---|
translate-x-* has no effect | A parent or another rule sets transform or translate after it | Computed styles: look at translate, not transform |
| Ring appears but shadow vanished | Hand-written box-shadow replaced the composed value | Remove the inline or CSS-file box-shadow |
| Shadow colour class ignored | shadow-* colour needs a shadow size class too | Pair shadow-md shadow-primary/20 |
| Utility missing from the build | Class name built by string concatenation, so the scanner never sees it | Write complete class names in source |
New @theme colour does nothing in dev | Turbopack does not hot-reload @theme | Restart npm run dev |
| Custom variable not animating | Unregistered custom properties transition as discrete flips | Register it with your own @property and a real syntax |
The last row is the useful extension of everything above. Tailwind registers its own variables with syntax:"*", which opts out of interpolation. If you want a custom variable to animate, register it with a typed syntax such as <length> or <color> in globals.css, outside the Tailwind layers.
A faster way to find the property
Rather than a cheat sheet, build once and ask the compiled CSS:
npm run build
F=$(ls .next/static/chunks/*.css | head -1)
grep -o '\.rounded-lg{[^}]*}' $F
grep -o '\.border{[^}]*}' $F
grep -o '\.shadow-sm{[^}]*}' $F
This only finds classes that appear in your source, which is the point: it shows what you ship, not what exists. To rank which properties your pages lean on, count declarations. A rough pass over this stylesheet puts background-color at 122 declarations and color at 72, ahead of max-width (65), width (53) and height (51). That count is approximate (the regex also catches syntax and inherits descriptors from @property), but the order is a fair picture of a marketing site: colour first, sizing second.
Frequently asked questions
Does Tailwind v4 still generate transform: translateX()?
Not for the translate utilities. In this build, -translate-x-1/2 sets --tw-translate-x and the standalone translate: property. Code or DevTools filters that look for transform will not find it.
Do I need to write @property rules myself?
Not for Tailwind's own variables; the compiler emits them (58 here). Write your own only for custom variables you want the browser to type-check or animate.
Why is there a --tw- prefix on everything?
It namespaces the compiler's internal variables so they do not collide with yours. Your theme tokens use --color-*, --spacing and --radius-* instead.
Where do I add my own tokens?
In the @theme block of your global stylesheet, as in src/app/globals.css. There is no JavaScript config in this repo.
Templates in this post
Every template below ships on the same v4 pipeline described here, with tokens in @theme and no config file: ASoc Mind, ASoc Momentum and ASoc Neuron.
Browse the sets: Tailwind landing page templates and Next.js landing page templates. For the class-name side of the story, read Tailwind classes next.
