Arbitrary values, brackets and the scanning model

Escape the scale when you must, understand how the scanner finds class names, and know why a class assembled from a variable never appears.

Arbitrary values and properties

<!-- arbitrary values, for anything the scale does not cover -->
<div class="w-[calc(100%-2rem)] max-h-[70vh] bg-[#bada55] text-[13px]">
  ...
</div>

<!-- arbitrary properties, when there is no utility at all -->
<div class="[mask-type:luminance] [scrollbar-gutter:stable] [text-wrap:balance]">...</div>

<!-- referencing theme values inside an arbitrary value -->
<div class="bg-[--brand] p-[--card-pad]">...</div>

<!-- a value that needs a space: use an underscore -->
<div class="grid-cols-[repeat(auto-fill,minmax(12rem,1fr))]">...</div>

<!-- important, as a suffix -->
<div class="bg-red-500!">...</div>
SyntaxMeans
w-[200px]An arbitrary value
[scrollbar-gutter:stable]An arbitrary property
_ inside bracketsA space in the generated value
bg-(--brand)A CSS variable shorthand
text-[color:var(--x)]A type hint, when the value is ambiguous
bg-red-500!Marks the declaration important
💡
Arbitrary values are an escape hatch, not a design system. If the same bracket value appears in four files, promote it to a theme token - otherwise every future change is a search and replace across markup, which is exactly the problem utility-first CSS was meant to remove.

How the scanner finds classes

Tailwind looks for class names as plain text in your source files. It does not execute your code and does not follow variables. A class name that is assembled at runtime therefore never appears in the output, because the scanner never saw that exact string.

// will NOT work: no complete class name exists in the source
const cls = "text-" + tone + "-600";

// works: the full class names are present as literals
const TONE = {
  danger: "text-red-600",
  warning: "text-amber-600",
  success: "text-emerald-600",
};
const cls = TONE[tone];

// also works, when you truly need a computed name
// @source inline("text-red-600 text-amber-600 text-emerald-600");
@import "tailwindcss";

/* scan an extra source that is outside the default detection */
@source "../node_modules/@acme/ui";

/* exclude something that produces false positives */
@source not "../fixtures";

/* declare a set of class names that only exist at runtime */
@source inline("grid-cols-{1,2,3,4}");
  • Automatic content detection walks the project and honours .gitignore, so build output and dependencies are skipped by default.
  • Files outside the project root - a monorepo sibling, a published component package - need an explicit @source.
  • Strings in tests and fixtures can produce false positives, which is harmless for size but can hide a real class name you thought you had removed.
  • A class name inside a comment is still a string the scanner sees, which is why a commented-out example can keep a utility alive.

Making dynamic classes safe

<!-- data attributes carry the variant, so the classes stay static -->
<div data-state="open" class="hidden data-[state=open]:block">...</div>
<div data-tone="danger" class="text-slate-600 data-[tone=danger]:text-red-600">...</div>

<!-- group and peer variants for parent-driven state -->
<div class="group" data-loading="true">
  <span class="opacity-100 group-data-[loading=true]:opacity-50">...</span>
</div>
// a lookup map keeps every class name in the source
const GRID = {
  1: "grid-cols-1",
  2: "grid-cols-2",
  3: "grid-cols-3",
  4: "grid-cols-4",
};

function gridCols(n) {
  return GRID[n] ?? GRID[1];
}
  1. Keep class names whole in the source. Compose state, not strings.
  2. Prefer a data attribute plus a variant over building a class name: the styling then lives in the markup where a reader can see it.
  3. Use a lookup map when the number of options is small and fixed.
  4. Use @source inline() when a third party generates class names you cannot enumerate in your own files.
  5. Verify a suspicious class in the built CSS rather than in the markup: if it is absent from the output, the scanner did not see it.
# does the utility actually exist in the built stylesheet?
npx @tailwindcss/cli -i src/app.css -o dist/app.css --minify
grep -c 'text-red-600' dist/app.css

# see what the scanner found
npx @tailwindcss/cli -i src/app.css -o dist/app.css --watch

FAQ

Why is my dynamic class name not working?
Because the scanner matches literal text, and "text-" + colour + "-600" never appears as a complete class name anywhere it can read. Either put the full names in a lookup object in your source, or declare them with @source inline().
Should I use arbitrary values or add a theme token?
Use an arbitrary value for a genuine one-off - a computed width, a vendor-specific property. Add a theme token as soon as the same value appears twice, or as soon as it is part of the design language. A bracket value repeated across the codebase is a token that has not been named yet.

Configuration and theming Production performance and the honest limits

Last refreshed 2026-09-18.