The id of the currently active item. Items matching this id get the
active styling and aria-current="page".
Whether the rail is currently collapsed to icons. Two-way bindable via [(Collapsed)].
Emitted when the user clicks the collapse toggle. Consumer updates + persists the state.
Width of the collapsed icons-only strip. The default 60 is derived from the rail's own numbers — 8+8 container padding, 12+12 item padding, 18px icon, 1px border — so icons stay centred; retune it if those change.
Opt-in desktop collapse: renders a compact double-angle toggle chip at the top of the rail and, when Collapsed, presents the rail as an icons-only strip (CollapsedWidth wide) — labels visually hidden but kept in the a11y tree, section labels folded to divider lines, badges docked on the icon corner, and per-item tooltips/aria auto-enabled.
The component owns the PRESENTATION only — the consumer owns the STATE (bind [(Collapsed)]
and persist it however the app persists preferences), the same split as ExpandedIds.
Desktop-only: ≤700px the rail is the off-canvas drawer and the toggle hides.
Two things to know when the rail's content is richer than a flat icon list:
icon render a monogram so they stay visible and hittable.[header] / [footer] slots are yours. The rail can't restyle projected content,
so a wide header (logo lockup + wordmark, a filter box) will overflow a CollapsedWidth
strip. Bind the same collapsed flag in the consumer and project a compact variant.Deliberately NOT included: hover-to-peek (auto-expand on pointer hover) — removed by ruling (2026-07-30): a rail that expands when the pointer grazes it moves the target the user was aiming at. Deliberate toggling beats clever hovering.
Ids of currently-expanded tree items. Items whose id appears here render
with a down-chevron and their children are visible; items not in this
list render with a right-chevron and their children stay collapsed.
Consumer owns the state — toggle in response to (ItemToggled).
Only applies to items where children !== undefined; flat items ignore it.
Set by a consumer that has visually collapsed the rail to an icons-only strip (via Width plus
its own CSS hiding the labels). When true, each item gains a native title tooltip AND an
aria-label carrying its full text.
Why it is opt-in rather than always-on:
.mj-left-nav__text, the button's
accessible name goes EMPTY — a screen reader announces "button" with no destination. That is
the hole this closes.Leaving it false reproduces the previous markup exactly, so existing consumers are unaffected.
Emitted when a non-disabled item is clicked.
Emitted when a tree item's chevron is clicked. Consumer should toggle
the item's id in / out of ExpandedIds. Does NOT also fire ItemClicked.
Whether the mobile off-canvas drawer is open. Component-owned, transient UI state — no effect above the 700px breakpoint where the rail is always inline.
Label shown on the mobile drawer header and used as the switcher fallback
label when no item is active. Defaults to 'Menu'. Set to something
domain-specific (e.g. 'Sections', 'Test Suites') for clearer context.
ReadonlyNavDOM id of the <aside>, so the collapse toggle's aria-expanded can name the region it
expands via aria-controls. Per-instance because a page may host more than one rail.
The sections + items that make up the rail. Sections render top-to-bottom in array order; items inside each section render in array order.
Width of the rail in pixels. Defaults to 240. Set to a smaller value (e.g., 200) for denser shells. Only applies to the desktop rail — on mobile the rail becomes a fixed-width off-canvas drawer.
The item matching ActiveId (searched recursively through trees),
used to label the mobile section switcher with the current section's icon
and label. Returns undefined when nothing is active.
Left margin that pins the toggle chip at the collapsed band's centre in BOTH modes, so the chip never relocates on toggle (the user never re-aims). Derived, not magic: (CollapsedWidth − 38px chip)/2 from the rail edge, minus the rail's 8px container padding. 38 = the 18px icon box + 2×10px chip padding.
Icon-only presentation is on when the consumer forces it OR the rail is natively collapsed.
IconOnly is deliberately NOT viewport-gated: that flag means the CONSUMER's own CSS has
narrowed the rail, and we can't know which breakpoints they gated it to. Withdrawing the
accessible names on their behalf could leave a genuinely icons-only rail unnamed — the exact
hole IconOnly exists to close. Native collapse is ours to gate; theirs is theirs.
The width the rail actually renders at, honoring the collapse state.
Reflects the collapsed state onto the host so the collapsed CSS can scope to it.
Reflects MobileNavOpen onto the host so CSS can drive the drawer.
Whether the rail is collapsed and in a viewport where collapse means anything. Below 701px
the rail is the off-canvas drawer — already dismissable, already showing full labels — so a
persisted Collapsed = true must not follow the user down there.
Whether tree structure (chevrons, depth indentation, expanded children) renders at all.
An icons-only rail is ~60px wide and shows nothing but a centred glyph per row, which leaves a tree with no way to express itself: the chevron would eat a third of the width and shove the icon off-centre, each depth level would push its icon 14px further right until the rail clipped it, and a child's icon (optional, and usually absent on tree leaves) would say nothing about which branch it belongs to. So while icons-only, each section renders its TOP-LEVEL items only — the same choice every icon rail makes (VS Code's activity bar shows one icon per container and defers the tree to the expanded pane).
Nothing is lost on the way back: ExpandedIds is consumer-owned and untouched here, so the tree
returns exactly as the user left it the moment the rail expands.
'page' for the active item itself; 'true' for a stand-in ancestor, which is current in the
set but is not the page the user is on. Distinguishing them keeps the announcement honest.
Whether any top-level item in this section actually has children to expand. The template calls this once per section; the result is threaded through the recursive itemTpl so leaf nodes only reserve a chevron-column placeholder when a sibling will actually render a chevron.
Top-level check is sufficient — if a deeply-nested item has children, its
parent's children.length > 0 already qualifies the section as expandable.
Whether an item paints as active. Normally an exact ActiveId match — but while the tree is suppressed (see ShowTreeStructure) the active item may be a child that isn't rendered, which would leave the collapsed rail with NOTHING highlighted and no sense of place. In that state a top-level item stands in for its active descendant.
Monogram for an icon-less item in an icons-only rail. Array.from rather than charAt so a
label starting with an emoji or an astral character yields that whole character instead of half
a surrogate pair. Falls back to a bullet for a blank label so the button is never empty.
The text surfaced as an icon-only item's tooltip / accessible name. Folds in the description and badge because collapsing the rail hides both: the badge survives as an unlabeled corner count, so "Batch approvals (2)" is the only place a hovering user learns what the 2 counts.
A callback method that performs custom clean-up, invoked immediately before a directive, pipe, or service instance is destroyed.
Escape closes the mobile drawer. No-op when it's already closed.
Flip + announce. The consumer receives the new value and persists it.
<mj-left-nav>— Canonical left rail for MJ Explorer dashboards that have an internal section-nav (Admin shells, KH Config, KH Analytics, AI Analytics, Communication, Credentials, APIKeys, etc.).Replaces the bespoke
.{name}-nav/.{name}-nav-itempatterns each of those dashboards used to declare in their own CSS files.Supports:
[header]and[footer]content slots (KH Config logo header, etc.)[MobileTitle]for the drawer label.Example
Responsive — at ≤700px the rail becomes an off-canvas drawer (see the feature list above). The consumer should still switch the parent's flex-direction from row → column at the same breakpoint (e.g.
<mj-page-body>Direction="row" with a@mediaoverride) so the in-flow section switcher stacks above the content pane rather than competing with it for horizontal space.