Skip to content
PolyFish CSS

Coming from Tailwind

The habit map from utility strings to what PolyFish does instead — and the honest list of what does not translate.

· ~4 min read · 0.21.0 in the changelog

The common reading of PolyFish is "a framework that is not Tailwind", which is true but unhelpful. The better question is what a page looks like when the thing you reach for is a component rather than a string of utilities — and what to do with the muscle memory the utilities built.

The reflex map

Most of the Tailwind vocabulary has a direct counterpart, and the counterpart is usually shorter because the decision it encodes has already been made once, inside the class.

Tailwind reflexPolyFishWhy it is different
flex gap-4.pf-clusterRow with wrapping and alignment decided once — the primitives own their overflow behaviour
flex flex-col gap-6.pf-stackVertical rhythm as a flow rule, not a flexbox
grid grid-cols-3 gap-4.pf-grid / .pf-switcherColumns come from content width, not a count: auto-fit means no empty columns at the wrap
p-4 m-2 mt-8--pf-space-* tokens + gap utilitiesSpacing is rhythm between siblings, not padding on everything — the gap does the margin's job
md:flex-row.pf-md-* variants, .pf-container-qComponent breakpoints answer "is there room here?", not "is the window wide?"
dark:A theme file or .pf-invertDark is a swapped stylesheet of eleven roles, not a prefix on every class
hover:bg-blue-700Baked into the componentHover is a state the button already owns; you only name it when you deviate
@applyThe component class itself.pf-btn is the composed utility string, pre-composed and named
tailwind.config:root { --pf-* }Config is a runtime stylesheet, not a build input — retinting a deployed page needs no rebuild
prose plugin.pf-proseSame idea, different spelling: typographic rhythm for article bodies

The bigger swap: where decisions live

Tailwind puts the decision in the markup — every element carries its layout, spacing and state. PolyFish puts the same decisions in the class once: .pf-btn knows its own padding, radius, hover and focus ring, and a page of buttons stays a page of pf-btn rather than a page of identical twenty-class strings that drift apart the first time someone edits one.

That is also the limit of the translation. Utilities compose to anything; components compose to what the framework decided. When the component is wrong for you, the answer is not a longer class string — it is your own class reading the same tokens, which inherits every theme for free.

What the variants actually are

The reflex worth unlearning is the breakpoint prefix. md: answers a question about the window; PolyFish's .pf-container-q answers a question about the space the component is actually in — a card in a sidebar adapts even when the window is wide. Where a window-level switch really is the intent (hide on mobile, swap a row), the .pf-md-* helpers exist, two tiers rather than five.

Arbitrary values, honestly

w-[347px] has no PolyFish spelling and never will. The escape hatch is a custom property or three lines in your own stylesheet, which is what arbitrary values compile to anyway — minus the compiler. The whole framework works the same way: the tokens are public, and the cheapest way to deviate is to write CSS that reads them.

Figures are measured from the build they describe.

What this doesn’t cover

A map is not the territory. The table translates the reflexes most pages actually use; niche utilities — aspect ratios, scroll snapping, masks, the plugin ecosystem — either have no counterpart or deserve their own entry. And the mapping runs one way: patterns in the other direction, like anchor-positioned overlays or invoker commands, are what PolyFish offers that no utility string can write.

Sources: the class reference for the PolyFish side, and Tailwind’s own documentation for the utility spellings. The mapping table was written for this post; the claims underneath it — two breakpoint tiers, eleven roles, the bundle’s gzip weight — are measured from the current build.

Guide Migration