SideNav
A sidebar navigation layout with keyboard-driven focus and a main content panel
A sidebar + main panel layout with built-in keyboard navigation. Arrow keys navigate the sidebar, Enter selects an item for interaction, Escape goes back.
Run demo
bunx @gridland/demo side-navcurl -fsSL https://raw.githubusercontent.com/thoughtfulllc/gridland/main/scripts/run-demo.sh | bash -s side-navInstallation
bunx create-gridland add side-navUsage
import { SideNav } from "@/components/ui/side-nav"const items = [
{ id: "inbox", name: "Inbox" },
{ id: "drafts", name: "Drafts" },
{ id: "sent", name: "Sent" },
]
<SideNav items={items} title="Mail">
{({ activeItem, isInteracting }) => (
<YourContent item={activeItem} />
)}
</SideNav>Examples
Basic
A simple sidebar navigating between static content panels.
const items = [
{ id: "files", name: "Files" },
{ id: "search", name: "Search" },
{ id: "settings", name: "Settings" },
]
<SideNav items={items}>
{({ activeItem }) => (
<box padding={1}>
<text>Viewing: {activeItem.name}</text>
</box>
)}
</SideNav>Interactive Content
When the user presses Enter on a sidebar item, SideNav wraps the panel in a
<FocusScope trap selectable autoFocus autoSelect>. Panel content just needs
to use its own focus-aware components (TextInput, SelectInput, etc. with
focusId) — the scope trap keeps focus contained until Esc exits.
function MyPanel() {
const [value, setValue] = useState("")
return (
<box padding={1}>
<TextInput
focusId="panel-name"
label="Name"
placeholder="type here..."
value={value}
onChange={setValue}
/>
</box>
)
}
<SideNav items={items} title="Settings">
{({ activeItem, isInteracting }) => (
<MyPanel />
)}
</SideNav>Custom Sidebar Width
<SideNav items={items} sidebarWidth={30} title="Dashboard">
{({ activeItem }) => <Content id={activeItem.id} />}
</SideNav>Item Suffixes
Use suffix on items to show additional context like counts or badges.
const items = [
{ id: "inbox", name: "Inbox", suffix: "(3)" },
{ id: "drafts", name: "Drafts", suffix: "(1)" },
{ id: "sent", name: "Sent" },
]
<SideNav items={items} title="Mail">
{({ activeItem }) => <Content id={activeItem.id} />}
</SideNav>Without Header
<SideNav items={items} showHeader={false}>
{({ activeItem }) => <Content id={activeItem.id} />}
</SideNav>Without Status Bar
<SideNav items={items} showStatusBar={false}>
{({ activeItem }) => <Content id={activeItem.id} />}
</SideNav>Programmatic Navigation
Use requestedActiveId to switch the active item from outside the component.
const [activeId, setActiveId] = useState<string>()
<button onClick={() => setActiveId("settings")}>Go to Settings</button>
<SideNav items={items} requestedActiveId={activeId}>
{({ activeItem }) => <Content id={activeItem.id} />}
</SideNav>Controls
| Key | Action |
|---|---|
↑ / ↓ | Navigate sidebar items |
Enter | Select item — interact with main panel content |
Escape | Deselect — return to sidebar navigation |
API Reference
SideNav
| Prop | Type | Default | Description |
|---|---|---|---|
items | SideNavItem[] | - | List of navigable items |
children | (ctx) => ReactNode | - | Render function for main panel content |
sidebarWidth | number | 20 | Width of the sidebar in columns |
title | string | - | Optional title above the sidebar |
showStatusBar | boolean | true | Show the keyboard shortcuts status bar |
showHeader | boolean | true | Show the active item name as a header in the main panel |
requestedActiveId | string | - | Programmatically switch active item by ID |
onActiveItemChange | (item: SideNavItem) => void | - | Called when the active item changes |
Colors are derived from the theme (useTheme()): focus indicators use theme.focusSelected/focusFocused/focusIdle, structural borders use theme.borderMuted, title and header use theme.primary, and idle text uses theme.muted. Customize by providing a different theme via ThemeProvider.
SideNavItem
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier |
name | string | Display label in the sidebar |
suffix | string | Optional text appended after the item name |
Children Render Context
| Field | Type | Description |
|---|---|---|
activeItem | SideNavItem | The currently focused/active item |
isInteracting | boolean | Whether the user has selected the item (Enter) |