Colour and Borders

Tailwind CSS Fundamentals

Chapter 3 ยท Colour, Backgrounds and Borders

Colour in Tailwind works like spacing: you pick from a ready-made palette with names and numbers, and the same names work on text, backgrounds, borders, rings and shadows. The edges of things (borders, rounded corners, rings, shadows) are a similar small system. This chapter covers both, including a measured look at colour contrast, which is the part people most often get wrong.

Every example here was run
CSS is copied from Tailwind CSS 4.3.3's output. Colour values, contrast ratios, computed styles and the two pixel readings were measured in headless Chrome. The contrast ratios use the WCAG 2 formula on the colours Chrome actually painted.

The Palette

The default theme has 26 colour families, each with 11 shades numbered 50, 100, 200, 300, 400, 500, 600, 700, 800, 900 and 950, plus black and white. I counted them in Tailwind's own theme.css: red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose; the greys slate, gray, zinc, neutral and stone; and mauve, olive, mist and taupe (those last four were added in v4.2). A low number is light and a high number is dark: blue-50 is nearly white, blue-950 nearly black, and blue-500 is the middle.

Every colour is a theme variable, written in oklch():

--color-blue-500: oklch(62.3% 0.214 259.815);

The three numbers are lightness, chroma (how vivid) and hue (the angle on the colour wheel). It is a good notation for building a palette because shades of one hue look evenly spaced. You never need to type it: you use the name, and Tailwind writes the variable only if you use the colour.

Putting Colour on Things

A colour class is a prefix, which says what to colour, followed by the family and shade. Each prefix maps to one CSS property:

PrefixColoursCSS property
text-blue-600textcolor
bg-blue-500backgroundbackground-color
border-gray-200borderborder-color
divide-gray-200the lines between childrenborder-color on every child but the last
ring-blue-500, outline-blue-500rings and outlines--tw-ring-color, outline-color
decoration-blue-500underlinestext-decoration-color
accent-blue-500, caret-blue-500checkboxes and sliders; the text cursoraccent-color, caret-color
fill-blue-500, stroke-blue-500SVG shapesfill, stroke
shadow-blue-500/50the colour of a shadow--tw-shadow-color

There are also a few special values that are not in the palette: bg-transparent (transparent), bg-current and border-current (currentcolor, meaning "the text colour"), and text-inherit (inherit). For a colour that is not in the palette use brackets, bg-[#bada55], or a variable, bg-(--brand), which compile to background-color: #bada55 and background-color: var(--brand). The colour of an input's placeholder text has two spellings that give the same result: placeholder-gray-400 and the variant form placeholder:text-gray-400 both compiled to a ::placeholder rule setting color. The variant form is the one that generalises (chapter 6).

Opacity: the Slash

To make a colour partly transparent, add a slash and a percentage to any colour class: bg-blue-500/50 is the blue at 50%. It works with every prefix in the table above, and with arbitrary values (bg-blue-500/[0.37] gave 37%). Here is what Tailwind wrote for bg-blue-500/50:

bg-blue-500/50
.bg-blue-500\/50 { background-color: color-mix(in srgb, oklch(62.3% 0.214 259.815) 50%, transparent); @supports (color: color-mix(in lab, red, red)) { background-color: color-mix(in oklab, var(--color-blue-500) 50%, transparent); } }

It uses color-mix(): a plain version first for older browsers, then, where the browser supports it, a version that mixes in the oklab colour space using the theme variable. Chrome painted it as oklab(0.623 -0.0378 -0.2106 / 0.5), which on a white page is a lighter blue (pixel value 44, 128, 255 at half alpha).

bg-black/40 is not the same as opacity-40
bg-black/40 makes only the background colour 40% black. opacity-40 (opacity: 40%) fades the whole element and everything inside it, including text. I measured both on a white page, each with the same amber heading inside a 100%-wide box: with bg-black/40 the box was grey (153, 153, 153) and the text was the pure amber (255, 210, 48). With bg-black opacity-40 the box was the same grey, but the text had faded to (255, 236, 171), a pale cream. For an overlay behind text, use the slash on the overlay's background and keep the text outside the faded element.

Readable Colour: Contrast

A colour scheme is only useful if people can read it. The WCAG accessibility guidelines measure the contrast between text and background as a ratio from 1:1 to 21:1. Level AA asks for at least 4.5:1 for normal text and 3:1 for large text (24px, or about 19px bold); AAA asks for 7:1. I painted common pairs in Chrome, read the real colours back, and computed the ratio:

PairRatioNormal text (4.5:1)Large text (3:1)
text-gray-400 on white2.60failsfails
text-gray-500 on white4.84passespasses
text-gray-600 on white7.56passespasses
text-gray-700 on white10.30passespasses
text-gray-900 on white17.75passespasses
text-white on bg-blue-5003.76failspasses
text-white on bg-blue-6005.25passespasses
text-white on bg-blue-7006.83passespasses
text-blue-500 on white3.76failspasses
text-blue-600 on white5.25passespasses

Two habits follow. The light-grey captions that look elegant (text-gray-400 on white) are hard to read and fail the guideline; gray-500 is the lightest shade of this grey that passes on white. And the popular white-on-blue-500 button fails for ordinary text size; moving one step darker, to bg-blue-600, fixes it. In general, for light backgrounds use shades 600 or darker for text, and for a coloured button with white text use shade 600 or darker. (Always check your own pairs: a different family has different numbers.) Exercise 1 gives you a snippet to do the measurement.

Backgrounds and Gradients

Colour is one kind of background; images and gradients are others. The small utilities map directly to properties:

ClassCSS
bg-cover, bg-containbackground-size: cover | contain
bg-center, bg-no-repeat, bg-fixedbackground-position: center, background-repeat: no-repeat, background-attachment: fixed
bg-[url('/img/hero.jpg')]background-image: url('/img/hero.jpg')
bg-clip-text text-transparentbackground-clip: text and color: transparent: lets a gradient show through the letters

A gradient is made from a direction class plus colour stops:

<div class="h-10 bg-linear-to-r from-cyan-500 via-blue-500 to-indigo-600"></div>
computed background-image: linear-gradient(to right, oklch(0.715 0.143 215.221) 0%, oklch(0.623 0.214 259.815) 50%, oklch(0.511 0.262 276.966) 100%)

bg-linear-to-r sets the direction (to right; there are eight, such as -to-br for bottom-right), from-*, via-* and to-* set the colours, and the three stops sit at 0%, 50% and 100%. Move a stop with a percentage: from-10% and to-90% are --tw-gradient-from-position: 10% and --tw-gradient-to-position: 90%. The same family gives you bg-linear-45 (an angle), bg-radial and bg-conic. In the generated CSS the colours are mixed in the oklab colour space (to right in oklab), which keeps the middle of a gradient from going grey; add /oklch (as in bg-linear-to-r/oklch) to ask for a different space. The older class name bg-gradient-to-r still compiles in v4, but bg-linear-to-r is the current name.

Borders

A border has a width, a style and a colour, and Tailwind sets each by class. The width classes:

ClassCSS
borderborder-style: var(--tw-border-style); border-width: 1px
border-2, border-4, border-0the same with 2px, 4px, 0px
border-t, border-x-4border-top-width: 1px; border-inline-width: 4px (sides: t r b l x y s e, as in the padding classes)
border-solid, border-dashed, border-dottedborder-style: solid | dashed | dotted
A border with no colour class takes the text colour
In Tailwind v4 the default border colour is currentcolor, the colour of the element's text. I put border on an element with text-red-600 and Chrome reported a border colour of oklch(0.577 0.245 27.325), which is the red of the text. In Tailwind v3 the default was a light grey, so tutorials and code you copy from older projects often write border and expect grey. In v4 always add a colour: border border-gray-200 gave oklch(0.928 0.006 264.531), a light grey. (Tailwind CSS Intermediate/Advanced 8 covers moving v3 code to v4.)

Rounded corners come from the radius scale:

ClassRadiusClassRadius
rounded-xs0.125rem (2px)rounded-xl0.75rem (12px)
rounded-sm, rounded0.25rem (4px)rounded-2xl1rem (16px)
rounded-md0.375rem (6px)rounded-3xl, rounded-4xl1.5rem, 2rem
rounded-lg0.5rem (8px)rounded-fullcalc(infinity * 1px) (a circle on a square, a pill on a rectangle)

A bare rounded is 0.25rem, the same as rounded-sm in v4. Round one side or corner with a suffix: rounded-t-lg (both top corners), rounded-s-md (both start-side corners, which flip in right-to-left text), or an exact value with rounded-[10px]. rounded-none removes rounding.

Rings and Outlines

A ring is a solid line around an element that takes up no space, which makes it ideal for focus and selected states, because the layout does not move when it appears. It is drawn with box-shadow, so ring-2 ring-blue-500 is a 2px blue ring:

ring-2 ring-blue-500
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); --tw-ring-color: var(--color-blue-500);

Two things are different from older Tailwind, according to the official v4 upgrade guide, and I checked the generated CSS. A bare ring is now 1px (v3's was 3px), and its default colour is currentcolor (v3's was blue-500), so a ring with no colour class matches the text. For the old width write ring-3, which compiled to a 3px ring. ring-offset-2 leaves a 2px gap between the element and the ring. inset-ring draws the ring inside the element.

An outline is the browser's own outline: outline (1px), outline-2, outline-blue-500, outline-offset-2. Browsers show an outline on keyboard focus by default, and the usual Tailwind pattern for custom focus styles is to replace it with your own, and v4 has two classes for "no outline". outline-hidden compiled to outline-style: none plus a rule that restores a transparent 2px outline in forced-colours mode (Windows High Contrast, @media (forced-colors: active)), so the focus indicator is not lost for people who use that mode. outline-none compiled to just outline-style: none. The upgrade guide says v3's outline-none behaved like today's outline-hidden, so older code that uses outline-none for focus styles should be outline-hidden now. Never remove the focus indicator without giving a visible replacement.

Shadows and Filters

Shadows come in a size scale. Each is made of one or two box-shadow layers at low opacity:

ClassGenerated shadow layers
shadow-2xs0 1px rgb(0 0 0 / 0.05)
shadow-xs0 1px 2px 0 rgb(0 0 0 / 0.05)
shadow-sm, shadow0 1px 3px 0 and 0 1px 2px -1px, both rgb(0 0 0 / 0.1)
shadow-md0 4px 6px -1px and 0 2px 4px -2px
shadow-lg0 10px 15px -3px and 0 4px 6px -4px
shadow-xl0 20px 25px -5px and 0 8px 10px -6px
shadow-2xl0 25px 50px -12px rgb(0 0 0 / 0.25)
shadow-none0 0 #0000

Bigger offset and blur means the element looks higher above the page. The clever part is how they combine. Every shadow and ring class writes into its own variable, and box-shadow reads all five, so classes stack rather than replace each other. A card with shadow-md ring-2 ring-blue-500 has the blue ring and the shadow, which Chrome showed as one list: four ring and offset layers (three empty, then the 2px blue ring) followed by 0 4px 6px -1px and 0 2px 4px -2px. Without the ring classes the same list has an empty slot where the ring was. Other shadow classes: inset-shadow-sm (a shadow inside), and a coloured shadow with shadow-blue-500/50.

A warning for code you copy from v3 projects: v4 renamed the scales so that every size has a name. The upgrade guide's table: v3 shadow-sm is now shadow-xs and v3 shadow is now shadow-sm; the same shift applies to rounded, blur, drop-shadow and backdrop-blur. The bare names still work (I saw that shadow and shadow-sm generate the same CSS in 4.3.3), but copied shadow-sm classes will look slightly stronger than they did in v3.

Filters. drop-shadow-md is a different tool: it is a filter that traces the shape of what is drawn, so it follows a transparent PNG or SVG outline where box-shadow would draw a rectangle. blur-sm blurs the element (blur(8px)); backdrop-blur-md blurs what is behind the element (blur(12px)), which gives the "frosted glass" panel when combined with a partly transparent background such as bg-white/50. The blur sizes come from the theme: xs 4px, sm 8px, md 12px, lg 16px, xl 24px, 2xl 40px, 3xl 64px.

Hands-On Exercises

Exercise 1

A page has a "Subscribe" button (bg-blue-500 text-white) and a grey caption (text-gray-400) on white. Measure the contrast of both pairs in the browser, say which fail WCAG AA for normal text, and change the smallest possible thing in each to make them pass. Show the new ratios.

๐Ÿ“„ View solution
Exercise 2

Make a card with padding, a 1px light-grey border, 12px rounded corners and a medium shadow. First write it with plain border and no colour, and read the computed border colour; then fix it. Finally add a "selected" state with a 2px blue ring and show that the shadow is still there.

๐Ÿ“„ View solution
Exercise 3

Build a hero banner: a left-to-right gradient from cyan-500 through blue-500 to indigo-600, with a darkening overlay at 40% and white heading text above it. Print the computed gradient. Then rebuild the overlay the wrong way (bg-black opacity-40 around the text) and prove from the pixels what happens to coloured text.

๐Ÿ“„ View solution

Chapter 3 Quick Reference

  • 26 colour families ร— 11 shades (50โ€“950) plus black and white; low number = light; colours are oklch() theme variables
  • Prefixes: text- bg- border- divide- ring- outline- decoration- accent- caret- fill- stroke- shadow-; specials transparent, current, inherit; off-palette bg-[#bada55], bg-(--brand)
  • Placeholder colour: placeholder-gray-400 or placeholder:text-gray-400 (same ::placeholder rule)
  • Opacity: bg-blue-500/50 changes only that colour (color-mix); opacity-50 fades the element and everything in it
  • Contrast (WCAG AA): 4.5:1 normal text, 3:1 large. On white: gray-500 passes (4.84), gray-400 fails (2.60); white on blue-600 passes (5.25), on blue-500 fails (3.76)
  • Gradients: bg-linear-to-r from-X via-Y to-Z, bg-linear-45, bg-radial, bg-conic, stops from-10%; bg-clip-text text-transparent for gradient text
  • Borders: border 1px, border-2, sides border-t, style border-dashed; v4 border colour defaults to the text colour: always add border-gray-200 or similar
  • Radius: rounded-xs 2px, sm/rounded 4px, md 6px, lg 8px, xl 12px, 2xl 16px, full circle/pill; rounded-t-lg, rounded-s-md
  • Rings are box-shadow, take no space: ring-2 ring-blue-500, ring-offset-2, inset-ring; bare ring is 1px and currentcolor in v4 (ring-3 for the old width); outlines: outline-2 outline-offset-2; outline-hidden for custom focus (keeps a forced-colours fallback; v4's outline-none removes it)
  • Shadows shadow-2xsโ€ฆshadow-2xl stack with rings in one box-shadow (v3's shadow-sm is v4's shadow-xs); drop-shadow-* follows the shape; backdrop-blur-md + bg-white/50 = frosted glass
Coming next
Tailwind CSS Fundamentals 4 is layout: display, flexbox and grid, with the centring and sidebar recipes you will use constantly.