EmpireUI
Get Pro
← Blog8 min read#tailwind#arbitrary values#custom css

Tailwind Arbitrary Values: When [brackets] Save the Day

Tailwind's arbitrary value syntax lets you write any CSS value inline without leaving utility-class land. Here's when to use it, when to avoid it, and how.

Developer writing Tailwind CSS code with bracket syntax on dark monitor

What Arbitrary Values Actually Are

Tailwind's design constraint is the whole point — spacing at multiples of 4px, a curated color palette, a type scale that makes sense. But you're building for real projects. Real projects have a logo that needs to be exactly 42px tall, a sidebar that a designer locked at 280px, a gradient that stops at a very specific angle. That's where arbitrary values come in.

The syntax is a pair of square brackets dropped directly into a utility class: w-[280px], text-[#ff6b6b], top-[calc(100vh-3.5rem)]. Tailwind v3 shipped this feature in 2021 and it hasn't changed much since — it's still one of the cleanest escape hatches in any CSS framework.

In practice, you're telling Tailwind: "generate the CSS for this one-off value on the fly, inline, right here." No config change, no custom plugin, no @layer utilities block. The purge pass still picks it up because you wrote it in your template. It's the best of both worlds — you stay in utility-class land but you're not boxed in.

Worth noting: arbitrary values don't mean arbitrary CSS *properties*. If you want a property Tailwind doesn't expose at all, you'll want arbitrary properties (the square bracket prefix syntax) or a plugin. But for standard utility categories — spacing, sizing, color, border-radius, opacity — arbitrary values handle 95% of one-off needs.

The Syntax, Properly Explained

The mental model is: {utility-prefix}-[{value}]. The prefix tells Tailwind which CSS property to target, the bracket content is the raw value. So p-[18px] generates padding: 18px, bg-[#1a1a2e] generates background-color: #1a1a2e, rounded-[20px] generates border-radius: 20px. Simple.

<!-- Exact pixel spacing from a design file -->
<div class="mt-[22px] mb-[14px] px-[18px]">
  <img class="w-[42px] h-[42px]" src="/logo.svg" />
  <p class="text-[13px] leading-[1.6] text-[#6b7280]">Caption text</p>
</div>

Colors deserve their own mention. You can pass hex (bg-[#ff6b6b]), rgb()/rgba() (text-[rgba(0,0,0,0.6)]), hsl(), or even CSS custom properties (bg-[var(--brand-primary)]). That last one is particularly powerful — it means arbitrary values and your design token system can coexist cleanly.

Spaces inside bracket values aren't allowed — Tailwind uses spaces as class separators, so whitespace breaks parsing. Use underscores instead and Tailwind converts them: bg-[url('/some_image.png')], grid-cols-[200px_1fr_auto]. Honestly, this trips people up more than anything else in the arbitrary value system. Remember: underscore = space inside brackets.

<!-- Grid with specific column sizes -->
<div class="grid grid-cols-[240px_1fr_320px] gap-6">
  <aside>Sidebar</aside>
  <main>Content</main>
  <aside>Right panel</aside>
</div>

calc(), clamp(), and Friends

Where arbitrary values really earn their keep is with CSS functions. calc() in particular becomes a first-class citizen. Sticky headers, offset positioning, fluid sizing — all of it works inside the brackets without any ceremony.

<!-- Content area that accounts for a 64px fixed header -->
<main class="min-h-[calc(100vh-64px)] pt-[64px]">
  <!-- ... -->
</main>

<!-- Centered overlay with exact offset -->
<div class="absolute top-[calc(50%-150px)] left-[calc(50%-200px)] w-[400px] h-[300px]">
  <!-- modal or card -->
</div>

clamp() is even better. Instead of fighting with responsive breakpoint classes, one arbitrary value gives you a fluid type scale or fluid spacing: text-[clamp(1rem,2.5vw,1.5rem)]. A single class that handles the full range from mobile to wide desktop. When you're building with design systems that expect specific fluid values, this is a lifesaver.

One more thing — min() and max() work too. w-[min(90vw,680px)] is cleaner than stacking w-[90vw] max-w-[680px] and they behave identically. Use whichever reads better to your teammates.

That said, if you're reaching for calc() and clamp() everywhere, take a step back. Sometimes what you actually need is a spacing token in your tailwind.config.js. Arbitrary values are for genuinely one-off values, not for reimplementing your design system inline.

Arbitrary Properties: The Nuclear Option

Different from arbitrary values, but worth covering: arbitrary *properties* use a [property:value] syntax and let you set any CSS property that Tailwind doesn't expose as a utility at all.

<!-- CSS properties Tailwind doesn't have utilities for -->
<div class="[writing-mode:vertical-rl] [text-orientation:mixed]">
  Vertical text
</div>

<div class="[scrollbar-width:none] overflow-y-auto">
  Hidden scrollbar
</div>

<!-- backdrop-filter with a specific value -->
<div class="[backdrop-filter:blur(12px)_saturate(180%)] bg-white/10">
  Custom glass effect
</div>

Look, arbitrary properties are powerful but they're also where codebases get messy. When you're writing [grid-template-areas:'header_header'_'sidebar_main'_'footer_footer'] inline on a div, you've probably crossed the line where a custom utility class or a CSS module would be more readable. Use your judgment.

In practice, the legitimate use cases for arbitrary properties are: CSS properties that landed in browsers after Tailwind's last major release, highly vendor-specific behavior, or the occasional content-[''] for pseudo-element tricks. For effects like glassmorphism — where you need backdrop-filter with multiple functions — either use a component abstraction or check out the pre-built glassmorphism components that have this already solved.

<!-- content property for pseudo-elements (via @layer) is better,
     but this works in a pinch -->
<span class="before:content-['→'] before:mr-2">
  Next step
</span>

When NOT to Use Arbitrary Values

There's a version of this article that just says "arbitrary values = good, use everywhere." That version is wrong. Arbitrary values are an escape hatch. Escape hatches should close behind you.

If you find yourself writing mt-[16px] instead of mt-4, stop. Tailwind's default spacing scale uses 4px increments — mt-4 is already 16px. Check the docs before reaching for brackets. Most of the time, the value you need exists in the default scale or is one extend entry in your config away.

Repeated arbitrary values are the biggest smell. If text-[#1a1a2e] appears in 20 files, that's not a one-off — that's a design token. Add it to your config under colors and use text-brand-dark. You get autocomplete, you get a single source of truth, and future you will say thank you.

// tailwind.config.js — do this instead of repeating arbitrary values
module.exports = {
  theme: {
    extend: {
      colors: {
        brand: {
          dark: '#1a1a2e',
          accent: '#ff6b6b',
        },
      },
      spacing: {
        '18': '4.5rem', // if you keep needing gap-[72px]
      },
    },
  },
}

Quick aside: JIT mode (which has been the default since Tailwind v3.0 in 2021) makes arbitrary values zero-cost at build time — you're not shipping extra CSS unless you actually use the class. So there's no *performance* argument against them. The argument is purely about maintainability and readability. A class like bg-[#3d2b1f] communicates nothing about *why* that color. A token name does.

Responsive and State Variants with Arbitrary Values

Arbitrary values compose with every variant Tailwind has — responsive prefixes, pseudo-class modifiers, dark mode, group and peer targeting. The bracket syntax is just a value slot; the variant machinery wraps around it exactly the same way.

<!-- Responsive arbitrary sizing -->
<aside class="w-[260px] lg:w-[320px] xl:w-[380px]">
  <!-- sidebar -->
</aside>

<!-- Hover state with arbitrary color -->
<button class="bg-[#1a1a2e] hover:bg-[#2d2b55] transition-colors">
  Click me
</button>

<!-- Dark mode with arbitrary value -->
<div class="bg-[#f4f4f5] dark:bg-[#18181b]">
  Adaptive surface
</div>

One genuinely nice trick: combining arbitrary values with group-hover or peer lets you build interactions that would've needed JavaScript a few years ago. A peer-checked:translate-x-[22px] on a toggle thumb is a fully functional toggle with zero JS, for instance.

For animation keyframes and durations that fall outside Tailwind's built-in scale, arbitrary values handle that too — duration-[400ms], delay-[150ms]. If you're building anything more complex, though, you'll want to look at Tailwind CSS animations which covers the @keyframes side of things in much more depth.

Worth noting: the order of variants matters. sm:hover:bg-[#ff6b6b] means "on sm screens and above, apply the hover color." hover:sm:bg-[#ff6b6b] technically does the same thing in Tailwind v3, but the first form is conventional and what everyone expects to read.

Putting It Together: A Real-World Example

Let's say you're building a design system card component. The design file says: 320px wide, 24px corner radius, a specific gradient that doesn't exist in your token set, and a shadow the designer eyeballed. Here's how that looks with arbitrary values doing the heavy lifting, while still staying in Tailwind-land for everything else:

function DesignCard({ title, description, children }) {
  return (
    <div
      className={[
        // Exact dimensions from design
        'w-[320px] min-h-[200px]',
        // Corner radius outside default scale
        'rounded-[24px]',
        // One-off gradient
        'bg-[linear-gradient(135deg,#1a1a2e_0%,#16213e_50%,#0f3460_100%)]',
        // Custom shadow
        'shadow-[0_8px_32px_rgba(0,0,0,0.4)]',
        // Standard Tailwind for the rest
        'p-6 flex flex-col gap-4',
        'border border-white/10',
      ].join(' ')}
    >
      <h3 className="text-[18px] font-semibold text-white">{title}</h3>
      <p className="text-sm text-[rgba(255,255,255,0.6)] leading-relaxed">
        {description}
      </p>
      {children}
    </div>
  );
}

Honestly, that's clean. The one-off values are obvious inline, the structural layout uses standard utilities, and anyone reading this in six months will understand immediately what's a design constraint and what's a reusable scale value.

If you're building something visually richer — say, layered glass cards or a cyberpunk aesthetic with neon glows — you're going to be reaching for arbitrary box-shadow and backdrop-filter values constantly. At that point it might be worth browsing the box shadow generator to nail the values first, then drop them into your classes directly. Getting the value right before baking it in beats trial-and-error in DevTools.

FAQ

Do arbitrary values slow down my Tailwind build?

No. Since v3.0, Tailwind uses JIT — it only generates CSS for classes it finds in your templates, arbitrary or not. The build impact is the same as any other utility class.

Can I use CSS custom properties (variables) inside arbitrary values?

Yes — bg-[var(--brand-color)] works exactly as you'd expect. It's a great way to bridge Tailwind utilities and a CSS variable–based design token system.

Why is my arbitrary value class not working?

Nine times out of ten, it's an unescaped space. Replace spaces with underscores inside brackets, or use calc() syntax. Also check that the class isn't being purged by a dynamic string concatenation that JIT can't statically analyze.

Should I use arbitrary values or extend the Tailwind config?

If you're using the value once, brackets are fine. If it appears more than twice across your codebase, add it to tailwind.config.js under theme.extend — you'll get autocomplete and a maintainable single source of truth.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Tailwind Arbitrary Values: When to Use Them and When to StopGlassmorphism in Tailwind CSS: backdrop-blur Patterns and TipsWhat Is Glassmorphism? A Free React + Tailwind GuideFree Stacked Cards Component for React — Cards Stack Animation