-
v0.4.0 Stable
released this
2026-08-28 13:45:01 +00:00 | 260 commits to main since this releaseBreaking
Sidebarfollows the D13 layered split instead of a shared controller, so
mountSidebaris no longer exported from@[secure]/ui/utils/controllers.
utils/controllers/is for code both tiers run; Sidebar's imperative
orchestration is Astro-only and now lives beside the component, exported as
@[secure]/ui/components/Sidebar.mount. Vue no longer wraps it at all —
Sidebar.vueis reactive throughout, which is what D13 already required
("wrapping an imperative DOM controller from Vue fights Vue's data flow…
the controller mutates the DOM, Vue patches it back, and race conditions
ensue"). Both tiers now derive from shared primitives in
@[secure]/ui/utils/navigation/sidebar:isHrefActive,deriveGroupKey,
computeSubmenuPosition,isSubmenuActive,groupContainsActive.
isHrefActiveandderiveGroupKeymoved there from the controller barrel.Sidebar'sitemsprop is nowtopItems, alongside a newbottomItems.
The column has two nav regions — the top nav, which takes the leftover
height and scrolls, and the bottom nav, pinned to the foot below a divider
for settings, account and the like. Entries in either get identical wiring:
active-href matching, keyboard navigation, and collapsed flyouts for their
groups. The#footerslot is renamed#bottomand still takes arbitrary
content besidebottomItems. Migrate:items="nav"to:top-items="nav".- The private recursive renderer is
SidebarMenuItem(was
SidebarItemRender), matching thedata-sidebar-menu-itemhook it emits.
Internal to the component — no export changed — but the file names did. - The nav list classes are
.pui-sidebar__top-navand
.pui-sidebar__bottom-nav(was.pui-sidebar__menu), and the foot wrapper
is.pui-sidebar__bottom(was.pui-sidebar__footer). Consumers styling
those internals will need to follow. Sidebarno longer uses a roving tabindex. Every menu entry is its own tab
stop, as an ordinary<nav>full of links should be. Roving left the whole
sidebar with a single tab stop, so a rail of four destinations had three
that Tab could never reach. This also matches CONVENTIONS.md, which reserves
roving for "single-focus-per-group controls … where one item represents the
entire group's focus target" — a navigation landmark is not that. Arrow keys,
Home and End still work, now as an accelerator on top of the natural order
rather than a replacement for it. Consumers relying on exactly one sidebar tab
stop will see more; nothing else changes.
Changed
--space-sidebar-width-collapsedis4.75rem(was4rem). At 4rem the new
rail captions had about 2rem of usable width once the panel's padding was
taken out, and real labels ellipsised to two characters.- The
Sidebarfocus ring colour follows the entry's tone via
--pui-sidebar-ring, matching how--btn-ringworks for Button: neutral by
default, brand on the entry wearing the brand wash, so the ring no longer
fights the fill underneath it. - The collapsed rail marks which icon the open flyout belongs to. Previously
only hover did, so a flyout opened from the keyboard left nothing pointing
back at its icon once focus moved into the panel. - The collapsed submenu flyout now sits on
--layer-below, so it genuinely
passes under the rail rather than across it — the panel's own background
hides where the slide starts. The open-flyout rule pairs the rung with
isolation: isolate, because "behind the rail" only holds while the sidebar
is the stacking context containing the flyout; without it the panel would go
behind the page in any shell that doesn't happen to give the root a
context. - The collapsed submenu flyout slides in from behind the rail when it opens,
and disappears instantly when it closes. The transition is declared only on
the open state — a transition belongs to the state being moved to — so
there is nothing to undeclare for the closing direction. An exit animation on
a dismissal only delays the content behind it, and the panel covers page
content.visibilitystays untransitioned so the entries are focusable the
instant the group takes focus; underprefers-reduced-motionthe panel
simply appears. - The collapsed
Sidebarrail stacks a caption under each icon instead of
going icon-only, so it is navigable without hovering. The rail sets its own
inline padding rather than inheriting the expanded panel's, and captions wrap
to a second line instead of truncating — an ellipsis hides the one thing a
caption is for. - A collapsed rail group whose subtree contains the current page carries the
brand wash. On the rail its children are hidden, so the group is the only
indicator of location, and a tint alone lost to the hover fill — a hovered
entry read as more current than the current one. The wash is reverted in
drawer mode, where the sidebar is a full expanded list: there the group sits
directly above its own current child and two identical fills merged into a
single block. Brand ink alone carries "this section holds the current page"
there, and the wash is left to the entry that actually is the current page. - The
Sidebarfocus ring uses--outline-width/--outline-offsetinstead
of hard-coded3px/2px. It is the standard outset ring, so it reads as a
ring around the entry rather than a line cutting into it and stays legible
over the selected entry's wash; the collapsed rail reserves inline padding so
the column edge never clips it. - A
Sidebarlink's selected state uses--bg-clr-brand-softrather than
--bg-clr-accent, which is what hover already uses — the current page was
indistinguishable from whatever the pointer was over.
Fixed
Sidebar's focus ring is no longer clipped in the top nav. That list is a
scroll container —overflow-y: autoforcesoverflow-x: auto— and a
scroll container clips at its padding box, so an entry filling it
edge-to-edge had the ring sliced off both sides. The list now reserves the
ring's width aspadding-inlinewith a matching negativemargin-inline,
so nothing moves.- Consumers must import
@[secure]/ui/stylesbefore any component. Each
component stylesheet is a bare@layer components { … }, and layer order is
fixed by first encounter — so a component loading first registers
componentsahead ofbase, and every base element rule starts outranking
component rules. In the Vue playground that made base'sa { color: … }beat
.pui-sidebar__link, turning sidebar links brand-coloured. The playground
entry point now imports the stylesheet first; the library contract is
unchanged (main.css declares the order once). - The mobile
Sidebardrawer no longer paints under the consumer's app header.
The rule that lifts the sidebar while a collapsed flyout is open applied at
every width, and az-indexother thanautomakes a grid or flex item a
stacking context even whileposition: static— which is what a sidebar in a
layout shell normally is. The drawer's ownz-index: 50was therefore sealed
inside that context and lost to any header above 30. The lift is now scoped
above the drawer breakpoint, where the flyout actually exists, and the drawer
lifts the root itself while open. Sidebar's focus handler no longer rewrites every entry'stabindex. The
roving pattern was removed from the initial render but survived in the
focusinhandler, which demoted every other entry to-1the moment one was
focused — so the sidebar looked fully tabbable until you actually tabbed into
it, and then collapsed to a single stop.- A collapsed submenu flyout's entries are reachable by Tab.
visibilitywas in
the flyout's transition list, so it flipped partway through the fade and the
entries were still unfocusable when Tab arrived — landing on a group jumped
straight past its flyout to the next rail item.visibilitynow steps
immediately on open and is delayed only on close, so the fade-out still plays. - The collapsed rail no longer stays visible below
drawerBreakpoint. The
drawer'swidth: 0sat on a bare.pui-sidebarinside a media query, which
grants no specificity, so.pui-sidebar[data-state='collapsed']outranked it
and left an empty bordered strip down the page on mobile. - The collapsed submenu flyout aligns flush with the top of the sidebar column
instead of the menu region. The menu's box starts inside the panel's padding,
which left a visible seam above the flyout. With a#brandslot filled the
flyout starts below the brand, so it still never covers the wordmark. Icon.astroforwards aclassprop instead of silently dropping it. It
hardcodedclass="pui-icon"and spread the rest of the props after, so an
incoming class was discarded — Vue applied it, Astro did not. One casualty
wasSidebar'spui-sidebar__group-carethook, which meant the caret never
rotated on expand in Astro.- A
Sidebarbrand slot no longer spills across the page when collapsed. The
rule that was meant to hide everything withoutdata-keep-when-collapsedwas
an empty placeholder, so a wordmark kept its full width in a rail narrower
than most product names. - The collapsed submenu flyout no longer renders behind page content. It is
position: fixed, which escapes overflow but not a stacking context, and
consumers routinely make the sidebarposition: stickyfor scroll — which
creates one. The root now raises its own z-index while a flyout is open. - A closed flyout is positioned as soon as it enters panel mode. A fixed
element with no coordinates resolves to its static position inside the rail,
so anything that made it briefly visible flashed the panel over the icons.
Added
Sidebartakes abottomItemsprop — the bottom nav. Same
SidebarItem[]shape astopItems, rendered at the foot of the column
below a divider, for settings / account / help. See the Breaking entry for
the accompanyingitems→topItemsrename.Sidebartakes acollapsedSubmenuprop ('hidden' | 'panel', default
'hidden'). With'panel', a collapsed rail's groups are no longer a dead
end: hover, focus, or click opens a group's children in a full-height flyout
beside the rail. Escape closes it and returns focus to the group toggle;
clicking the toggle pins the flyout so it survives pointer-out; only one
flyout is open at a time. The default keeps the previous behavior, so
existing consumers are unaffected.mountSidebargainsopenSubmenu(key),closeSubmenu(), and
openSubmenuKey(), plus acollapsedSubmenuconfig field. They are no-ops
unless the policy is active — which needs the sidebar collapsed and the
viewport abovedrawerBreakpoint, since below it there is no rail.- A stacking ladder:
--layer-below,--layer-0…--layer-5,--layer-top.
Every positioned layer in the library names a rung instead of inventing a
number, so the whole order is legible in one place and two components cannot
accidentally tie.Toast,Table's loading pane,NumberField's focused
step button and everySidebarlayer moved onto it; the numbers they
resolve to are unchanged. --shadow-lg, for a surface that covers page content rather than sitting on
it.--shadow-mdis tuned for a card in the flow and reads flat under
something as large as a sidebar flyout.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads