---
title: "SidebarNav"
description: "A vertical workspace navigation with search and grouped destinations."
category: Navigation
status: beta
since: 0.3.9
tags: [navigation, sidebar, workspace, search]
syncStatus: reviewed
verified: true
---

## Usage

### Workspace Navigation
`search-hint="/"` makes `/` focus the search field; the tinted highlight follows the pointer.
:::TuffDemoWrapper{demo="SidebarNavSidebarNavDemo" code-lang="vue"}
---
code: |
  <template>
    <TxSidebarNav
      v-model="active"
      v-model:query="query"
      :items="items"
      :groups="groups"
      :workspace="{ name: 'Creamery Ops', description: 'Production Workspace' }"
      search-placeholder="Quick search"
      search-hint="/"
      action-label="New task"
      @select="onSelect"
      @action="onNewTask"
    >
      <template #item-icon="{ item }">
        <svg viewBox="0 0 24 24"><path :d="icons[item.value]" /></svg>
      </template>
    </TxSidebarNav>
  </template>
---
:::

### Best Practices

- Pass icons as inline SVG through the `item-icon` slot; the component sets their size and stroke.
- Past a dozen items, turn on search rather than adding more groups.
- For remote search, pass `filter="items => items"` and swap `items` on `update:query`; otherwise the built-in match filters the results again.
- Make `searchHint` either a single typeable character, which really binds, or a complete glyph such as `⌘K` that you wire to `focusSearch()`. A half-hint like `Ctrl` is neither.
- Reserve badges for counts that need follow-up; decorative numbers dilute the real ones.

## API Reference

### Props

| Prop | Type | Default | Description |
|------|------|---------|------|
| `items` | `SidebarNavItem[]` | - | Navigation items. |
| `groups` | `SidebarNavGroup[]` | - | Group definitions; items matching no group lead the list without a header. |
| `modelValue` | `string \| number` | - | Active item (`v-model`). |
| `query` | `string` | - | Search text (`v-model:query`). |
| `workspace` | `SidebarNavWorkspace` | - | Workspace details; omit to drop the switcher row. |
| `workspaceLabel` | `string` | `'Switch workspace'` | Accessible name of the switcher button. |
| `searchPlaceholder` | `string` | - | Search placeholder; omit to drop the search row. |
| `searchLabel` | `string` | - | Accessible name of the field; falls back to the placeholder. |
| `searchHint` | `string` | - | Shortcut glyph at the end of the search row, such as `/`; a single character also binds that key. |
| `actionLabel` | `string` | - | Primary action label; omit to drop the button. |
| `filter` | `(items, query) => items` | - | Replaces the built-in match; pass `items => items` for remote results. |
| `ariaLabel` | `string` | `'Workspace'` | Accessible name of the `<nav>` landmark. |
| `indicatorDuration` | `number` | `220` | Highlight speed in ms, time-scaling the glide springs; values under 100 play as 100. |

### Events

| Event | Payload | Description |
|------|------|------|
| `update:modelValue` | `SidebarNavValue` | Fires when the active item changes. |
| `update:query` | `string` | Fires when the search text changes. |
| `select` | `SidebarNavItem` | Fires when an item is activated; disabled items don't emit. |
| `action` | - | Fires when the primary action button is pressed. |
| `itemAction` | `SidebarNavItem` | Fires when a row's trailing quick action is pressed. |
| `workspaceClick` | - | Fires when the workspace switcher is pressed. |

### Slots

| Name | Description |
|------|------|
| `workspace` | Replaces the whole workspace switcher row. |
| `item-icon` | Replaces an item's leading icon; scope is `{ item, active }`. |
| `footer` | Appended below the groups. |

### Exposed Methods

| Name | Description |
|------|------|
| `focusSearch()` | Focuses the search field, for hosts wiring their own shortcut. |
| `refreshIndicator()` | Re-measures the highlight after a layout change the observers miss; it lands in place. |

### Types

| Name | Description |
|------|------|
| `SidebarNavItem` | `{ value, label, group?, icon?, badge?, action?, disabled? }`; `icon` is an icon class that the `item-icon` slot overrides, and a changed `badge` replays its pop-in. |
| `SidebarNavGroup` | `{ key, label }`; pass `label` in normal case, CSS uppercases it. |
| `SidebarNavWorkspace` | `{ name, description?, initials? }`; `initials` defaults to the first character of `name`. |

## Overview

- The active row has `aria-current="page"`; disabled items are real `disabled` buttons and emit no `select`.
- The highlight is a pointer, not the selection: it follows hover and keyboard focus and returns to the active row when the pointer leaves the list. Label weight and badges carry the selection.
- The highlight travels only when its target changes; the first measurement and re-measures of the same row land in place, and under reduced motion it jumps without the fade.
- Typing filters live (case-insensitive `includes` on `label`), and a group left empty collapses with its header.
- A single-character `searchHint` binds a `document` keydown that focuses the search field; a multi-character one such as `⌘K` renders only as a badge. The binding stands down when focus is in a text field, Meta / Ctrl / Alt is held, the event was already handled, or there is no search row.
- The trailing quick action is a sibling `<button>` of the row button and stays visible on touch (`hover: none`); group headers label their lists through `aria-labelledby`.

## Technologies

- `useIndicatorBox` measures the highlight and the glide material of `useJellyIndicator` moves it. `useIndicatorBox` is exported with the component (`import { useIndicatorBox } from '@talex-touch/tuffex'`) and returns `top / left / width / height` for reuse in horizontal segmented controls.
- The shortcut listener lives on `document`; enable `enableAutoUnmount(afterEach)` in tests, or a wrapper left mounted keeps consuming keys.
- Source: `packages/tuffex/packages/components/src/sidebar-nav/`. Adapted from [Beautiful UI](https://www.beautifului.dev), © 2026 Shane Levine, MIT.

<TuffDocSourceLink />
