Gesso
Navigation

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.

Ramson · Inbox
Close the sidebar, then open the picker from the mark.

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.

VariantObjectFeel
deckThe current tenant, with the other marks fanned beside itSits flush in the header. Open it and the panel deals every tenant as a card, the current one first and marked "now".
sealsA row of round letter sealsFast, with a radio for each seal. Hover or focus previews the name and hint, and the current seal lifts.
ticketsPerforated stubs in a sunken wellEach row shows the mark, name, hint, and a stub number. The current ticket is raised paper.
indexA card-catalog drawerA 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

PropTypeDefaultWhat it does
tenantsTenant[]The workspaces to choose from, each with id, name, and optional mark, tone, and hint.
valuestringThe selected tenant's id, controlled.
defaultValuestringThe first tenantThe id to start on when uncontrolled.
onValueChange(id: string) => voidCalled with the id when a tenant is picked.
variant"deck" | "seals" | "tickets" | "index""deck"The craft of the trigger and panel.
collapsedbooleanFollows the SidebarOpens the panel to the right of the trigger. Does not restyle the trigger.
classNamestringAdded 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 reports aria-expanded and 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 deck and tickets each tenant is a button, and the current one has aria-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 seals the panel is a fieldset labelled "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 index the 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 has aria-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 ButtonBorder border. 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 index craft 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

On this page