Sidebar
The persistent navigation of an application shell, with a configuration surface of its own.
A sidebar is the navigation an application keeps on screen: destinations gathered into sections, each with an icon, a label and optionally a count, in a column that can fold down to its icons when the content needs the room. What separates it from a menu is that it stays: it is a landmark with a current destination, not a popover with a selection.
It is the largest of the navigation components because it has a surface of its own: the column collapses, the labels it loses come back as tooltips, its rows are dressed in two states separately, and its edge is drawn in four treatments. Each of those is a token in the cascade, so a product states its navigation once.
Usage
import { Sidebar } from "@asheeui/web";
<Sidebar
items={[
{ id: "dashboard", label: "Dashboard", href: "/dashboard", icon: <DashboardIcon /> },
{ id: "campaigns", label: "Campaigns", href: "/campaigns", badge: 12 },
]}
activeKey="dashboard"
title="Ashee CRM"
/>
items takes one of two shapes: a flat list of destinations, or a list of sections that each
hold destinations. The first entry decides which, so an array is read as sections when its first
entry has items of its own. The test is made once, on that entry, so a mixed array is not read
as anything: state sections with sections and destinations with destinations. A section carries
an optional label, shown above its rows while the column is expanded, and the same roles an
item does.
An item is an id, a label and a href, plus an icon before the label, a badge after it,
a target and rel for a destination that leaves the site, roles and disabled. Its id is
what activeKey is compared against and what onSelect reports, and label is the row's text
and the only other required part.
onSelect receives the whole item on every press, including a press on a row that also has an
href, so a page can record the choice and let the link navigate. A disabled row drops its
href and its pointer and is dimmed, so it is drawn without being reachable: it is for a
destination that is not ready rather than for one the reader must be told about.
userRole filters both levels: a section or an item with roles is shown only to a reader whose
userRole matches, and a role-guarded entry is hidden when userRole is not given at all. This
is what a reader is shown rather than what a reader is allowed, so the server still refuses what
the filter leaves out.
Every row is a link. link states the component that renders one and the props it takes, for
the whole column at once, and anchorProps states anchor attributes for every row as well; the
link props are merged last and win. Substituting a router's link is
the same seam every navigation component uses.
The header appears when there is a title, an onBack, or both: onBack puts a back control
before the title, which is where a column below a list of workspaces wants it, and backIcon
replaces its glyph. footer is a slot at the foot of the column, for a reader chip, an invite
button, a version — or the page's own control that folds the column.
A sidebar is the navigation column of an application shell rather than a page of its own;
SidebarLayout places it beside the content. See
Page compositions.
Collapsing
Two options decide whether the column can fold and how it starts. collapsible (registered
true) is whether folding is possible at all, and defaultCollapsed (registered false) is
where an uncontrolled column starts. isCollapsed with onCollapseChange makes the state
controlled; without them the column keeps its own, starting from defaultCollapsed, and still
reports each change.
The control that folds the column is the application's, not the framework's: a sidebar that draws
its own button decides the shape, the words and the place of something the page owns. Read the
state the column publishes with useSidebarCollapse, which returns isCollapsed, collapsible,
setCollapsed and toggleCollapse, and draw whatever button fits the product. A control inside
the column — its footer slot, for instance — reads the hook directly:
import { Sidebar, useSidebarCollapse } from "@asheeui/web";
function FoldButton() {
const { isCollapsed, collapsible, toggleCollapse } = useSidebarCollapse();
return (
<button
type="button"
disabled={!collapsible}
aria-expanded={!isCollapsed}
aria-label={isCollapsed ? "Expand sidebar" : "Collapse sidebar"}
onClick={toggleCollapse}>
{isCollapsed ? "Expand" : "Collapse"}
</button>
);
}
<Sidebar items={items} title="Ashee CRM" footer={<FoldButton />} />;
A control that lives outside the column — in a header, beside a workspace switcher — cannot read the column's context, and does not need to: lift the state and drive the controlled form.
const [collapsed, setCollapsed] = useState(false);
<button type="button" aria-expanded={!collapsed} onClick={() => setCollapsed(!collapsed)}>
Toggle navigation
</button>
<Sidebar items={items} isCollapsed={collapsed} onCollapseChange={setCollapsed} />;
Every request is refused when collapsible is false, and the hook reports the option so a
control can disable itself instead of offering an affordance that would do nothing.
A collapsed column is the icon column: 4rem at sm, 4.5rem at md and 5rem at lg, against
14, 16 and 18rem when it is expanded. The rows lose their visible labels and badges, the section
labels disappear, the header keeps its back control but loses its title — a header left with
nothing to show is not drawn at all — and the footer slot stays
where it is, so the control that unfolded the column is still there to fold it back. The icons
stay and the current row is still marked. A row's label is not lost to assistive technology when
it is folded away: it is rendered visually hidden, so the row keeps its name even though it shows
only a glyph.
The width transition is a cascade value too: animated (registered true) is what makes folding
animate rather than jump, and components.sidebar.animated turns it off for every column.
footer is drawn whether or not the column can fold and whether or not it is folded: the slot
belongs to the page, and the page's control is usually in it. Its wrapper clips rather than
widening the rail, so a slot written for the expanded column cannot push the column out of its
shell.
A row with no room for its label explains itself instead: tooltip.show (registered true)
puts the label back on hover and on keyboard focus, at tooltip.placement, registered as
right, which is the side a collapsed column has room on. variant and color are not
registered, so the hint falls back to a solid secondary treatment unless the application states
otherwise.
Item and section states
The two states of a row are option groups of their own. options.inactive dresses every row
that is not the current one and options.active dresses the one that is. The registered pair is
a ghost row with no colour of its own, so an idle row takes a quiet wash on hover, and a solid
primary row for the destination the reader is on.
options.radius rounds the rows and resolves from the application's defaultRadius, falling
back to md, so a product that rounds its controls rounds its navigation with them. A row
treatment of underlined resolves that radius to none, which is the same rule the button
follows. options.itemClassName is the escape hatch for anything the tokens do not state.
A section carries disabled as well as a row: marking one draws all of its rows as unavailable,
no destination, aria-disabled, the press refused, and dims the section's label with them, which
is what an entitlement the reader does not have looks like.
The column's own radius rounds its surface, and full is read as xl there: a column cannot
be a circle in a shell that lays it out as a rectangle. The surface itself is variant:
default draws a border on the edge the column sits against, bordered a heavier one,
floating insets it as a rounded panel with a shadow, and ghost keeps the background and
drops the border.
Props
The registered defaults cover the behaviour rather than the dressing: size md,
collapsible true, defaultCollapsed false, animated true,
tooltip.show true with placement right, and the two row states as a ghost inactive row and
a solid primary active one.
variant, radius, options.radius, options.inactive.color, tooltip.variant and
tooltip.color are not registered, so each falls through to the next tier that states it:
radius and options.radius resolve from the application's defaultRadius and round the column
and its rows together, the surface falls back to default, an idle row to no colour of its own,
and the tooltip to a solid secondary treatment.
items, activeKey, onSelect, isCollapsed, onCollapseChange, userRole, link,
anchorProps, footer, navLabel and the four class-name overrides are instance-only, because
the destinations, the current one, and who is reading them are statements about one screen rather
than about the design. isCollapsed and onCollapseChange are how a control outside the column
folds it, and useSidebarCollapse is how one inside it does.
Accessibility
- On its own the column is an
asidewith anavinside it, so it is both a complementary landmark and a navigation landmark:aria-labelon the component names the complementary region andnavLabel(defaultSidebar navigation) names the navigation landmark, so two columns on one page, a docs column beside an on-page one, can be told apart in a landmark list. Inside aSidebarLayoutcolumn it renders adivinstead, because the column already is the complementary landmark, and contributes only the navigation one. - The current row carries
aria-current="page", so the reader's destination is announced rather than only filled. Adisabledrow is drawn without anhrefand witharia-disabled, so the press is refused while the row stays readable; a whole section markeddisabledrenders its rows the same way. - The control the page draws should name what it will do —
Collapse sidebarorExpand sidebar— and carryaria-expanded, so its state is announced rather than inferred from the direction of a chevron. The header's back control is namedGo back. - A collapsed row keeps its name: the label is rendered visually hidden instead of removed, so the icon is not the only thing naming the destination. The tooltip stays a description, it explains the row rather than being what announces it.