14 August 2026CSSDesign systemsTailwind

A component library has no business resetting your document

Three interlocking CSS bugs, one default nobody warns you about, and the reason it stayed invisible in my own Storybook.

I maintain a component library that four of my own apps depend on and nobody else has a stake in. That makes me both the author and the person who finds out what shipping it actually costs. For a while, what it cost was a slow drip of styling bugs I could not account for.

A button had a permanent border that no rule explained. A row of avatars lost its spacing when a wrapper was added. aspect-video silently did nothing. Every one of these looked like a bug in the consuming app. Every one of them was Roster.

The root cause

The default path ships everything

The compiled stylesheet was 100KB, which is a lot for forty components. Inside it was an entire Tailwind build: preflight, the theme layer, and every utility the library touched, all unlayered, all landing in the consuming app at whatever specificity they felt like.

That was not a decision so much as a default. Point Vite’s library mode at a CSS entry containing @import "tailwindcss" and it compiles the framework into your dist. The granular entrypoints that let you take only the parts you need — tailwindcss/theme.css, tailwindcss/utilities.css — exist and are documented, but nothing steers you toward them, and every library setup guide I had read did exactly what I did.

What kept it alive is that the bug is invisible from the inside. In Storybook, the library is the app, so a bundled Tailwind build is simply correct there: preflight is doing its job, the utilities resolve, everything looks right. The failure only exists where the library meets an app that already has its own Tailwind, which is the one arrangement a component library’s own tooling never reproduces.

Three separate failures came out of that one default.

The reset. Preflight sets * { margin: 0; padding: 0; border: 0 solid }. Unlayered, that outranks any layered utility. So the library’s own padding and border utilities were being erased by the library’s own reset. That was the permanent border: a button whose padding-left measured 0px no matter what the class list said.

The variables. Tailwind’s utilities coordinate through --tw-* custom properties. Two copies of Tailwind in one document means two sets of those, and the second one to load wins. That was aspect-video doing nothing: the app set the variable, the library reset it.

The order. With everything unlayered, whether the library won or lost came down to import order in a file most consumers never think about.

Import order was load-bearing, which is another way of saying nobody had decided anything.
The tell I kept ignoring
The fix

Three changes, none of them clever

Preflight became opt-in. It ships from its own entry point now. If your app runs Tailwind you already have a reset and you import nothing; if it does not, you ask for one explicitly.

Everything got wrapped in a cascade layer. The library imports Tailwind’s theme and utilities inside @layer roster, and declares the full order up front:

@layer roster-preflight, theme, base, components, roster, utilities;

Layers are ranked by first declaration, not by specificity, so this one line settles every fight in advance. The library sits above base, so a host preflight cannot erase its spacing. It sits below utilities, so the app’s own classes still win. Import order stopped mattering, which is the actual goal.

Tokens moved to @theme inline, so the library’s utilities compile to var(--roster-primary-500, #0f6498) rather than to a hex. A consuming app can then repigment the whole library by redefining custom properties it already controls.

The part I got wrong twice

Green tests are not the same as a working page

After dropping preflight, all 514 tests passed. Every component also rendered in Times New Roman with bulleted lists, which was an unpleasant surprise when I checked Storybook.

The fix for that collapsed space-x-* spacing, and the suite stayed green through that too. Unit tests assert behavior and class names. Neither regression touched either one.

A test suite that cannot see the page will happily certify a page nobody can read.
The part worth keeping

There is a version of this post where I claim I designed the layer order from first principles. What actually happened: I shipped a broken major version with bad advice attached, “import the library before Tailwind,” verified against a single class that happened to pass for unrelated reasons. The layer declaration exists because I got it wrong in a way a more careful check would have caught.

What I would tell you

If you publish CSS

Ship no reset. Wrap everything you emit in a named layer and declare the order in your own stylesheet, so a consumer inherits a working arrangement instead of debugging one. Compile to custom properties rather than values, so your palette is a suggestion instead of a decision. And look at the page, in both themes, before you believe your tests.