preflight & css reset

[check: …]
📖 Pair this live Tailwind demo with the companion guide (.md) — this page is the rendered ground truth. ↗ Sibling to @source & content detection (Phase 7) — Preflight is the base layer that ships unconditionally; @source decides which utility classes ship on top of it.

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.

@import "tailwindcss"; /* expands to (layer order declared first, lowest → highest priority): */ @layer theme, base, components, utilities; @import "tailwindcss/theme.css" layer(theme); @import "tailwindcss/preflight.css" layer(base); /* ← Preflight lives here */ @import "tailwindcss/utilities.css" layer(utilities);
layerwhat shipsprioritycan override?
themedesign tokens as CSS variables (--color-*, --text-*)1 (lowest)via @theme
basePreflight (the reset) + your @layer base rules2add rules in @layer base
componentscomponent classes (v4 ships this empty by default)3via @layer components
utilitiesthe utility classes Tailwind generates from your source4 (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 / selectorbrowser defaultafter preflightwhy
* (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.)

class="border"
color → red, border follows
*, ::before, ::after { box-sizing: border-box; border: 0 solid; /* width:0 style:solid color:currentColor */ } /* so `.border { border-width: 1px }` ⇒ 1px solid currentColor */

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.)

reset by preflight

h1 (bare)

restored via @layer base

h1.restore-h1

@layer base { h1 { font-size: var(--text-2xl); font-weight: 700; } /* restore headings */ a { color: var(--color-blue-600); text-decoration: underline; } /* or undo a specific reset — e.g. keep bullets on real lists: */ ul[role="list"] { list-style: disc; padding-left: 1.5rem; } /* or scope Preflight away from a third-party widget: */ .google-map * { border-style: none; } }

reset h1 font-size: — · restored h1 font-size: — (target: 2rem / 32px)

6 · preflight: v3 → v4 changes

behaviorv3v4migration 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

intentpatternwhy
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]