Layout & navigation

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.

<app-workspace-switcher current-id="acme">
    <app-workspace-option value="acme" name="Acme Corp" badge="Owner" href="/switch/acme" />
    <app-workspace-option value="globex" name="Globex Ltd" href="/switch/globex" />
    <app-workspace-option value="initech" name="Initech" href="/switch/initech" />
</app-workspace-switcher>
compact is a shape switch, not a resizeThe trigger is a full-width card unless compact is on — constrain the container, not the component.
Menu open motion product-wide via data-wui-menu-motion — pick one, then open the menus below

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.

Current workspace plus a menu
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>
The switcher fills its slotThe trigger is a full-width card, so it takes the width of whatever it is dropped into — a sidebar rail, a topbar cell. Constrain the container, not the component.

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.

A logo replaces the monogram
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>
Two letters, not one“Acme Corp” draws 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.

A role badge on the owned workspace
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>
Keep a badge to one wordThe row already holds a mark, a name and a tick. “Owner”, “Trial”, “Pro” fit; a sentence truncates the name it is meant to qualify.

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.

The collapsed-rail trigger
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.

sm · md · lg
Small
Medium
Large
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>
Sizes scale the trigger, not the menuA larger size grows the card and its monogram, but the workspace list stays the same 280px panel. Pair 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).

A filter over a long list
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>
Don't add it to a short listWith three workspaces the filter is a control the user has to skip past on the way to the thing they can already see. Leave 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.

Create plus a pinned footer action
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>
Gate it on permission, not on role copySet 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

Do
The current workspace is named on the trigger, ticked in the list, and settings are one row away.
Don't
A compact trigger in a full-width sidebar hides the workspace name — the one thing the control exists to show. Save it for a collapsed rail.

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

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)
Razor has no linkComponentReact's 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

AttributeTypeDefaultDescription
(content)markup—The workspaces — one <app-workspace-option> each, plus an optional <app-workspace-footer> slot.
current-idstringfirst optionId of the active workspace — drives the trigger and the tick.
menu-labelstring—Optional header label above the list.
searchableboolfalseClient-side filter over the list. Only earns its place past roughly eight workspaces.
can-createboolfalseRenders the create row. Off by default because most members cannot create a workspace.
create-labelstringCreate workspaceCopy for the create row.
create-hrefstring—Where the create row navigates. Without it the row is a button carrying data-workspace-create (React's onCreate).
compactboolfalseRenders the 36px monogram button instead of the full card. Same menu either way.
sizesm | md | lgmdTrigger size tier — scales the card (and the compact square), its mark and name. The menu is unchanged.
aria-labelstringWorkspace <name>Accessible name for the trigger.
Option shape<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? }.