AppWorkspaceSwitcher
The identity control that shows which workspace you are in and lets you move to another.
<app-workspace-switcher current-id="acme">…</app-workspace-switcher><app-workspace-switcher> is the tenant control that sits at the top of a sidebar or in a topbar: it answers “which workspace am I looking at?” at a glance, and opens a menu to move to another one. It is the anchor of a multi-tenant portal — without it, a user with two workspaces has no way to tell them apart or move between them.
The list is composed from <app-workspace-option> children. A row with an href navigates; a row without one renders as a button carrying data-workspace-id for the page's own handler — the same “the author owns the action” shape as <app-dropdown>, which React expresses as the onSelect callback.
Playground
Every meaningful attribute, live. Each change re-renders the real tag helper on the server, so the markup underneath is the exact call that produced the switcher above it.
compact is on — constrain the container, not the component.The switcher
A card trigger showing the current workspace, and a menu of the ones the user can reach. The active row is tinted and ticked, so the current workspace is obvious even mid-scroll.
Show code
<app-workspace-switcher current-id="acme">
<app-workspace-option value="acme" name="Acme Corp" badge="Owner" />
<app-workspace-option value="globex" name="Globex Ltd" logo-src="/globex.png" href="/switch/globex" />
<app-workspace-option value="initech" name="Initech" href="/switch/initech" />
</app-workspace-switcher>
Monogram or logo
Each workspace shows a mark: its logo when logo-src is set, otherwise a monogram built from the first and last initials of the name. Both draw in the same box, so a workspace gaining a logo never reflows the row.
Show code
<app-workspace-switcher current-id="globex">
<app-workspace-option value="acme" name="Acme Corp" href="/switch/acme" /> @* monogram → AC *@
<app-workspace-option value="globex" name="Globex Ltd" logo-src="/globex.png" />
</app-workspace-switcher>
AC, “Wayne Heavy Industries” draws WI (first and last, middle words ignored), and a one-word “Initech” draws I. The React component computes the same letters, so a workspace looks identical on both surfaces.Badges
A workspace can carry a short badge — the user's role in it, or its plan. It sits at the end of the row, before the tick.
Show code
<app-workspace-switcher current-id="acme" menu-label="Workspaces">
<app-workspace-option value="acme" name="Acme Corp" badge="Owner" />
<app-workspace-option value="globex" name="Globex Ltd" badge="Trial" href="/switch/globex" />
</app-workspace-switcher>
Compact
compact renders a 36px monogram button instead of the full card — the shape a collapsed sidebar rail wants. The menu is unchanged, and the accessible name still reads the full workspace name, so the button is never just two letters to a screen reader.
Show code
<app-workspace-switcher current-id="acme" compact="true">
<app-workspace-option value="acme" name="Acme Corp" badge="Owner" />
</app-workspace-switcher>
Sizes
Three sizes. Default (md) suits almost everything; use sm inside a dense topbar cell or filter bar, lg for a roomy sidebar header. The dropdown menu is a fixed 280px panel at every size — only the trigger scales.
Show code
<app-workspace-switcher current-id="acme" size="sm" menu-label="Workspaces">…</app-workspace-switcher> <app-workspace-switcher current-id="acme" menu-label="Workspaces">… @* md (default) *@</app-workspace-switcher> <app-workspace-switcher current-id="acme" size="lg" menu-label="Workspaces">…</app-workspace-switcher>
size with compact and the square scales too (32 / 36 / 44px).Searchable
Past roughly eight workspaces a list stops being scannable. searchable adds a filter box above it that narrows the rows as the user types (wired by webority-ui.js, the same behavior React holds in component state).
Show code
<app-workspace-switcher current-id="acme" searchable="true" menu-label="Workspaces">
<app-workspace-option value="acme" name="Acme Corp" badge="Owner" />
<app-workspace-option value="globex" name="Globex Ltd" href="/switch/globex" />
…
</app-workspace-switcher>
searchable off until the list is long.The create row
can-create pins a “create workspace” row under the list. With create-href it navigates; without one it renders a button carrying data-workspace-create for the page's own handler (React fires onCreate). It is off by default because most members cannot create a workspace.
Show code
<app-workspace-switcher current-id="acme" can-create="true" create-href="/workspaces/new">
<app-workspace-option value="acme" name="Acme Corp" badge="Owner" />
<app-workspace-footer>
<a class="workspace-switcher-footer-action" href="/settings">
<app-icon name="settings" size="16"></app-icon> Workspace settings
</a>
</app-workspace-footer>
</app-workspace-switcher>
can-create from the same permission check the create endpoint enforces. A visible row the server rejects is worse than no row.When to use
Use it when
- A user can belong to more than one workspace, tenant, organisation, or company.
- The current workspace must stay visible so a user never acts in the wrong one.
- The sidebar or topbar needs one place to switch tenant and reach workspace settings.
Reach for something else when
- You are switching the signed-in user, not the workspace. → AppDropdown
- You are picking a value into a form field. → AppSelect
- You need a flat list of actions behind a “⋯” trigger. → AppDropdown
- You are navigating within one workspace. → AppShell
Best practices
Every option
The whole surface of the component, one cell per value — every mark shape, every footer flavour, and the degenerate lists an ordinary example never shows.
compact — off vs on
(default) — full card
compact="true" — 36px monogram button
searchable — off vs on
(default) — no filter box
searchable="true" — filter box above the list
can-create — off vs on
(default) — no create row
can-create="true" — default label
can-create="true" create-label="New tenant"
mark — logo vs monogram
logo-src set — image mark
name="Acme Corp" — monogram "AC" (first + last word)
name="Initech" — one-word monogram "I"
badge — present vs absent
badge="Owner" — trailing pill on the row
(no badge) — row without a pill
menu-label — off vs on
(none)
menu-label="Workspaces"
footerAction / app-workspace-footer — node vs. descriptor vs. none (React) / none vs a rendered row (Razor)
(no <app-workspace-footer>) — footer omitted entirely
plain markup — any content, rendered as-is
<a href> — a navigating row (React's descriptor + href)
footerAction descriptor routes an href through a caller-supplied linkComponent (a router Link). Razor's <app-workspace-footer> is plain markup you write yourself, so the equivalent is simply writing whatever anchor tag helper your router uses inside the slot.degenerate lists
(no <app-workspace-option> children) — empty trigger, empty note
one <app-workspace-option> — solo row plus footer
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| (content) | markup | — | The workspaces — one <app-workspace-option> each, plus an optional <app-workspace-footer> slot. |
| current-id | string | first option | Id of the active workspace — drives the trigger and the tick. |
| menu-label | string | — | Optional header label above the list. |
| searchable | bool | false | Client-side filter over the list. Only earns its place past roughly eight workspaces. |
| can-create | bool | false | Renders the create row. Off by default because most members cannot create a workspace. |
| create-label | string | Create workspace | Copy for the create row. |
| create-href | string | — | Where the create row navigates. Without it the row is a button carrying data-workspace-create (React's onCreate). |
| compact | bool | false | Renders the 36px monogram button instead of the full card. Same menu either way. |
| size | sm | md | lg | md | Trigger size tier — scales the card (and the compact square), its mark and name. The menu is unchanged. |
| aria-label | string | Workspace <name> | Accessible name for the trigger. |
<app-workspace-option> takes value (the id matched against current-id), name (the label and the source of the monogram), and optional logo-src, badge and href. React expresses the same list as the tenants array of { id, name, logoUrl?, badge? }.