Tab bar
ProA phone's bottom tab bar: up to five main places, each an icon above its word, the current one marked as the current page. Give it to AppLayout's tabBar, which draws it as the frame's last row below tabBarAt (640 px of frame by default) in place of the menu button; above that, the sidebar does the job and the bar isn't drawn.
Each destination is a link when it has an href (a plain click goes through onNavigate when you give one, for your router) and a button otherwise. The app's link render draws the link when one is set (LinkProvider, CandorProvider's link), else a plain <a>. Every target is at least 44 px tall, and the bar is padded for the phone's home indicator (env(safe-area-inset-bottom)). A badge is read in the destination's name ("Inbox, 3 new"), not colour alone. more adds a More button that opens AppLayout's sidebar sheet, for the places beyond the bar; it is drawn only inside an AppLayout. A sixth destination is a warning in development: put it behind More.
Version 5.2.0· other versions
This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).
Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 3.0.0, 4.0.0, 4.0.1, 4.0.2, 4.1.0 and 4.2.0: the page came later, or had another name then.
Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.
Preview
Loading the preview
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/tab-bar
Brings in: app-layout, link, locale, tokens, utils · npm: lucide-react@^1.67.0
Usage
"use client"
import * as React from "react"
import { Bot, Inbox, MessagesSquare, Settings, Workflow } from "lucide-react"
import { AppLayout } from "@/components/candor/app-layout"
import { AppSidebar } from "@/components/candor/app-sidebar"
import { TabBar, type TabBarDestination } from "@/components/candor/tab-bar"
const destinations: TabBarDestination[] = [
{ id: "threads", label: "Threads", icon: MessagesSquare },
// A badge only when the service gave a count; it's read in the name: "Inbox, 2 new".
{ id: "inbox", label: "Inbox", icon: Inbox, badge: 2 },
{ id: "agents", label: "Agents", icon: Bot },
]
export default function Example() {
const [place, setPlace] = React.useState("threads")
return (
// A phone-sized frame: below tabBarAt (640 by default) the tab bar is the last row and More opens the sidebar.
<div className="h-[480px] w-full max-w-[360px] overflow-hidden rounded-lg border">
<AppLayout
sidebar={
<AppSidebar
brand={<span className="px-1 text-sm font-semibold">Keel</span>}
nav={[
...destinations.map(({ id, label, icon }) => ({ id, label, Icon: icon })),
{ id: "flows", label: "Flows", Icon: Workflow },
{ id: "settings", label: "Settings", Icon: Settings },
]}
activeId={place}
onNavigate={setPlace}
user={{ name: "Sam", email: "sam@acme.example" }}
/>
}
tabBar={<TabBar destinations={destinations} activeId={place} onNavigate={setPlace} more />}
>
<header className="flex h-11 shrink-0 items-center border-b px-4">
<h2 className="text-sm font-semibold capitalize">{place}</h2>
</header>
<div className="flex-1 overflow-y-auto p-4 text-sm text-muted-foreground">Flows and Settings are behind More, in the sidebar.</div>
</AppLayout>
</div>
)
}<TabBar>
| Prop | Type | Default | Description |
|---|---|---|---|
| destinationsrequired | TabBarDestination[] | — | The places, in order; up to five. |
| activeId | string | undefined | — | The id of the place the person is in, marked as the current page, when you control it. |
| defaultActiveId | string | undefined | — | The place to start in, when the bar keeps it itself: with this and no `activeId`, a destination chosen in this page (not opened in a new tab) becomes the current one, and `onNavigate` is still called. Without either, none is marked. |
| onNavigate | ((id: string) => void) | undefined | — | Called with a destination's id when it's chosen. For one with an `href`, a plain click calls this instead of following the link (for an app's own router); other clicks open the link as usual. |
| label | string | undefined | — | The bar's accessible name. Defaults to "Main". |
| more | boolean | undefined | false | Adds a More button that opens AppLayout's sidebar sheet, for places beyond the bar. Defaults to false. |
| className | string | undefined | — | Classes for the bar, merged with Candor's. |
Strings
The fixed words, in English by default. Change any of them under tabBar in <LocaleProvider strings={{ … }}>; the table is exported as tabBarStrings.
| Key | English |
|---|---|
| main | Main |
| more | More |
| withBadge | {label}, {count} new |
Motion
| Moment | Family | What it does |
|---|---|---|
| tint | tint | A destination's word and icon darken when the pointer is over it. |
Set with --cd-motion-tab-bar-<moment>-* in your brand file; the families are in the design reference.
Types
- TabBarDestination
- One place in the tab bar.
{ /** Passed to `onNavigate`, and compared with `activeId`. */ id: string; /** The words under the icon; always shown, never icon-only. */ label: string; /** The icon above the label. */ icon: LucideIcon; /** Where it goes. With an `href` the destination is a link (it opens in a new tab, its address can be copied); without one, a button. */ href?: string | undefined; /** How many new things are there, when the service says; drawn as a count and read in the name ("Inbox, 3 new"). 0 or null draws none. */ badge?: number | null | undefined; /** * The destination's name while it has a badge, in place of "{label}, {count} new", when "new" would say something * false ("Approvals, 3 waiting": NavRail's `navRailTabs` sets it). Start it with `label`, so the name starts with the * visible words. Without it, the name is 4.3's. */ badgeLabel?: string | undefined; }