DocsnavigationSidebar

Sidebar

A sidebar of sections whose single active marker arcs between rows and crossfades out of one section's ink into the next, as a static rail or as the same panel in a drawer.

Marker

Pick a row and the marker arcs to it, arriving in the colour of the section it lands in — jump between sections and it crossfades on the way, and the label is shoved aside as it arrives. The drawer is the same panel again, over the page.

Installation

npx shadcn@latest add fujiDevv/calamansi-ui/sidebar

How to use

demo.tsx
import { Sidebar } from "@/components/ui/sidebar"
export function Demo() {
const [open, setOpen] = useState(false)
return (
<>
{/* beside the content, from md up */}
<Sidebar
sections={SECTIONS}
activeHref={pathname}
marker="pip"
className="sticky top-20 hidden h-[calc(100vh-5rem)] w-60 md:flex"
/>
{/* the same panel, over it, on a phone */}
<Sidebar
variant="drawer"
sections={SECTIONS}
activeHref={pathname}
open={open}
onOpenChange={setOpen}
onNavigate={() => setOpen(false)}
/>
</>
)
}

Props

Options you can pass to customize this component.

  • sectionsoptional
    SidebarSection[]

    `{ label, color, items }` — an eyebrow, the section's ink, and its rows. A row is a label, or `{ label, href, icon, badge, disabled }`; one with an href renders as a link, one without as a button.

  • activeHrefoptional
    string

    The row to mark, matched against an item's href — the route-driven case. A route the nav does not contain leaves the marker off rather than parked on the wrong row.

  • valueoptional
    number

    Active row as a flat index, for controlled use.

  • defaultValueoptional
    number

    Active row as a flat index on mount. Ignored once `activeHref` or `value` is given.

  • onChangeoptional
    (index: number, item: SidebarItem) => void

    Fired with the flat index and the row that was picked.

  • onNavigateoptional
    (item: SidebarItem) => void

    Fired on every selection, whatever drives the active row. This is where a drawer gets closed.

  • headeroptional
    ReactNode

    Sits above the list and does not scroll — a wordmark, a project switcher.

  • footeroptional
    ReactNode

    Pinned below the list and does not scroll — an account row, a version.

  • variantoptional
    raildrawer

    The static panel, or the sliding overlay. Both draw the same list, so they can be mounted side by side.

  • openoptional
    boolean

    Whether the drawer is showing. Drawers only.

  • onOpenChangeoptional
    (open: boolean) => void

    Fired when the drawer asks to close — Escape, the scrim, the close button. The page behind it is locked and unlocked for you.

  • navLabeloptional
    string

    The nav's accessible name, and the drawer's dialog name.

  • markeroptional
    dotpipbarglow

    Shape of the active marker: a plain dot, the citrus-seed pip, a slim bar, or a dot carrying its own glow.

  • markerColoroptional
    string

    Marker colour, and the fallback for any section that does not bring its own.

  • fadeoptional
    boolean

    Fade a list that overflows at both edges, with the 3rem of padding the stops are matched to — at rest the rows sit inside the opaque zone. The padding comes and goes with the fade, so turning it off leaves no dead space.

  • classNameoptional
    string

    The panel's own box — width, position and borders. The rail does not position itself, so sticking it is your call: `sticky top-20 h-[calc(100vh-5rem)] w-60`.

Dependencies

motionlucide-react

Contact

Found a bug or issue? Open an issue or send a note.

License & Usage

  • Free to use and modify in personal and commercial projects.
  • Attribution to Calamansi UI is appreciated when using a component.
  • Please do not resell the components as your own kit.