gridland
Components

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.

SideNav
?

Run demo

Terminal
bunx @gridland/demo side-nav
Terminal
curl -fsSL https://raw.githubusercontent.com/thoughtfulllc/gridland/main/scripts/run-demo.sh | bash -s side-nav

Installation

Terminal
bunx create-gridland add side-nav

Usage

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.

Basic
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.

Interactive
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

Wide sidebar
<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.

Suffixes
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

No header
<SideNav items={items} showHeader={false}>
  {({ activeItem }) => <Content id={activeItem.id} />}
</SideNav>

Without Status Bar

No 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.

Programmatic navigation
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

KeyAction
/ Navigate sidebar items
EnterSelect item — interact with main panel content
EscapeDeselect — return to sidebar navigation

API Reference

SideNav

PropTypeDefaultDescription
itemsSideNavItem[]-List of navigable items
children(ctx) => ReactNode-Render function for main panel content
sidebarWidthnumber20Width of the sidebar in columns
titlestring-Optional title above the sidebar
showStatusBarbooleantrueShow the keyboard shortcuts status bar
showHeaderbooleantrueShow the active item name as a header in the main panel
requestedActiveIdstring-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

FieldTypeDescription
idstringUnique identifier
namestringDisplay label in the sidebar
suffixstringOptional text appended after the item name

Children Render Context

FieldTypeDescription
activeItemSideNavItemThe currently focused/active item
isInteractingbooleanWhether the user has selected the item (Enter)