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.
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():
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:
| Prefix | Colours | CSS property |
|---|---|---|
| text-blue-600 | text | color |
| bg-blue-500 | background | background-color |
| border-gray-200 | border | border-color |
| divide-gray-200 | the lines between children | border-color on every child but the last |
| ring-blue-500, outline-blue-500 | rings and outlines | --tw-ring-color, outline-color |
| decoration-blue-500 | underlines | text-decoration-color |
| accent-blue-500, caret-blue-500 | checkboxes and sliders; the text cursor | accent-color, caret-color |
| fill-blue-500, stroke-blue-500 | SVG shapes | fill, stroke |
| shadow-blue-500/50 | the 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:
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 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:
| Pair | Ratio | Normal text (4.5:1) | Large text (3:1) |
|---|---|---|---|
| text-gray-400 on white | 2.60 | fails | fails |
| text-gray-500 on white | 4.84 | passes | passes |
| text-gray-600 on white | 7.56 | passes | passes |
| text-gray-700 on white | 10.30 | passes | passes |
| text-gray-900 on white | 17.75 | passes | passes |
| text-white on bg-blue-500 | 3.76 | fails | passes |
| text-white on bg-blue-600 | 5.25 | passes | passes |
| text-white on bg-blue-700 | 6.83 | passes | passes |
| text-blue-500 on white | 3.76 | fails | passes |
| text-blue-600 on white | 5.25 | passes | passes |
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:
| Class | CSS |
|---|---|
| bg-cover, bg-contain | background-size: cover | contain |
| bg-center, bg-no-repeat, bg-fixed | background-position: center, background-repeat: no-repeat, background-attachment: fixed |
| bg-[url('/img/hero.jpg')] | background-image: url('/img/hero.jpg') |
| bg-clip-text text-transparent | background-clip: text and color: transparent: lets a gradient show through the letters |
A gradient is made from a direction class plus colour stops:
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:
| Class | CSS |
|---|---|
| border | border-style: var(--tw-border-style); border-width: 1px |
| border-2, border-4, border-0 | the same with 2px, 4px, 0px |
| border-t, border-x-4 | border-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-dotted | border-style: solid | dashed | dotted |
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:
| Class | Radius | Class | Radius |
|---|---|---|---|
| rounded-xs | 0.125rem (2px) | rounded-xl | 0.75rem (12px) |
| rounded-sm, rounded | 0.25rem (4px) | rounded-2xl | 1rem (16px) |
| rounded-md | 0.375rem (6px) | rounded-3xl, rounded-4xl | 1.5rem, 2rem |
| rounded-lg | 0.5rem (8px) | rounded-full | calc(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:
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:
| Class | Generated shadow layers |
|---|---|
| shadow-2xs | 0 1px rgb(0 0 0 / 0.05) |
| shadow-xs | 0 1px 2px 0 rgb(0 0 0 / 0.05) |
| shadow-sm, shadow | 0 1px 3px 0 and 0 1px 2px -1px, both rgb(0 0 0 / 0.1) |
| shadow-md | 0 4px 6px -1px and 0 2px 4px -2px |
| shadow-lg | 0 10px 15px -3px and 0 4px 6px -4px |
| shadow-xl | 0 20px 25px -5px and 0 8px 10px -6px |
| shadow-2xl | 0 25px 50px -12px rgb(0 0 0 / 0.25) |
| shadow-none | 0 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
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.
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.
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.
Chapter 3 Quick Reference
- 26 colour families ร 11 shades (50โ950) plus
blackandwhite; low number = light; colours areoklch()theme variables - Prefixes:
text- bg- border- divide- ring- outline- decoration- accent- caret- fill- stroke- shadow-; specialstransparent,current,inherit; off-palettebg-[#bada55],bg-(--brand) - Placeholder colour:
placeholder-gray-400orplaceholder:text-gray-400(same::placeholderrule) - Opacity:
bg-blue-500/50changes only that colour (color-mix);opacity-50fades the element and everything in it - Contrast (WCAG AA): 4.5:1 normal text, 3:1 large. On white:
gray-500passes (4.84),gray-400fails (2.60); white onblue-600passes (5.25), onblue-500fails (3.76) - Gradients:
bg-linear-to-r from-X via-Y to-Z,bg-linear-45,bg-radial,bg-conic, stopsfrom-10%;bg-clip-text text-transparentfor gradient text - Borders:
border1px,border-2, sidesborder-t, styleborder-dashed; v4 border colour defaults to the text colour: always addborder-gray-200or similar - Radius:
rounded-xs2px,sm/rounded4px,md6px,lg8px,xl12px,2xl16px,fullcircle/pill;rounded-t-lg,rounded-s-md - Rings are
box-shadow, take no space:ring-2 ring-blue-500,ring-offset-2,inset-ring; bareringis 1px andcurrentcolorin v4 (ring-3for the old width); outlines:outline-2 outline-offset-2;outline-hiddenfor custom focus (keeps a forced-colours fallback; v4'soutline-noneremoves it) - Shadows
shadow-2xsโฆshadow-2xlstack with rings in onebox-shadow(v3'sshadow-smis v4'sshadow-xs);drop-shadow-*follows the shape;backdrop-blur-md+bg-white/50= frosted glass
display, flexbox and grid, with the centring and sidebar recipes you will use constantly.