Maquina Components, the server-rendered UI component library for Rails and Tailwind, is out in 0.6.0. It adds a token layer for radius, elevation, focus rings and font weight, moves all engine CSS into @layer components, and fixes a focus ring that had been dead on six of seven button variants. The release is deliberately breaking: seven changes, and the first one fails silently in every existing application.
Until now the library was themeable in color and in nothing else. Radius, elevation, focus rings, font weight and hover states were written directly into the stylesheets, so changing any of them meant overriding selectors rather than declaring values. Theming one real application against the 0.5 releases took roughly 1,700 lines of override CSS, and most of that was not expressing a design. It was reaching past the cascade.
Why tokens alone would not have fixed it
Every engine rule was unlayered and carried specificity from its variant and state qualifiers, so appearance and structure shared one flat cascade. A theme could not reach the appearance without also being able to break the structure, and the engine defended against that by being hard to override at all. Adding tokens on top of that arrangement would have changed nothing.
Three things changed together. There is now a token layer: --control-radius, --surface-radius, --focus-ring-width, --elevation-raised, --label-weight and the rest, declared in @theme and read from every rule that used to hardcode a value. All twenty stylesheets moved into @layer components, which is what lets a caller’s Tailwind utilities apply. And specificity is flat now: every rule sits at 0,1,0, with variants and states in :where(), so a theme’s [data-component="button"] means every button, which it previously did not.
Which gives a contract worth stating plainly: a theme changes values, not selectors. Reach for a selector only when you want to change a component’s shape, like a different padding rhythm or a variant the engine does not ship.
A flat theme is six lines:
:root {
--elevation-control: none;
--elevation-raised: none;
--elevation-overlay: none;
--control-radius: 0.25rem;
--surface-radius: 0.25rem;
}
The theming guide has the full token reference. Every component demo on the documentation site now carries a shape toggle in its chrome; flip it to brutal or soft and the whole library changes shape from token declarations alone.
The focus ring was dead
[data-component="button"]:focus-visible was declared before the variant rules, at the same specificity. Each variant then re-declared box-shadow for its own elevation, and later-at-equal-specificity wins. The focus ring was silently overwritten on every variant that set a shadow.
On the demo page, two of the sixteen buttons showed a ring, and both were destructive — the only variant that happened to re-declare its own focus rule after its variant rule. Primary, secondary, outline, ghost and link had no visible keyboard focus at all. That is a WCAG 2.4.7 failure. It shipped, and it was invisible in code review because every rule involved looked correct on its own.
Focus is now an outline rather than a box-shadow. Outlines do not participate in box-shadow, so a variant’s elevation can no longer overwrite a ring. They survive forced-colors mode, and they are not clipped by overflow: hidden ancestors, which had been quietly cutting rings off inside the sidebar and drawer. Every focusable button rings now, and a test asserts that state rules follow variant rules in every stylesheet, so the ordering that caused this cannot come back.
Your utility classes now win
css_classes: is the documented way to adjust one instance of a component, and it has been partly a lie. Because engine rules were unlayered, they beat any Tailwind utility passed through them:
[data-component="input"]setw-full, so anyw-*you passed was dead.[data-form-part="actions"]setdisplay: flex, sosm:hiddendid nothing.[data-component="form"]setdisplay: grid. Passsm:flex-rowand it silently stopped being a row.
These are layout failures, not restyles, and they failed quietly, which is why the workaround was always a wrapper element. With the engine in @layer components, utilities win. Measured: an input with a width utility goes from 448px to 137px.
Worth searching your views for css_classes: after upgrading. Anything you passed as decoration and never saw is about to take effect.
Before you upgrade: run the scanner
The release ships a scanner. Run it inside your application:
bin/rails maquina:doctor
It reads your CSS, views and JavaScript and prints file:line for every pattern this release changes, grouped by severity: the unlayered * rule, component overrides the token layer makes redundant, restated SVG data URIs, [data-active] presence selectors, .dark twins. It never edits anything and always exits 0.
Breaking changes
Seven. The first affects every existing application and fails silently.
-
The preflight shim in your
theme.cssnow outranks the engine. Your installedtheme.csscarries this rule:* { border-color: var(--color-border); }Unlayered CSS outranks every layer at any specificity. Now that engine rules live in
@layer components, that one universal rule wins over the tinted borders on all alert and toast variants: a destructive alert renders with a plain grey border where 0.5.1 painted a red one. The generator template is fixed, but the rule lives in your file. Wrap it:@layer base { * { border-color: var(--color-border); } } - Utility classes now win. Anything passed through
css_classes:that was previously overridden by an engine rule will take effect. - Radius and elevation defaults normalize. Card goes 12px to 8px, popovers 6px to 8px, and four
shadow-lgsites collapse to--elevation-overlay. - Focus rings become outlines, and form fields stop ringing on mouse click.
merge_component_dataprecedence narrows to identity keys.- Surfaces that sit above the page stop painting
--background. - Tinted badges lose a stray hairline the shim had been forcing onto them.
Every one of them is a value, so the upgrading guide closes with an appendix that restores the 0.5.1 look with a single token block.
Upgrading
bundle update maquina-components
Then re-run the installer to append the new shape and state tokens to your theme. It is idempotent and will not touch your palette:
bin/rails generate maquina_components:install
Then read the upgrading guide.
Also in this release
New:
- Drawer gained
title,description,sectionandseparatorpartials. The first two had been styled by CSS since the beginning with nothing to emit them, so the documentation told you to hand-write<h2 class="text-lg font-semibold">. - Sidebar gained menu badges, menu actions and group actions on the same footing.
- There is a
labelpartial now, which makes the required-field indicator reachable.
Fixed:
dropdown_menu_simpleraisedNoMethodErrorandcombobox_simplerendered an empty popover. Both had zero call sites in the repository, which is exactly why they shipped broken.- Two components were building correct data attributes and then discarding them, so
[data-variant="bordered"]on a table was unreachable.
What this reinforced
Every bug in this release looked correct in the stylesheet. The focus ring rule was right there in the file, and the dead table variant was right there in the partial. What caught them was asserting on compiled output and computed styles: does this token reach the browser, does this rule come after that one, does this element actually have a ring. The tests that came out of the audit assert those things.
The quieter finding was a set of styled hooks that turned out to be emitted by nothing at all — CSS that read as supported API and matched no markup. That is worse than a missing feature, because it looks finished. Six became real partials here and two were deleted.