What Is a Tailwind Class? 658 className Attributes, One Compiler Rule
A Tailwind class is a compiler-generated CSS rule that only exists because a literal string appeared in your source. Audited from 658 className attributes and 91 components.
A Tailwind class is a single-purpose utility that maps to one CSS declaration (or a small named group), generated by the compiler only when it appears in your source. p-6 becomes padding: 1.5rem, bg-primary becomes background-color: var(--color-primary). There is no runtime, no framework CSS baseline, and no cascade you have to out-specify — the whole vocabulary is decided by the @theme block plus whatever your components actually reference. This storefront's HTML carries 658 className="…" attributes across 91 components and 107 more inside src/app/, and the generated stylesheet is 13,675 bytes gzipped. Here is what a Tailwind class actually is, resolved from real files.
The one-sentence model
A Tailwind class is not a shortcut for a CSS rule. It is a CSS rule the compiler generates because the class appeared in your source at build time. Two consequences follow:
- Classes that never appear in source produce no CSS. Delete an unused utility from your markup and it disappears from the stylesheet on the next build.
- Classes are not composable at runtime —
p-and-6don't get joined intop-6by JavaScript. The whole stringp-6has to be literally present somewhere the compiler can see it.
That is the constraint that shapes every other rule below.
Utility, variant, arbitrary value — the three shapes you'll actually write
| Shape | Example | What it does | Where the value comes from |
|---|---|---|---|
| Bare utility | p-6 | One declaration | The --spacing scale in @theme |
| Variant + utility | md:p-6, hover:bg-primary-600, dark:bg-gray-900 | Wraps the declaration in a media query, pseudo-class or class selector | Named variants (built-in or @custom-variant) |
| Arbitrary value | p-[13px], bg-[#ff00aa] | Emits the literal value, no scale lookup | Whatever's inside the brackets |
Every class in this codebase is one of those three. Chained variants (md:hover:bg-primary-600) are the same thing — order matters and the last segment is the utility.
The @theme block in src/app/globals.css is what makes bg-primary a legal class:
/* src/app/globals.css */
@import "tailwindcss";
@theme {
--font-sans: var(--font-outfit), ui-sans-serif, system-ui, sans-serif;
--color-primary: #465fff;
--color-primary-25: #f2f7ff;
/* ...primary-50 through primary-950... */
--color-gray-25: #fcfcfd;
--color-gray-900: #101828;
/* ...gray-25 through gray-950... */
--color-text-color: #344054;
--color-title-color: #1d2939;
--color-stroke: #e4e7ec;
--breakpoint-2xsm: 375px;
--breakpoint-xsm: 425px;
}
Every declared token unlocks a family of utilities. --color-primary alone unlocks bg-primary, text-primary, border-primary, ring-primary, divide-primary, outline-primary, and their hover: / focus-visible: / dark: variants — but only the ones you actually write get emitted.
The Button atom, read one class at a time
The Button atom is a good specimen — every one of its classes is one of the three shapes above, starting with the inline-flex that 53 of this codebase's 257 display utilities also reach for:
// src/components/atoms/Button.tsx
const base =
"inline-flex items-center justify-center gap-2 rounded-lg px-6 py-3 text-base font-medium shadow-xs duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2";
const variants: Record<ButtonVariant, string> = {
primary: "bg-primary text-white hover:bg-primary-600",
outline:
"border border-stroke-tertiary bg-white text-text-color hover:bg-gray-50 hover:text-gray-800",
dark: "bg-gray-900 text-white hover:bg-gray-800",
};
Twelve utilities in base, three per variant. bg-primary and hover:bg-primary-600 both resolve through @theme. border-stroke-tertiary resolves through the semantic --color-stroke-tertiary token defined above. shadow-xs uses Tailwind's own default shadow scale — the theme extends the defaults, it doesn't replace them.
The composition happens in one line at the bottom of the same file:
className={`${base} ${variants[variant]} ${className}`.trim()}
That's the entire "component library." One Button is used at every CTA site in the marketplace (~30 in the marketing pages alone). The verbose class strings that make people wince about Tailwind exist in exactly one file.
What "spelled at build time" actually rules out
The rule "classes must be literal strings the compiler can see" has real consequences the docs mention once. The wrong-looking-but-legal patterns from this codebase:
// FINE — literal strings the compiler can grep for
const variants: Record<ButtonVariant, string> = {
primary: "bg-primary text-white",
outline: "border border-stroke-tertiary bg-white",
};
className={variants[variant]}
// BROKEN — nothing in the source spells `bg-red-500`
const color = "red";
className={`bg-${color}-500`}
The second form compiles, runs, and silently produces no styling. bg-red-500 is never anywhere in the source, so the compiler never generates the rule. The safelist workaround exists but is a last resort — restructure the code so the class is literal instead.
When arbitrary values are actually the right call
p-[13px] looks like a code smell. It usually is — most of the time the fix is "use the closest scale value" (p-3.5 for 14px). But the codebase does reach for arbitrary values in three specific cases, and they are all legitimate:
- A one-off measurement dictated by an image or an outside file. A hero image with a 47px reveal band gets
pt-[47px], notpt-12. Rounding to the scale would misalign. - A CSS variable already declared elsewhere.
bg-[var(--surface)]is common in the multi-tenant setup. - A dynamic value the compiler cannot see at build time.
[--brand:theme(colors.primary)]on a wrapper lets a descendant read the token at runtime.
Everything else should stay on the scale. w-[100px] for what could be w-24 (96px) is the class of usage that produced the finding in tailwind-max-width: 62 max-w-* uses in this codebase, 57 arbitrary, 22 of those already matched a declared token.
@apply and where this codebase does — and doesn't — reach for it
@apply compiles a set of utilities into a regular CSS class. It looks like the escape hatch you want when a component's class list gets long. In practice it undoes the property that made Tailwind worth using: locality. The class is redefined off-file, and the "what does this render" question stops being answerable from the JSX.
This codebase uses @apply twice, both in globals.css, both for cases where a class name is dictated by markup a component can't wrap:
.container {
@apply mx-auto w-full px-4 sm:px-6 lg:px-8;
max-width: 100%;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
Everywhere else — the atom, molecule and organism layers — extracts a component, not a class. See sass-vs-tailwind for the full accounting of when either escape hatch earns its keep.
Dark-mode variants: class-based, not media-based
The rule that decides the whole dark-mode story is at the top of globals.css:
@custom-variant dark (&:where(.dark, .dark *));
That single line makes dark:bg-gray-900 fire when a .dark class is on <html> (or any ancestor). The where() keeps its specificity at zero so it doesn't outrank an unstyled utility. The alternative (@media (prefers-color-scheme: dark)) would give the browser control instead of the app — which was the wrong call here because the user's saved preference is a page-level decision, not an OS-level one.
Every dark-mode utility in the codebase — dark:bg-gray-800, dark:text-white, dark:border-gray-700 — is a variant compiled through that one line.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| A new utility silently doesn't work in dev | Tailwind v4 + Turbopack doesn't hot-reload @theme changes | Restart npm run dev after editing tokens in globals.css |
bg-red-500 produces no CSS | The class is composed at runtime (`bg-${color}-500`), so the compiler never sees a literal bg-red-500 | Store the full class strings in a lookup object; safelist as a last resort |
| A component that worked in isolation loses its styling in production | Content sources not configured — the compiler scanned only some of your files | v4 auto-detects source globs; check the @source directive if you moved files outside the default globs |
dark: variants only fire when the OS is dark | You're using a media-query variant, not the class-based one | Add @custom-variant dark (&:where(.dark, .dark *)); and toggle .dark on <html> |
| A utility outranks another one you wrote later | Cascade order — Tailwind classes have equal specificity; whichever appears last in the compiled stylesheet wins | Rely on variant ordering (hover: after base) or use a more specific variant, not !important |
| The stylesheet keeps growing after you delete markup | Some templates aren't in the build's source graph (backup files, .mdx outside the include path) | Confirm every file that carries className is under the paths Tailwind scans |
FAQ
How many Tailwind classes are there? There is no fixed number — the compiler generates a class only when it appears in your source. The vocabulary is defined by the property × modifier × value grid, most of which you never touch. Here the compiled stylesheet holds 1,153 rules for the whole site; the theoretical vocabulary is far larger and unused entries produce nothing.
Is inline style={{}} equivalent to a Tailwind class?
No. Inline styles bypass the compiler, the token system, and every variant. They are the right answer for a value your JavaScript computes (a dynamically animated translateX(${x}px)); they are the wrong answer for anything the compiler could have generated. This codebase reserves inline styles for those computed cases — see styling-react-components for the audit.
Do class strings hurt HTML size? Marginally — long class strings compress well, and gzipped payload is what actually moves over the wire. What hurts more is a component library that ships CSS you never use. Tailwind's payload scales with the utilities you actually reference; ours is 13,675 bytes gzipped for 420 routes.
Can I use Tailwind classes in email templates?
Not directly — email clients strip <style> and don't run a compiler. Precompile the utilities you need with a tool like maizzle (Tailwind → inlined CSS for MJML), or hand-write the styles for the ~20 utilities most emails actually need.
Templates in this post
ASoc Byte is an IT-company landing page, ASoc Canvas a no-code page-builder template, and ASoc Catalyst an AI-automation-agency page — three of the ~66 landing templates whose whole visual system is composed from the utility grammar audited above.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates. For the design-token layer that decides which utilities exist, see Tailwind design tokens; for the max-width audit that showed how often arbitrary values sneak in, Tailwind max width.
