SidebarPrimary
The primary navigation rail, with a collapsible width.
Usage
import {
SidebarPrimary,
SidebarPrimaryHeader,
SidebarPrimaryContent,
SidebarPrimaryFooter,
SidebarPrimarySection,
SidebarPrimaryMenu,
SidebarPrimaryMenuItem,
SidebarPrimaryMenuItemExtras,
SidebarPrimaryCollapseTrigger,
} from '@acronis-platform/ui-react';SidebarPrimary is the top-level navigation rail. It renders a <nav> and owns
a controlled/uncontrolled expanded (rail width) state. Compose a
SidebarPrimaryHeader, a SidebarPrimaryContent (one or more
SidebarPrimarySections of SidebarPrimaryMenu → SidebarPrimaryMenuItem), and
a SidebarPrimaryFooter. SidebarPrimary and SidebarPrimaryMenuItem are
polymorphic via Base UI's useRender (the render prop) — pass a router link
for menu items. Every menu item carries a leading icon, since that icon is the
only visible part of the row once the rail is collapsed. Themed by the
--ui-sidebar-primary-* tokens.
Examples
A full rail:
<SidebarPrimary>
<SidebarPrimaryHeader logo={<ProductLogo />} collapsedLogo={<ProductMark />} />
<SidebarPrimaryContent>
<SidebarPrimarySection>
<SidebarPrimaryMenu>
<SidebarPrimaryMenuItem href="#" icon={<MonitorIcon />} selected>
Assets
</SidebarPrimaryMenuItem>
<SidebarPrimaryMenuItem href="#" icon={<ShieldCheckIcon />}>
Protection management
</SidebarPrimaryMenuItem>
</SidebarPrimaryMenu>
</SidebarPrimarySection>
</SidebarPrimaryContent>
<SidebarPrimaryFooter>
<SidebarPrimaryMenu>
<SidebarPrimaryCollapseTrigger
icon={<ChevronsLeftIcon />}
expandTooltip="Expand menu"
>
Collapse menu
</SidebarPrimaryCollapseTrigger>
</SidebarPrimaryMenu>
</SidebarPrimaryFooter>
</SidebarPrimary>A trailing affordance goes in the extras prop, not in children — children
is the label, and anything nested there is clipped by its truncation. Figma only
offers size="sm" for the tag slot, so always pass a small Tag:
<SidebarPrimaryMenuItem
href="#"
icon={<InboxIcon />}
extras={
<SidebarPrimaryMenuItemExtras
variant="tag"
tag={
<Tag variant="info" size="sm">
3
</Tag>
}
/>
}
>
My inbox
</SidebarPrimaryMenuItem>Controlled collapse — the trigger above still works, it just reports the next value instead of owning it:
const [expanded, setExpanded] = React.useState(true);
<SidebarPrimary expanded={expanded} onExpandedChange={setExpanded}>
{/* … */}
</SidebarPrimary>icon is required on a menu item, so a row that genuinely has no icon has to opt
out explicitly with noIcon:
<SidebarPrimaryMenuItem href="#" noIcon>
Legacy console
</SidebarPrimaryMenuItem>Behavior
Collapsing the rail
SidebarPrimaryCollapseTriggerflips the rail width itself: it updates the uncontrolled state and always emitsonExpandedChange, so it works in both controlled and uncontrolled modes.- One
iconcovers both states — it rotates 180° when the rail collapses (and the rotation inverts under RTL, so a chevron keeps pointing the way the rail will move). Don't pass a separate "expand" glyph. - Collapsed, labels stay in the DOM as
sr-only, trailing extras are hidden, the header padding shrinks and the logo swaps tocollapsedLogo. The header row keeps a constant height across the transition so the menu below it doesn't jump while the two logo graphics swap. That height is the larger of the two states'padding-y × 2 + logo heightsums — with the defaults,max(16×2 + 32, 8×2 + 48)= 64px in both states. - Because that clamp is unconditional,
logoHeightandcollapsedLogoHeightare not independent for row height: each sizes only its own state's logo, but whichever state produces the larger sum sets the row height for both states. RaisinglogoHeightto 64, for instance, makes the shared rowmax(16×2 + 32, 8×2 + 64)= 80px, so the collapsed header grows too even though its 32px mark is unchanged. That's deliberate — it's what keeps the row from jumping — but budget for it when overriding either prop. - While collapsed, the trigger shows
expandTooltip(defaults to'Expand') — pass a localized string.
Tooltips on truncated and collapsed labels
- Expanded: a row whose label is actually clipped opens a tooltip with the full label. Rows that fit never open one, and hovering the icon or the extras doesn't open it either — only the label does.
- Collapsed: every row always opens a tooltip with its label, because the
sr-onlylabel itself can't be hovered. - Either way the tooltip is anchored to the whole row rather than the shrinking label, and opens to the side (right in LTR, left in RTL) instead of on top.
- Wrap the rail (or the app) in
TooltipProviderto share open/close delays. In a shadow-DOM host, wrap it inPortalContainerProviderso these internal tooltips portal inside the shadow root — see Shadow DOM.
Right-to-left
- Spacing, text alignment and the collapse icon's rotation are pure CSS
(logical properties plus
ltr:/rtl:variants), so they mirror off the nearestdirancestor. - Tooltip placement is decided in JS from the document root's direction (its
dirattribute or computeddirection). Adir="rtl"set only on an inner wrapper mirrors the layout but leaves tooltips opening to the right.
Accessibility
- The root is a
<nav>landmark;aria-labeldefaults to"Primary"— override it when a page has more than one navigation landmark. - Menus are
<ul>/<li>and rows are native<a>s (or whateverrendersupplies). The current row carriesaria-current="page". - Tab moves between rows; both Enter and Space activate one (Space is handled explicitly so link rows match the button-rendered ones).
- The collapse trigger is a
<button>whosearia-expandedreflects the rail state. - Labels are never removed when collapsed, only visually hidden, so each icon-only row keeps its accessible name.
API Reference
SidebarPrimary
SidebarPrimaryHeader
logo and collapsedLogo let you swap distinct graphics per rail state (e.g.
a full lockup vs. a monogram) instead of resizing/hiding one node. When
neither is given, children renders in both states.
The header sizes the logo slot to the design system default (48px expanded,
32px collapsed) unless overridden. If a tenant-branded logo is authored taller
than that default, pass logoHeight/collapsedLogoHeight (in pixels) instead
of letting the default height force-downscale it. The anti-"jump" row-height
clamp (see Behavior) picks up the override
automatically, so the header row still doesn't jump mid-transition — but note
that it pins both states to the larger of the two
padding-y × 2 + logo height sums. Each prop sizes only its own state's logo,
yet either one can grow the other state's header row:
<SidebarPrimaryHeader
logo={<TenantLogo />}
collapsedLogo={<TenantMark />}
logoHeight={64}
collapsedLogoHeight={40}
/>Here the expanded sum is 8×2 + 64 = 80px and the collapsed sum is
16×2 + 40 = 72px, so both states get an 80px header row — 16px taller
than the 64px row you'd get with no overrides at all (max(16×2 + 32, 8×2 + 48)).
logoHeight alone accounts for all of that growth: drop
collapsedLogoHeight back to its 32px default and the row is still 80px
(max(16×2 + 32, 8×2 + 64)), because the expanded sum is the larger one
either way.
The collapsed rail also constrains the mark's width: at 48px wide with 8px
of horizontal padding, the collapsed logo has a 32px content box. Give
collapsedLogo a portrait-oriented mark — narrower than it is tall — so a
taller collapsedLogoHeight doesn't push it past that box.
SidebarPrimaryMenuItem
icon looks optional below because the table flattens the prop union; at the
type level an item must supply either icon or noIcon, never neither.
SidebarPrimaryMenuItemExtras
SidebarPrimaryCollapseTrigger
The other structural parts (SidebarPrimaryContent, SidebarPrimaryFooter,
SidebarPrimarySection, SidebarPrimaryMenu) accept the standard attributes
of the element they render.