Skip to content

feat(registry)!: adopt the new Remix APIs in the Vanilla preset - #342

Draft
leoafarias wants to merge 17 commits into
stack/follow-ups-integrationfrom
feat/registry-vanilla-adopt-remix-apis
Draft

leoafarias wants to merge 17 commits into
stack/follow-ups-integrationfrom
feat/registry-vanilla-adopt-remix-apis

Conversation

@leoafarias

@leoafarias leoafarias commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Draft, stacked on #339. Do not merge until remix 1.0.0-beta.14 and naked_ui 1.3.0 are on pub.dev.

Vanilla now uses the Remix APIs from the open follow-up PRs.

  • Focus rings: tab, toggle, toggle group option, menu trigger, accordion, and disclosure draw vanillaFocusRing() through containerEffects (triggerEffects on accordion and disclosure) instead of a foreground decoration.
    • A toggle group option draws its ring inside itself (inset: true), because the group clips.
    • vanillaFocusRingDecoration is removed.
    • The accordion ring used to wrap the whole opened section. It now wraps the trigger only.
  • Labels: the radio and switch recipes set the new label as a form label (textSm, w500, foreground), with a 12px gap on a radio and 8px on a switch. The fixture and widgetbook demos pass label.
  • Kebab: the order-table kebab is a real ghost VanillaIconButton passed to RemixMenuTrigger.control. It has one focus stop, the menu's expanded state is merged onto the button, and the button keeps a muted fill and glyph while the menu is open, including under the pointer.
  • Select width: the panel is max(128, trigger width). It keeps the 128px minimum and adds remixAnchorWidth as a second minimum through a box modifier.
  • Account menu: the dashboard account trigger draws its own focus ring through a RemixCard with containerEffects, and its border turns ring on focus, because a RemixMenuTrigger.builder replaces the styled trigger and its effects. The menu docs' avatar example now does the same.
  • Popovers (fix(naked_ui): keep focus on the caller's control in a popover opened without tap #338): the dashboard header popovers (notifications, theme settings) and the fixture popover use openOnTap: false with one FocusNode shared by the popover and its button. The button is the only tab stop, keeps its own accessible name, shows its focus ring, and gets focus back on Escape. Before, the popover's wrapper took focus with no visible ring and announced the popover's label instead. The Vanilla popover recipe's doc comment shows the same pattern.
  • Spec deviation: the inset ring on toggle group options is recorded in specs/vanilla.md. The outline toggle now draws its shadow-xs through the effects slot.

Left as is:

  • the account menu stays a RemixMenuTrigger.builder trigger, because it is a profile row, not a button. Only its focus ring is new;
  • the settings rows' semanticLabel.

Restacked onto the refreshed integration hub #339 (d5990f4d) on main ca9331be. Remains DRAFT and release-blocked until naked_ui 1.3.0 and Remix 1.0.0-beta.14 are available on pub.dev and their runtime/registry floor updates are approved. Registry dependency floors are unchanged.

Tab, toggle, toggle group option, menu trigger, accordion, and disclosure
drew their ring with a foreground decoration, which needed a second helper
with its own radius and inset arguments and, on the accordion, ringed the
whole section so the ring grew with the revealed panel.

They now use vanillaFocusRing in containerEffects, or triggerEffects on the
accordion and disclosure, so the ring follows the part's own corners, takes
no layout space, and wraps only the trigger. A toggle group option rings
inside itself with the new inset argument because the group clips. The
vanillaFocusRingDecoration helper is removed.
RemixRadio and RemixSwitch take a label drawn inside the control's own tap
target, focus ring, and accessible name. The Vanilla recipes now set it as a
form label like the checkbox does, textSm at medium weight in foreground,
12px from a radio and 8px from a switch, so callers no longer compose a
Row of the control and a Text.

The fixture gallery and the Vanilla widgetbook radio and switch cases pass
label instead.
…button

The order table kebab was a Semantics-wrapped Box passed as a visual
RemixMenuTrigger.builder, so it had no hover, focus ring, or disabled state
of its own. It is now a ghost VanillaIconButton given to
RemixMenuTrigger.control with the menu handle's focusNode and toggle, keeping
the muted glyph. The menu still owns the overlay, Escape, and focus return,
and merges its expanded state onto the button, which stays the one focus stop.
… trigger

The using-remix Vanilla reference lists the single focus ring helper and the
slots it goes in, the radio and switch label, and how to open a menu from a
real button, and both it and the open-code guide tell an older install to
reinstall the ring recipes with the theme now that the decoration helper is
gone.
…debar

The ring probe matched a drawPath call, so most cases failed against the old
foreground-decoration recipes for how the ring was painted rather than for
what a reader sees. It now compares the frame before and after focus: the
ring's color laid over what was there at each point it must cover, and no
change at the points it must not, keeping the size and neighbor checks.

A sidebar destination case is added, and the state distinguishability case
for the sidebar destination reads the spec as before instead of its effects
layer. RemixSidebar draws destinations with RowBox, which does not paint
containerEffects, so both fail until the sidebar paints them.
… trigger

shadcn's default item-aligned select never draws the panel narrower than its
trigger. The panel keeps its 128px minimum and adds remixAnchorWidth as a
second minimum through a box modifier, so the panel is max(128, trigger) and a
caller who replaces the floor through style still keeps the trigger's width.
…is open

shadcn's dashboard block fills the kebab with muted while its menu is open.
The button reads the menu handle's isOpen and merges that fill in, and the
dashboard test checks the fill follows the menu open and closed.
…bel note

The Vanilla spec and the open-code guide said the focus ring is always drawn
outside with no offset. A toggle group option draws it inside because the
group clips its options, where the reference lifts the focused item with
focus-visible:z-10, so the spec gains a deviation row and the guide notes it.

The using-remix Vanilla reference no longer says the radio and switch label
sits inside the focus ring: the label is part of the tap and focus target,
and the ring wraps the circle or track only. The toggle's "No shadow" comment
is dropped, since a decoration shadow is no longer the reason the outline
toggle has none, and the spec row says so.
The dashboard's account row is a RemixMenuTrigger.builder, and a builder
replaces the styled trigger and its effects, so the row was a keyboard focus
stop that showed no focus. Its content is now a RemixCard that draws
vanillaFocusRing() in containerEffects on focus-visible, resolved under the
menu's widget states, so the ring paints outside the row and moves nothing.
It is the only builder trigger in the Vanilla recipes.

The ring test focuses the real trigger in the dashboard and checks the ring
pixels and that the trigger and its content keep their boxes. The menu docs'
avatar builder example draws its own ring and says why, and the Vanilla
reference notes it.
…Tap false

The fixture and the Vanilla popover recipe docs said a button trigger needs
a controller because the button consumes the tap that openOnTap waits for.
NakedPopover with openOnTap false no longer wraps its child in a focus stop of
its own, so the button can be the only tab stop and closing can return focus
to it when the popover and the button share one FocusNode.

The fixture gallery now passes openOnTap false, the shared node as
triggerFocusNode, and the same node to the button, keeping the controller
toggle. The fixture test asserts one focus stop inside the popover, that
focus leaves the button when the panel opens, and that Escape closes the
panel and returns focus to the button. The popover recipe doc comment shows
the same pattern, with its template and installed copies regenerated.
The notifications and theme settings popovers in the dashboard header held a
VanillaIconButton with no onPressed inside a popover that kept the default
openOnTap. The popover's own wrapper was the one focus stop and the one
semantics node, so the styled button inside could never take focus: keyboard
focus showed no ring, and the button's own label was hidden behind the
popover's.

A small stateful trigger now owns the button: openOnTap is false, the button
toggles a MenuController, and one FocusNode, owned and disposed by that State,
is both the popover's triggerFocusNode and the button's focusNode. The button
is the only focus stop, rings on keyboard focus, names itself, reports the
expanded state the popover's anchor puts on it, and gets focus back when the
popover closes. The popover's semanticLabel, ignored with openOnTap false, is
dropped.

The dashboard test checks one focus stop, the button's label and expanded
state, opening and closing from the button, and focus returning on Escape for
both triggers; the ring test checks the ring pixels for both.
The kebab fills with muted while its menu is open, but Mix applies the active
hover and press variants after the base, so with the pointer still on the
button the icon turned accentForeground and, in the dark theme, the fill
became accent at half strength.

The open style now repeats its fill and muted glyph in its hover and press
variants, so it stays in force under the pointer. The dashboard test opens the
menu with a mouse that is already hovering the button and holds a press, in
both themes.
The account row is bordered, and a bordered control turns its own border
ring while it has focus, as the buttons, the text field, the select trigger,
and the outline toggle do. The row's focus fragment drew only the ring band,
so the border stayed its resting color.

The focus fragment now also sets vanillaFocusBorder(). Only the color
changes, so the row keeps its box. The ring test checks the border pixel turns
ring over the row's card fill.
The reference's outline toggle carries shadow-xs, and the toggle spec now has
an effects layer that cuts the box out of its shadow the way CSS does, as the
outline icon button, radio, and switch already use. The outline variant draws
the xs shadow through containerEffects behindContent, and keeps it while the
ring is drawn on focus.

The spec targets read the shadow for the outline variant, no shadow for the
ghost variant, and both the shadow and the ring when focused. The deviation row
now covers only the toggle group strip, which has no effects layer.
…tests

The popover recipe's doc example now says the controller and focus node live
in a State and the node is disposed there, as the Naked UI example does, with
the installed copies regenerated. The Vanilla reference gains a Popover
bullet for the openOnTap false pattern.

The fixture popover drops a semanticLabel that openOnTap false ignores. The
header popover ring test checks the adjacent appearance toggle stays put
rather than a button across the header, and a probe comment now says the
captured frame has one pixel per logical pixel.
@leoafarias
leoafarias force-pushed the stack/follow-ups-integration branch from c931a3a to d5990f4 Compare October 29, 2026 21:44
@leoafarias
leoafarias force-pushed the feat/registry-vanilla-adopt-remix-apis branch from 1444ab2 to 615d647 Compare October 29, 2026 21:44

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant