Skip to content
PolyFish CSS

Cascade layers and right-to-left

Cascade layers mean your CSS always wins, and the framework now mirrors itself under dir=rtl.

· ~4 min read · 0.17.0 in the changelog

This is the biggest release since we split the modules, and I'll be upfront: one of the two changes renames things. Nobody enjoys a rename. I'll explain why this one was worth it, and what we did so it does not cost you an afternoon.

Cascade layers

Every module now wraps its rules in @layer polyfish.<module>, and every file declares the full layer order so it holds regardless of the order you link them. Unlayered CSS beats layered CSS whatever its specificity, which means your own stylesheet overrides any framework rule with a plain class selector. No !important, and no dependence on Utilities loading last.

Right to left

182 margin-left declarations, 34 padding-left, 18 border-left, 20 positioning offsets and 16 text alignments became their logical equivalents. A document with dir="rtl" now mirrors itself with no second stylesheet and no [dir] selectors.

That exposed a naming problem of the same kind as the colour tokens in 0.16: .pf-pr-4 implied a physical side while applying a logical one. The directional spacing utilities are now .pf-ps-*, .pf-pe-*, .pf-ms-* and .pf-me-*. All 48 old names are aliased one-for-one in the compatibility file.

Still 0.x

This is not a 1.0. The framework stays pre-1.0 until it has been through a Playwright run across five device profiles with screenshot baselines, a screen reader pass, and a Lighthouse run. Until then a minor release may still rename things — every rename is aliased in the compatibility file.

Epilogue, three months on

Layers did what we wanted: your unlayered CSS beats the framework without !important, and we have a browser test that proves it. What the original post did not say — because we had not yet learned it — is what layers cost.

Specificity stops being the tiebreaker inside the framework too. A rule in polyfish.core loses to a rule in polyfish.content however specific it is, and there is no warning: the overridden rule is valid CSS that simply never applies. We shipped that bug twice. A 16px form-field floor for touch devices, written in Core, was silently overridden by Content's field styles for two minor versions, and iPhones zoomed on every focused field. .pf-invert, which flips a panel to the other end of the palette, painted from Core and lost its background to .pf-card, leaving dark text on a dark card.

The rule we now follow: a declaration that has to beat a component belongs in that component's own module, and where it cannot, it says which layer it is written into and why. Both cases are now enforced by the checker rather than by memory. If you are adopting layers in your own system, that is the part to plan for — not the syntax, which is easy, but the fact that your own internal overrides now need a layer strategy as deliberate as the one you designed for your users.

What this post does not cover

  • Bidirectional text handling beyond layout mirroring — dir and logical properties are not the whole of bidi.
  • Vertical writing modes, which the framework does not currently style for.

Checked against Cascade layers, logical. Figures are measured from the build they describe.

What this doesn’t cover

Layers do not help with specificity inside a layer, and they do nothing for the rules you write outside the framework — those always win, which is the point. Nothing here has been tested in a browser that does not support @layer; the framework assumes it, rather than degrading to something sensible without it. And logical properties only mirror what you wrote logically: an illustration with baked-in text, or a chevron drawn pointing one way, stays pointing that way.

Sources: MDN on @layer, CSS Cascading and Inheritance Level 5, and the framework architecture page. No code blocks in this post; the layer order quoted in prose is the one declared at the top of every module.

Cascade Layers RTL Logical Properties Naming Release CSS