Preflight, Tailwind's built-in CSS reset
Preflight, the CSS reset Tailwind loads for free, and why clearing the browser's defaults gives your design system a clean canvas to build on.
You scaffold a fresh Tailwind app, drop in a chunk of plain, semantic HTML of the kind you’d write on any other day, and the page comes back looking wrong.
<div className="card"> <h1>Upgrade to Pro</h1> <ul> <li>Unlimited projects</li> <li>Priority support</li> </ul> <button>Start trial</button></div>The <h1> is the same size and weight as the list items, body text wearing a heading’s tag.
The <ul> has lost its bullets and indent, so the features sit flush against the left edge.
The <button> has no border, no fill, no native look, just inline text.
You wrote correct markup, and the browser rendered something that looks unstyled in a way it never does anywhere else.
Almost everyone reacts the same way, and you may have too: Tailwind broke my HTML.
It didn’t.
The moment you added @import "tailwindcss" back in the last chapter, Tailwind ran a small, deliberate reset called Preflight, and Preflight deleted the browser’s default styling on purpose.
The bold heading, the bullets, the native button: those styles were never yours.
They came from the browser, and Tailwind stripped them so you could start from nothing.
This lesson takes you from “broke my HTML” to “blank canvas”: a flattened surface you want. You’ll see exactly what got deleted, recognize it in DevTools, and learn the two narrow cases where an engineer brings some defaults back on purpose.
Two earlier lessons left threads to pick up here.
In The cascade, Preflight lives in @layer base, which is why a bare <h1> has nothing for your text-2xl to fight against.
In CSS inheritance, Preflight’s one font: inherit rule re-opens form controls to your typography.
Both were named in passing; here they get explained in full.
Preflight is the base layer, loaded for free
Section titled “Preflight is the base layer, loaded for free”Preflight is a small set of CSS rules Tailwind ships and you never wrote.
You wrote one line, @import "tailwindcss", and it loads more than your utilities.
Expanded, here is what it pulls in:
@layer theme, base, components, utilities;@import "tailwindcss/theme.css" layer(theme);@import "tailwindcss/preflight.css" layer(base);@import "tailwindcss/utilities.css" layer(utilities);The third line places Preflight in layer(base), the same base layer you sorted your own resets into in The cascade, and that placement decides everything that follows.
Recall the layer gate from The cascade: a declaration in a later layer beats one in an earlier layer no matter how specific either selector is, and the gate is checked before specificity gets a turn.
Preflight sits in base; your utilities sit in utilities, the last layer declared.
So your utilities win every conflict with Preflight automatically, which is why you never out-specify it or reach for !important against it.
Why ship a reset at all? Browser defaults are a liability for a design system: decades of accumulated decisions, they differ between browsers, and not one is on your scale. A default heading isn’t on your type scale, a default margin isn’t on your spacing scale, a default button doesn’t read your color tokens. Preflight clears all of that away, so every bit of visual weight on the page comes from a utility you wrote: predictable, tokenized, and identical in every browser.
What Preflight strips, element by element
Section titled “What Preflight strips, element by element”You don’t need to recite Preflight’s stylesheet. You need to predict the flattened look of a common element and recognize each reset for what it is: a browser default being subtracted, with the replacement always the same. Styling is now a utility’s job.
The sequence below renders one fragment of markup, a card with a heading, a short list, a line of text, and a button, and walks it from raw browser defaults to the flat Preflight surface, one reset at a time. The page isn’t degrading; it is being cleared.
Upgrade to Pro
- Unlimited projects
- Priority support
Your trial ends in 3 days.
Upgrade to Pro
- Unlimited projects
- Priority support
Your trial ends in 3 days.
Upgrade to Pro
- Unlimited projects
- Priority support
Your trial ends in 3 days.
Headings inherit body size and weight. Nothing left for text-2xl to override.
Upgrade to Pro
- Unlimited projects
- Priority support
Your trial ends in 3 days.
Upgrade to Pro
- Unlimited projects
- Priority support
Your trial ends in 3 days.
font: inherit; color: inherit re-opens the button to your typography. With the border reset gone too, it’s an unadorned run of text.
Upgrade to Pro
- Unlimited projects
- Priority support
Your trial ends in 3 days.
Upgrade to Pro
- Unlimited projects
- Priority support
Your trial ends in 3 days.
Now you paint. text-2xl font-bold on the heading, space-y-* on the list, a real button: the weight is back, but this time it’s yours and on your scale.
Here are Preflight’s headline resets in one place, the literal CSS Tailwind ships, each tied to what it deletes and which earlier thread it picks up.
*,::before,::after { box-sizing: border-box; border: 0 solid; margin: 0; padding: 0;}
h1, h2, h3, h4, h5, h6 { font-size: inherit; font-weight: inherit;}
ol, ul, menu { list-style: none;}
button, input, select, textarea { font: inherit; color: inherit;}Margins and padding go to zero on every element, plus ::before and ::after. No default space above an <h1>, between paragraphs, or around lists, <figure>, or <hr>. Spacing stops being an accidental browser value and becomes yours, declared with gap, p-*, and m-* on your --spacing scale. (The course prefers gap to sibling margins, a choice the box-model chapter covers.)
*,::before,::after { box-sizing: border-box; border: 0 solid; margin: 0; padding: 0;}
h1, h2, h3, h4, h5, h6 { font-size: inherit; font-weight: inherit;}
ol, ul, menu { list-style: none;}
button, input, select, textarea { font: inherit; color: inherit;}Two resets in one rule. box-sizing: border-box makes every element measure its padding and border inside its declared width, the sensible model the next chapter covers in full. And border: 0 solid matters more than it looks: it pre-sets every border to zero width and solid style. Your border utility sets only the width, so it renders a visible line only because Preflight already set the style to solid. Strip Preflight away and a bare border paints nothing, because a border’s default style is none. No color is pinned here, so a fresh border starts at the current text color, currentColor (the thread from CSS inheritance), until you set an explicit border-* color.
*,::before,::after { box-sizing: border-box; border: 0 solid; margin: 0; padding: 0;}
h1, h2, h3, h4, h5, h6 { font-size: inherit; font-weight: inherit;}
ol, ul, menu { list-style: none;}
button, input, select, textarea { font: inherit; color: inherit;}Headings are flattened, the thread from The cascade. A bare <h1> inherits its size and weight, so it renders at body size and weight. There’s no large default left for text-2xl font-bold to override. A heading’s size and weight are now entirely whatever utility you put on it.
*,::before,::after { box-sizing: border-box; border: 0 solid; margin: 0; padding: 0;}
h1, h2, h3, h4, h5, h6 { font-size: inherit; font-weight: inherit;}
ol, ul, menu { list-style: none;}
button, input, select, textarea { font: inherit; color: inherit;}Lists go unstyled: no bullets, no numbers, and the margin/padding reset already removed the indent. This can feel like a loss, but look at what a real web UI is made of: nav menus, command palettes, option lists, dropdowns. Almost all are a <ul> semantically and not bulleted visually, so a bulletless list is what you want roughly nine times in ten. The tenth case, a genuinely bulleted prose list, is handled by prose, coming up.
*,::before,::after { box-sizing: border-box; border: 0 solid; margin: 0; padding: 0;}
h1, h2, h3, h4, h5, h6 { font-size: inherit; font-weight: inherit;}
ol, ul, menu { list-style: none;}
button, input, select, textarea { font: inherit; color: inherit;}Form controls re-inherit your typography, the thread from CSS inheritance. font: inherit; color: inherit makes a <button> drop its native OS typography and take your body font and color instead, so controls that normally resist inheritance rejoin the cascade you set up on <body>.
A few more resets are worth a sentence each, because you’ll see their effects but rarely think about them.
Images and media go block-level and constrained. Preflight sets img, svg, video, … { display: block; vertical-align: middle } and img, video { max-width: 100%; height: auto }. The first removes the mysterious sliver of space under an inline image (the inline baseline gap); the second stops an image from overflowing its container.
Links lose their underline, and their color inherits. A bare <a> comes out body-colored rather than browser-blue, with no underline, a clean slate you’ll style per design with something like text-primary underline-offset-*, or get for free from a component later.
A few more are real but low-stakes: tables get border-collapse, [hidden] is enforced as display: none, and a placeholder’s color resolves to the current text color at 50% opacity rather than a hardcoded grey. Recognize them if they come up, but don’t memorize them.
One reset people expect and won’t find: Preflight does not set cursor: pointer on buttons. Tailwind v4 removed it, so a bare <button> uses the browser’s default cursor: default. If a button feels unclickable, the cursor is your job: you add cursor-pointer, or in practice shadcn’s <Button> handles it for you.
Is it the reset, or is it a bug?
Section titled “Is it the reset, or is it a bug?”The skill to take away: when an element comes out flattened, decide whether that’s Preflight doing its job or an actual mistake. Most of the time it’s the reset, and the fix is to add a utility. Occasionally it’s a real bug. The goal is to tell the two apart.
You’re staring at a fresh Tailwind app, nothing painted yet. Which symptoms are Preflight doing its job — flattened on purpose, where the fix (if you even want a difference) is to add a utility — rather than an actual bug? Select all that apply.
<h1> lands at the exact size and weight of the paragraph sitting under it.<select> shed its chunky native OS look and now reads in your body font.border utility draws no line at all.text-2xI never matched anything.The two intended cases are the first (h1–h6 inherit body size and weight) and the second (font: inherit on form controls re-opens them to your typography). The reset cleared a browser default; if you want the difference back, that’s a utility’s job.
The other two are real bugs. Stripping the import deletes the border: 0 solid reset, so a border’s style falls back to none; your border utility sets width, not style, and paints nothing without Preflight underneath. And text-2xI is a typo (capital I, not lowercase l): the class never landed, so trace it the way The cascade taught. The reusable test: the reset is doing its job; the utility is your job.
Reading Preflight in DevTools
Section titled “Reading Preflight in DevTools”Select any flattened element, open the Styles panel, and scroll to the bottom.
Beneath every other rule you’ll find Preflight’s, in a section labeled @layer base.
That label confirms three things at once: it’s a reset, it sits structurally beneath everything, and any utility you add lands in @layer utilities above it and wins cleanly.
This is not the cause of a problem; it’s confirmation that the reset is where it should be.
text-2xl sits in the later layer, so it
wins. Its declarations aren't struck.
font-size: inherit is struck through:
the reset did its job, your utility took over.
Carry one sentence away from that view: the reset is doing its job, and the utility is my job.
The same panel tells you when the problem isn’t Preflight.
Suppose text-2xl really is there in @layer utilities and the heading is still body-sized.
Trace it the way The cascade taught: open Computed, expand font-size, and read which rule won and from which layer.
Often it’s an unlayered rule of your own sitting above utilities and stealing the win, and the trace points you straight at it.
When you bring defaults back, do it on purpose
Section titled “When you bring defaults back, do it on purpose”An experienced engineer does not strip Preflight to undo the flattening. They paint on the blank canvas with utilities, and bring browser defaults back only in two tightly bounded situations. Any fix outside these two is the wrong one.
Carve-out 1: prose for content you didn’t author
Section titled “Carve-out 1: prose for content you didn’t author”Sometimes your app renders a blog post, a changelog, or a help article written in Markdown or pulled from a CMS.
What reaches your component is a blob of generated HTML, <h2>, <p>, <ul>, <blockquote>, <code>, <a>, none of it carrying your classes.
You can’t put a utility on each element, because you never wrote the elements.
And here you want the typographic defaults back, with real heading sizes, bulleted lists, styled links, and sensible spacing, because long-form reading is where they earn their keep.
That’s what the @tailwindcss/typography plugin is for. You install it in your CSS next to the import:
@import "tailwindcss";@plugin "@tailwindcss/typography";and wrap the rendered content in the prose class:
<article className="prose dark:prose-invert max-w-prose"> {/* rendered Markdown lives here */}</article>The family: prose is the base, prose-sm and prose-lg resize it, prose-invert flips it for dark mode, max-w-prose caps the line length for readability, and max-w-none removes that cap.
The key point is that prose is not “undoing Preflight.” It doesn’t strip the reset or override it. It’s a scoped, tokenized typographic system applied to exactly the subtree that needs it, the one place you can’t reach the elements one by one. It even themes through @theme tokens like --prose-body and --prose-headings, so it stays on your design system rather than escaping it.
Carve-out 2: a scoped @layer base override for a third-party widget
Section titled “Carve-out 2: a scoped @layer base override for a third-party widget”The second case is embedding something you don’t control: a payment provider’s iframe host, an embedded rich-text editor, or a legacy script that injects its own DOM.
The widget expects browser defaults and visibly breaks without them.
You bring the defaults back, but scoped to that widget’s container, and you keep the override inside @layer base so it stays beneath your utilities and doesn’t leak into the rest of the app:
@layer base { .third-party-widget :where(h1, h2, h3) { font-size: revert; font-weight: revert; }}Two details here draw on earlier parts of the chapter.
revert, from CSS inheritance, rolls the property back to the user-agent value, which is exactly “give me the browser default again.”
And the @layer base plus :where() choice comes straight from The cascade: keeping the override in base means your utilities still win everywhere they apply, and :where() holds its specificity at zero so it never starts a specificity conflict.
The three fixes to reject
Section titled “The three fixes to reject”With the two legitimate moves in hand, the wrong ones are easy to name.
Don’t strip Preflight globally to “fix” the flattened look. You’d lose box-sizing: border-box, the border-style reset (so your borders stop rendering, the dependency you just met), and form-control inheritance. The flattening is local; the cost of stripping the reset is everywhere.
Don’t @apply h1 { … } or write a global h1 { font-size: … } to “restore” headings. That re-introduces an element rule competing with your utilities, the exact anti-pattern The cascade warned against. Put the utilities on markup you own; reach for prose on content you don’t.
Don’t reach for !important. There is nothing here to override. Preflight is in base, your utility is in utilities, and the utility already wins the layer gate before importance or specificity is consulted. You’d be forcing a win you already have.
One rare case is worth recognizing: omitting Preflight entirely, which is legitimate when you ship utilities into an environment that already has its own reset, such as a widget mounted in a third-party site. For a standalone web app, Preflight always loads.
Match the situation to the fix
Section titled “Match the situation to the fix”Four situations, four correct moves, including the case where the right move is simply to add a utility.
Match each situation to the right move. Click an item on the left, then its match on the right. Press Check when done.
<h1> on markup you wrote looks like body text.text-2xl font-bold on it.prose class.@layer base override using revert.@layer base.Building components on the cleared canvas
Section titled “Building components on the cleared canvas”Every example here styled a bare <button>, <input>, or <h1>. On a real project you rarely do.
shadcn’s <Button> and <Input>, and your own components, are built on top of the blank canvas Preflight provides: they assume the reset ran, then add tokenized styling on a surface where nothing competes with them.
A <Button> looks identical in every browser because Preflight cleared the browser’s opinion first.
That same @layer and inheritance machinery is what makes design tokens predictable: a value set high in the tree, flowing down, overridable in a subtree. That’s the next lesson, Custom properties & tokens.
External resources
Section titled “External resources”The official reference for every reset Preflight applies — the canonical list behind this lesson's catalog, kept current with the v4 source.
The typography plugin that powers the prose class — install, the prose / prose-invert / max-w-prose family, and how to theme it with tokens.
Andy Bell on why a reset exists at all and what each rule buys you — the durable why behind Preflight, framework-agnostic.