1 · preflight in the CSS cascade
@import "tailwindcss" is sugar for three layered imports.
Preflight lands in the base layer — between the design
tokens (theme) and your utilities (utilities).
Layer order, not source order, decides who wins: a utility always beats
a Preflight rule of the same specificity, and your @layer base
additions ride alongside Preflight inside the same base layer.
| layer | what ships | priority | can override? |
|---|---|---|---|
theme | design tokens as CSS variables (--color-*, --text-*) | 1 (lowest) | via @theme |
base | Preflight (the reset) + your @layer base rules | 2 | add rules in @layer base |
components | component classes (v4 ships this empty by default) | 3 | via @layer components |
utilities | the utility classes Tailwind generates from your source | 4 (highest) | — (always wins) |
Because utilities sit in the highest-priority layer, a single
text-2xl on an <h1> beats Preflight's
h1 { font-size: inherit } — no !important needed.
2 · WITH preflight vs WITHOUT (bare HTML)
Left: a <div> styled only by Preflight (no utility
classes) — headings collapse to body text, the list loses its bullets.
Right: the same markup in an <iframe> with no Tailwind
at all — raw browser defaults (big <h1>, disc bullets).
Preflight is the difference between the two columns.
WITH preflight (this page)
h1 — same size as body
h2 — also same size
paragraph — zero margin.
- no bullet
- no bullet
- no bullet
WITHOUT (bare iframe, no tailwind)
h1 computed font-size: — (UA default would be 2em / 32px)
3 · what preflight resets (element → reset)
| element / selector | browser default | after preflight | why |
|---|---|---|---|
* (all) |
margin: various; box-sizing: content-box |
margin: 0; padding: 0; box-sizing: border-box |
spacing comes only from your scale; width includes padding/border |
* borders |
border: 0 none currentColor (no visible border) |
border: 0 solid (color → currentColor) |
so class="border" alone draws a 1px solid currentColor line |
h1–h6 |
big sizes + bold (e.g. h1 ≈ 2em) |
font-size: inherit; font-weight: inherit |
avoid sizes outside your type scale; style headings deliberately |
ol, ul, menu |
disc / decimal markers, padding | list-style: none (no markers) |
most UI lists are layout, not outlines — opt markers back in with list-disc |
img, svg, video, … |
display: inline (baseline gap!) |
display: block; vertical-align: middle |
kills the mysterious under-image whitespace gap |
img, video |
intrinsic size (can overflow) | max-width: 100%; height: auto |
responsive by default — never wider than the parent |
button, input, … |
system font, doesn't inherit | font: inherit (v4) |
form controls match your typography |
[hidden] |
display: none |
display: none !important (unless hidden="until-found") |
a utility can't accidentally reveal a hidden element |
Built on modern-normalize — Preflight adds the opinionated resets above on top of it.
4 · border default = solid + currentColor
Preflight's universal border: 0 solid means the
border utility (which sets only border-width)
draws a solid 1px line in the element's own text color. In v4 the
border color is currentColor — change the text color and the
border follows. (v3 defaulted to gray-200 instead.)
border-color: — · color: — (should match = currentColor)
5 · overriding preflight in @layer base
Don't want headings unstyled? Add base styles in the same
base layer Preflight lives in. Because your rule and
Preflight share a layer, normal specificity + source order decide — a
class beats the bare h1 selector, and a later same-specificity
rule wins. (Utilities still beat both, since the utilities
layer is higher priority.)
h1 (bare)
h1.restore-h1
reset h1 font-size: — · restored h1 font-size: — (target: 2rem / 32px)
6 · preflight: v3 → v4 changes
| behavior | v3 | v4 | migration note |
|---|---|---|---|
| default border color | gray-200 (Preflight forced it) |
currentColor (CSS default, no override) |
borders now follow text color; add border-gray-200 to restore the old look |
| button / form font | partly inherited | font: inherit fully |
form controls now match body type — fewer font-sans overrides needed |
::placeholder |
color forced to gray-400, opacity: 1 |
opacity: 1, color inherits (no forced gray) |
placeholders inherit text color; set placeholder:text-gray-400 explicitly if needed |
ring default width |
3px with offset |
1px, uses currentColor |
ring now draws a hairline — use ring-4 for the old thickness |
| layer architecture | two layers (base, components, utilities via @layer) |
native cascade layers (@layer theme, base, components, utilities) |
theme tokens are now first-class CSS variables in their own layer |
intent → pattern
| intent | pattern | why |
|---|---|---|
| let preflight do its thing | @import "tailwindcss"; |
Preflight is injected into base automatically — nothing else needed |
| restore heading styles | @layer base { h1 { font-size: var(--text-2xl) } } |
same layer as Preflight; class/utility still beats it |
| give a real list its bullets back | <ul class="list-inside list-disc"> or <ul role="list"> + base style |
list-disc is a utility; role="list" keeps VoiceOver announcing it |
| disable preflight entirely | omit @import "tailwindcss/preflight.css" layer(base); |
import theme + utilities only — for embedding Tailwind into an existing design system |
| scope preflight away from a widget | @layer base { .widget * { border-style: none } } |
third-party libs (Google Maps) can break under Preflight's border reset |
| get the old v3 gray border | class="border border-gray-200" |
v4 border defaults to currentColor — set the color explicitly |
| hide something permanently | hidden attribute or class="hidden" |
Preflight forces display:none !important on [hidden] |