Context Switcher <mv-context-switcher>

Seletor de cabeçalho entre um contexto raiz (um grupo, uma organização, todas as lojas) e um de seus membros, construído sobre o menu: o gatilho mostra onde você está, a lista pode chegar depois da primeira renderização com uma linha de carregamento, um botão opcional volta para a raiz e um evento change cancelável permite que o app confirme ou recuse a troca.

CategoriaNavegação
TipoWeb Component (<mv-context-switcher>)
Statusestável
Também instalapopover, menu
Keywordscontext switcher, scope switcher, organization switcher, company switcher, workspace switcher, team switcher, tenant, multi-company, group view, app header, back office, erp, menuitemradio, async list

When to use

  • A back office moves between a consolidated group view and one company, with a quick way back to the group
  • An app header must show which organization, workspace or store the whole screen is scoped to
  • The list of companies or workspaces comes from an API after the page has rendered
  • The app must confirm or refuse a context change, for example while a form has unsaved edits

Avoid when

  • The list holds hundreds of entries that people need to search by typing → use Combobox instead
  • The trigger opens actions such as Rename or Delete rather than a choice of scope → use Menu instead
  • The choice is a form value submitted with other fields → use Select instead

Instalação

node scripts/add.mjs context-switcher --out ./src/marvelous

Agente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["context-switcher"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Arquivos copiados (dependências incluídas): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/focus.js, core/motion.js, core/position.js, core/svg.js, components/popover/popover.js, components/popover/popover.css, core/dismiss.js, components/menu/menu.js, components/menu/menu.css, components/context-switcher/context-switcher.js, components/context-switcher/context-switcher.css.

Uso

Início rápido, a menor marcação que funciona:

<mv-context-switcher root-label="Halden Group" back-button>
  <option value="halden-fr" data-description="Paris · EUR">Halden France</option>
  <option value="halden-uk" data-description="London · GBP">Halden UK</option>
</mv-context-switcher>

Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis CSS:

<div id="cs-demo" style="display:grid;gap:1.5rem;width:100%;max-width:56rem;margin-inline:auto">
  <div style="border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);overflow:hidden;background:var(--mv-surface)">
    <header style="display:flex;align-items:center;gap:.75rem;min-height:3.5rem;padding:.375rem .75rem;border-bottom:1px solid var(--mv-border)">
      <mv-context-switcher id="cs-erp" root-label="Halden Group" back-button loading></mv-context-switcher>
      <span style="margin-inline-start:auto;font-size:.8125rem;color:var(--mv-fg-muted);white-space:nowrap">October 2026</span>
    </header>
    <div style="display:grid;gap:.25rem;padding:1.25rem 1.25rem 1.5rem">
      <span id="cs-erp-scope" style="font-size:.8125rem;color:var(--mv-fg-muted)">Consolidated, 4 companies</span>
      <strong id="cs-erp-revenue" style="font-size:1.75rem;font-weight:600;letter-spacing:-.01em;font-variant-numeric:tabular-nums">$9,642,300</strong>
      <span style="font-size:.8125rem;color:var(--mv-fg-muted)">Revenue this month, reported in USD</span>
    </div>
  </div>

  <div style="display:flex;flex-wrap:wrap;align-items:center;gap:.75rem;padding:.5rem .75rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg)">
    <mv-context-switcher id="cs-stores" data-size="sm" data-variant="outline" root-label="All stores" list-label="Stores" value="lisbon" back-button>
      <option value="lisbon" data-description="Rua Augusta 112">Lisbon Baixa</option>
      <option value="porto" data-description="Rua de Santa Catarina 48">Porto Centro</option>
      <option value="madrid" data-description="Calle de Fuencarral 21">Madrid Malasaña</option>
    </mv-context-switcher>
    <span style="font-size:.8125rem;color:var(--mv-fg-muted)">Compact, outlined, list written as <code>&lt;option&gt;</code> children</span>
  </div>
</div>

<script type="module">
  const erp = document.getElementById("cs-erp");
  const stores = document.getElementById("cs-stores");
  stores.strings = { root: "All stores", item: "Store", rootHint: "Every location", none: "Select a store" };

  const companies = [
    { value: "halden-fr", label: "Halden France", description: "Paris · EUR", revenue: 3218400 },
    { value: "halden-de", label: "Halden Germany", description: "Munich · EUR", revenue: 2764900 },
    { value: "halden-uk", label: "Halden UK", description: "London · GBP", revenue: 2105000 },
    { value: "halden-us", label: "Halden US", description: "Chicago · USD", revenue: 1554000 },
    { value: "halden-jp", label: "Halden Japan", description: "Osaka · onboarding", disabled: true, revenue: 0 },
  ];
  const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 });
  const total = companies.reduce((sum, c) => sum + c.revenue, 0);

  // The list arrives after the first render, as from an API.
  setTimeout(() => {
    erp.items = companies.map(({ revenue, ...c }) => c);
    erp.loading = false;
  }, 1400);

  erp.addEventListener("mv-change", (e) => {
    const company = companies.find((c) => c.value === e.detail.value);
    document.getElementById("cs-erp-scope").textContent = e.detail.root ? "Consolidated, 4 companies" : `${e.detail.label}, ${e.detail.item?.description ?? ""}`;
    document.getElementById("cs-erp-revenue").textContent = usd.format(e.detail.root ? total : company?.revenue ?? 0);
  });
</script>

API

Attributes

NameTipoDefaultDescription
valuestringValue of the active member. Empty, absent or equal to root-value: the root context is active. Setting it does not emit mv-change.
root-labelstringName of the root context (e.g. the group). Without it there is no root entry and no back button: a plain member switcher.
root-valuestring""Value that stands for the root context in value and in mv-change.
list-labelstringHeading above the members (overrides strings.list).
loadingbooleanThe member list is being fetched: a busy row replaces it (aria-busy) and the trigger shows strings.loading if value is not known yet. Removing it while the menu is open announces the count.
back-buttonbooleanShows an icon button next to the trigger, while a member is active, that returns to the root context.
placementtop | bottom | left | right[-start|-end]bottom-startMenu position relative to the trigger (automatic flip/shift).
data-sizemd | smmdsm: one-line trigger for dense toolbars (the kicker stays for screen readers).
data-variantghost | outlineghostoutline: bordered trigger on a surface, for toolbars and filters.
data-contextroot | itemSet by the component on itself: which kind of context is active (for styling the page around it).

Properties

NameTipoDescription
itemsArray<{ value, label, description?, mark?, hue?, disabled? }> | nullMembers, for lists loaded by script. Replaces the <option> children; null goes back to them. Assignable before the element is defined.
strings{ root, item, list, rootHint, none, loading, empty, loaded, back }Default texts, for translation: kickers ("Group view", "Company"), list heading, root hint ("All companies"), placeholder, loading, empty, loaded announcement ("Companies loaded: {count}") and back button name ("Back to {label}"). Assignable before the element is defined.
current{ value, label, root }The active context (read-only).
isOpenbooleanWhether the menu is open (read-only).

Methods

NameDescription
show({ focus })Opens the menu; focus = "first" | "last" | "panel".
hide({ focus })Closes the menu; focus=true returns focus to the trigger.

Events

NameDescription
mv-changeThe user picked another context (menu or back button). Cancelable: preventDefault keeps the current one. detail: { value, label, root, item, previous } (item is the member object, null for the root).
mv-open / mv-closeFrom the inner menu, when it opens and closes.

Content structure

NameDescription
<option>One member each: value, label (or text), data-description, data-mark (badge text, initials by default), data-hue (badge hue 0-360, derived from the value by default), disabled. Never rendered; added, removed or changed later, they update the list.

CSS classes

NameDescription
mv-context-switcher-trigger / -mark / -kicker / -label / -chevronGenerated trigger parts (mark and chevron are aria-hidden; the root mark carries data-root).
mv-context-switcher-backGenerated back button (hidden while the root is active).
mv-context-switcher-panel / -option / -name / -description / -statusGenerated menu panel, member rows (role=menuitemradio) and the loading or empty row.

CSS variables

NameDefaultDescription
--mv-context-switcher-max-width18remTrigger maximum width; longer names are truncated with an ellipsis.
--mv-context-switcher-menu-width16remMenu minimum width.
--mv-context-switcher-mark-size1.75remTrigger badge size (1.25rem in data-size="sm").

Accessibility

APG Menu Button: the trigger is a native button with aria-haspopup=menu and aria-expanded, named by its visible text (kicker then the active context, e.g. “Company Halden UK”). The panel is a role=menu with two labelled groups (the root, then the members) of menuitemradio rows whose aria-checked follows the active context. Keyboard from the menu: Enter, Space or ↓ open on the first row, ↑ on the last; ↑ ↓ Home End move (wrapping), letters jump by name (accent-insensitive), Enter or Space switch, Escape closes and Tab closes and moves on, focus returns to the trigger, which then reads the new context. Disabled members stay visible with aria-disabled and are skipped. While loading, the list has aria-busy and a polite status announces “Loading companies…” on open and the count once the list arrives; if rows are replaced while focus is in them, focus moves to the checked or first row instead of being lost. The back button is a real button named “Back to Halden Group”; it is removed from the tab order while the root is active and focus moves to the trigger after it is used. Badges are decorative (aria-hidden), colors are never the only cue, forced colors use system colors. Known limits: no search field inside the menu (use combobox for very long lists), and the menu grows with the list up to the viewport height, then scrolls.

Esta página foi traduzida com IA. Informar um problema de tradução