best practices and anti patterns of bracket notation
Arbitrary values open Tailwind up to any conceivable CSS value, without ever having to fall back to @apply or a separate stylesheet file. Used correctly, they elegantly solve genuine one off cases. Used incorrectly, they turn a clean utility system into unreadable class lists and undermine purge, consistency and team understanding.
Table of contents
- 1. What arbitrary values technically are
- 2. Reading bracket notation and data type hints correctly
- 3. Arbitrary properties: when no utility exists
- 4. Combining arbitrary values with CSS variables
- 5. Arbitrary values with variants and modifiers
- 6. The most common anti patterns in practice
- 7. Impact on the JIT compiler and bundle size
- 8. Governance: when an arbitrary value becomes a rule
- 9. Arbitrary values compared to alternatives
- 10. Summary
- 11. FAQ
1. What arbitrary values technically are
Arbitrary values in Tailwind allow specifying any CSS value directly in a utility class by wrapping it in square brackets, for example top-[117px] or bg-[#1da1f2]. The JIT compiler recognizes this pattern at build time, generates a matching CSS rule from it, and adds it to the final stylesheet, all without the class ever being defined in a configuration file beforehand. That fundamentally sets arbitrary values apart from the static value list of classic utility frameworks.
This flexibility is one of the reasons Tailwind has won out over plain CSS-in-JS or hand written CSS: you never fully leave the utility system, even when a design value does not appear in the default scale. At the same time, this very openness is exactly where arbitrary values get misused most often, because they are just as easy to type as any other utility class, but without the consistency guarantees of a real scale.
An important thing to understand: arbitrary values are not a feature flag you can turn on or off. They are always available because they emerge directly from the compiler scanning class names in the source code. The responsibility to use them sensibly rests entirely with the development team, not with the tooling configuration.
2. Reading bracket notation and data type hints correctly
Bracket notation follows the pattern utility-[value], where spaces in the value must be replaced with underscores, since class names in HTML cannot contain spaces. grid-template-columns: repeat(3, minmax(0, 1fr)) becomes grid-cols-[repeat(3,minmax(0,1fr))]. In ambiguous cases where Tailwind cannot automatically infer the value type, an explicit data type prefix like [length:...] or [color:...] helps avoid misinterpretation.
A typical stumbling block with arbitrary values: w-[50vh] is recognized as a length, but text-[length:1.5rem] is explicitly required whenever Tailwind needs to distinguish between a length and a color for text-*, because both utility families share the same prefix base. Anyone who ignores these data type hints risks a class that looks syntactically valid but is not recognized by the compiler and is therefore silently discarded.
<!-- Basic arbitrary value: exact pixel positioning for a one-off overlay -->
<div class="absolute top-[117px] left-[calc(50%-160px)]">Overlay</div>
<!-- Arbitrary color from a brand guideline, not part of the default palette -->
<button class="bg-[#1da1f2] hover:bg-[#1a91da] text-white px-4 py-2 rounded-lg">
Share
</button>
<!-- Explicit data type hint disambiguates text-* between length and color -->
<p class="text-[length:1.125rem] text-[color:var(--brand-ink)]">
Disambiguated arbitrary value
</p>
<!-- Complex grid template as a genuine one-off layout requirement -->
<div class="grid grid-cols-[repeat(3,minmax(0,1fr))] gap-4">
<div>1</div><div>2</div><div>3</div>
</div>
3. Arbitrary properties: when no utility exists
Beyond arbitrary values for existing utilities, Tailwind also supports arbitrary properties for CSS properties that have no dedicated utility at all. The syntax [mask-type:luminance] creates an entirely new CSS declaration, without Tailwind ever having to know about that property. This is especially useful for exotic or very new CSS features that are not yet part of the utility catalog.
The difference from classic arbitrary values is subtle but important: while bg-[#1da1f2] feeds an existing utility family with a new value, [mask-type:luminance] creates the entire declaration itself. Both mechanisms run through the same bracket syntax parser, but arbitrary properties should be used even more sparingly, since they are furthest removed from the actual utility idea and essentially amount to plain inline CSS wrapped in Tailwind's syntax.
4. Combining arbitrary values with CSS variables
One of the strongest combinations arises when arbitrary values are filled not with hardcoded numbers but with CSS variables, for example bg-[var(--brand-primary)]. This pattern combines the flexibility of bracket notation with the centralization of design tokens: the actual value continues to live in a single place in the stylesheet, while the utility class merely references it instead of duplicating it.
It becomes even more dynamic when the CSS variable is set at runtime through inline styles or Alpine.js, for example style="--progress: 72%" combined with w-[var(--progress)]. That allows data driven widths like progress bars to be implemented purely declaratively, without generating a dedicated utility class for every possible percentage value. This interplay between arbitrary values and CSS variables is one of the most underrated techniques in everyday Tailwind work.
<!-- Arbitrary value referencing a centralized CSS custom property, not a literal color -->
<div class="bg-[var(--brand-primary)] text-[var(--brand-on-primary)] rounded-lg p-4">
Token-driven card
</div>
<!-- Runtime-driven width via an inline custom property, e.g. from a progress score -->
<div class="w-full bg-slate-200 rounded-full h-2">
<div class="h-2 rounded-full bg-[var(--brand-primary)] w-[var(--progress)]"
style="--progress: 72%"></div>
</div>
5. Arbitrary values with variants and modifiers
Arbitrary values combine without restriction with responsive breakpoints, state variants and dark mode modifiers, exactly like any regular utility class. lg:top-[calc(100%-2rem)] or dark:bg-[#0b1220] work identically to their scale based counterparts, because the variant mechanism operates independently of whether the value comes from the scale or from bracket notation.
Arbitrary selectors push this flexibility even further: [&:nth-child(3)]:bg-slate-50 injects a complete CSS selector directly into the class and allows styling rules for which no named variant exists. This technique is powerful but should remain the last resort, after checking whether a more semantic solution like group, peer or a dedicated component class fits better, since arbitrary selectors are harder to read in markup than named modifiers.
6. The most common anti patterns in practice
The most widespread anti pattern with arbitrary values is repetition: the same value like text-[15px] shows up scattered across dozens of files instead of being defined once as a named design token. Every repetition is a potential inconsistency waiting to happen once the value needs to change, because a project wide search and replace becomes necessary instead of adjusting a single variable.
A second anti pattern is unreadable, highly complex arbitrary values with multiple nested functions like top-[calc(50%-theme(spacing.4)+2px)]. Expressions like that are barely comprehensible to other team members and belong in a named CSS variable with a descriptive name rather than directly in the markup. A third, more subtle anti pattern is using arbitrary values as a quick escape hatch when the real problem is actually a design flaw or a missing component, for example recurring odd numbers in icon sizes that point to a missing icon scale.
<!-- ANTI PATTERN: same arbitrary value repeated across the codebase, no single source of truth -->
<p class="text-[15px]">Caption A</p>
<p class="text-[15px]">Caption B</p>
<p class="text-[15px]">Caption C</p>
<!-- BETTER: one named theme value instead of a repeated arbitrary literal -->
<!-- @theme { --text-caption: 0.9375rem; } -->
<p class="text-caption">Caption A</p>
<p class="text-caption">Caption B</p>
<!-- ANTI PATTERN: unreadable nested arbitrary expression -->
<div class="top-[calc(50%-theme(spacing.4)+2px)]">Fragile offset</div>
<!-- BETTER: named CSS variable with a descriptive purpose -->
<div class="top-[var(--modal-offset)]" style="--modal-offset: calc(50% - 1rem + 2px)">
Readable offset
</div>
7. Impact on the JIT compiler and bundle size
The modern JIT compiler only generates CSS for class names it actually finds in the source code, instead of pre generating the entire utility palette. That means every arbitrary values class produces exactly one additional CSS rule, no matter how exotic the value is. Contrary to what some teams fear, this mechanism does not automatically bloat the bundle, as long as no class names are dynamically assembled at runtime.
It becomes critical when arbitrary values are assembled from template strings or variables at runtime, for example ` bg-[${color}]` in JavaScript. Tailwind's static scanner cannot find fragments like that, because it searches the source code as text and has no knowledge of runtime values. The result: the class ends up in the rendered HTML, but the matching CSS rule is completely missing from the build, and the element stays unstyled. This trap affects dynamic class names in general, not only arbitrary values, but it shows up there particularly often.
8. Governance: when an arbitrary value becomes a rule
A practical governance approach does not treat arbitrary values as a blanket ban, but as a deliberate exception with a clear escalation rule. If the same value shows up two or three times in the code, that is the trigger to migrate it into @theme as a named scale value or design token. A simple lint rule check or a regular grep pass in CI is often enough to enforce this rule automatically, without every pull request needing manual review.
For teams with a strict design system, a short comment requirement is worth adding on top: every arbitrary value in the code gets a short comment providing the justification, for example "exact match for a third party widget, fixed dimensions." That forces developers to pause briefly and check whether there really is no better solution before a new odd value migrates into the project.
9. Arbitrary values compared to alternatives
The following table compares arbitrary values against other common solutions and shows which approach is the better choice in which situation.
| Approach | Best for | Downside |
|---|---|---|
| Arbitrary values | Genuine one off cases, fast iteration | No reusability without discipline |
| @theme extension | Recurring, named design values | Requires an upfront team decision |
| @apply component | Recurring class combinations | Loses utility transparency in markup |
| CSS variable + arbitrary value | Runtime dynamic, centrally defined values | Additional indirection in the code |
| New component/utility | Recurring structural pattern | Higher upfront effort |
In practice, a clear order of preference works well: first check whether the default scale is enough, then check whether a named @theme value makes sense, and only once both are ruled out reach for arbitrary values. This order keeps the project consistent without losing the flexibility of bracket notation for genuine edge cases.
Mironsoft
Tailwind code reviews and utility architecture
Arbitrary values that do not slow your project down?
We review existing Tailwind code bases for wildly grown bracket notation, find repeated values, and set up clear governance rules so arbitrary values stay the exception instead of becoming a second scale.
Code audit
Analysis of all arbitrary values and how often they repeat
Refactoring
Migrating repeated values into named @theme tokens
Lint rules
CI checks against uncontrolled growth of bracket notation
10. Summary
Arbitrary values are one of Tailwind's most powerful features, because they make any CSS value directly available through bracket notation without ever leaving the utility workflow. Used correctly, they elegantly solve genuine one off cases like third party integrations or exact pixel requirements. Combined with CSS variables, they even become a bridge between utility classes and centrally maintained design tokens.
The downside appears as soon as arbitrary values are repeated without control, contain highly complex nested expressions, or paper over real design gaps. A clear escalation rule that moves repeated values into @theme, and a watchful eye on dynamically assembled class names in the JIT compiler, keep the feature exactly where it adds the most value: as a controlled exception for genuine edge cases.
Arbitrary Values in Tailwind — Key Takeaways
Syntax
utility-[value] with underscores instead of spaces. Data type prefix like [length:...] when ambiguous.
Arbitrary properties
[property:value] for CSS features with no dedicated utility. Use very sparingly.
Main anti pattern
Repeated values without a central source. Migrate into @theme after two or three repetitions.
JIT trap
Class names dynamically assembled at runtime are not recognized by the static scanner.