Tenant picker
A workspace switcher for the Sidebar header, with a mark that stays when the rail collapses and four crafts for the panel.
Tenant picker switches between the workspaces, organizations, or accounts a person belongs to, from the header of a Sidebar. Its trigger keeps the tenant's mark when the sidebar collapses to icons and hides the name, and the panel opens to the right of a collapsed rail and below an expanded one. It holds the choice and nothing else, so loading tenants and switching the app's data stay in your code.
Deck — ghosted current tenant, other marks beside it. Open to deal the rest. Now in Ramson.
The preview's stage has a SidebarTrigger, so close the rail and open the picker from the
mark. Switch between the four crafts to compare them.
Usage
import {
Sidebar,
SidebarHeader,
SidebarInset,
SidebarTrigger,
} from '@/components/ui/sidebar';
import { TenantPicker } from '@/components/ui/tenant-picker';
<Sidebar collapsible="icon">
<SidebarHeader>
<TenantPicker
variant="deck"
tenants={[
{ id: 'ramson', name: 'Ramson', mark: 'R', tone: 'primary', hint: 'Studio · US-East' },
{ id: 'northwind', name: 'Northwind', mark: 'N', tone: 'support', hint: 'Trading · EU-West' },
{ id: 'paperplain', name: 'Paperplain', mark: 'P', tone: 'inverse', hint: 'Press · Tokyo' },
]}
value={tenant}
onValueChange={setTenant}
/>
</SidebarHeader>
</Sidebar>
<SidebarInset>
<SidebarTrigger />
</SidebarInset>Slot it into App shell as the header of the sidebar
prop. The picker reads the Sidebar's collapsed state from context, so it needs to sit inside a
SidebarProvider to follow the collapse.
Four crafts
The crafts share the same tenants and the same selection, and each is its own object and not a
restyled menu. Set one with variant, and the default is deck.
| Variant | Object | Feel |
|---|---|---|
deck | The current tenant, with the other marks fanned beside it | Sits flush in the header. Open it and the panel deals every tenant as a card, the current one first and marked "now". |
seals | A row of round letter seals | Fast, with a radio for each seal. Hover or focus previews the name and hint, and the current seal lifts. |
tickets | Perforated stubs in a sunken well | Each row shows the mark, name, hint, and a stub number. The current ticket is raised paper. |
index | A card-catalog drawer | A search field above display-letter cards. Type to filter by name or hint. |
Picking a tenant calls onValueChange with its id and closes the panel. In seals the
arrow keys move a pending choice, which the preview names, without switching. Enter, Space, or
a click switches to it and closes the panel, and Escape leaves the workspace as it was.
Tenants and marks
Each tenant is an object with an id and a name, and three optional fields. mark is one
or two letters and falls back to the first character of the name. tone colors the mark with
a token role, primary (the default), support, or inverse, so a mark never needs a swatch
of its own. hint is a second line on the deck cards, tickets, seals preview, and index cards.
The stub number on tickets and deck cards is the mark in capitals and the tenant's position in
the list, like N–02.
The value is the selected tenant's id. Use value and onValueChange to control it, or
defaultValue to start somewhere, and it starts on the first tenant when neither is set. An
unknown id also falls back to the first tenant, and an empty tenants list renders nothing.
Collapsing to the mark
Inside a collapsed collapsible="icon" Sidebar the trigger shrinks to a 32px square showing
only the mark. The name and caret are hidden, but the button keeps its accessible name, so the
mark alone still says which workspace is current. In deck the other tenants' marks fan out
behind it, and they spread a little more on hover and while open. The panel moves to the
right of the rail while collapsed.
Every craft's trigger carries data-slot="tenant-picker-trigger", so a header can size it from
outside, for example [&_[data-slot=tenant-picker-trigger]]:min-h-14 to fill a taller band.
collapsed forces the panel to open to the right and sets data-collapsed on the wrapper. It
does not restyle the trigger, which follows the Sidebar's collapse through context, so use it
only to choose which side the panel opens on. Inside a Sidebar on a phone the panel opens below.
Props
| Prop | Type | Default | What it does |
|---|---|---|---|
tenants | Tenant[] | The workspaces to choose from, each with id, name, and optional mark, tone, and hint. | |
value | string | The selected tenant's id, controlled. | |
defaultValue | string | The first tenant | The id to start on when uncontrolled. |
onValueChange | (id: string) => void | Called with the id when a tenant is picked. | |
variant | "deck" | "seals" | "tickets" | "index" | "deck" | The craft of the trigger and panel. |
collapsed | boolean | Follows the Sidebar | Opens the panel to the right of the trigger. Does not restyle the trigger. |
className | string | Added to the wrapper. |
TenantMark and TenantPickerTrigger are also exported, for a header that draws its own
trigger.
Accessibility
- The trigger is a
<button>named "Workspace: " and the current tenant's name, and the deck adds "Open the deck." It opens a Popover, so it reportsaria-expandedand the panel is a dialog. Focus moves into the panel when it opens, Escape closes it, and focus returns to the trigger. - The mark is decorative and
aria-hidden, which is why the name has to come from the label. That keeps the collapsed trigger named when only the mark is showing. - In
deckandticketseach tenant is a button, and the current one hasaria-current="true". Tab moves between them and Enter or Space picks one. The stub number and "now" are part of the button's text, so a screen reader reads them with the name and hint. - In
sealsthe panel is afieldsetlabelled "Workspaces" holding a radio for each tenant, named by the tenant, so arrow keys move between them and a screen reader announces the group. The checked radio is the pending choice, and nothing switches until Enter, Space, or a click. - In
indexthe list is a Command with a search field named "Find a workspace" and arrow keys, and typing filters the cards by name and hint. The current tenant hasaria-current="true"as well as its raised background. - On a touch screen the trigger has an invisible 44px hit area. With a fine pointer it is the size it appears.
- In forced colors the trigger gains a
ButtonBorderborder. Under reduced motion the deck deal, the fanning marks, and the lift on hover do not animate.
When to use something else
- Select when the choice is a value in a form, with a label and a field around it. Tenant picker is app chrome and not a field.
- Combobox when a field needs to search a long list or hold
several values. The
indexcraft searches too, but it picks one tenant. - Dropdown menu when the rows are actions and not a choice of where you are working, like Settings or Sign out.
- A plain Sidebar header with one static workspace needs no picker.
Install
shadcn add @gesso/tenant-picker