Club switcher

One identity can belong to several clubs. The active club is always visible in the top bar or sidebar header, so a trainer of two clubs never records attendance in the wrong one (UX guide §3.4).

With two or more clubs, the switcher is a button that opens a menu (popover, js/menu.js) listing every club membership with its logo; the active one is marked with a check and aria-current. Each entry is a link to that club's home — switching reloads navigation and data and never lands on a page of the previous club.

With one club (or no clubs), only the logo and name are shown as static text. Never render a menu with a single entry.

Examples

Member of two clubs (with menu)

Data
render({
  "current": {
    "name": "Taekwondo Bern",
    "shortName": "TKD Bern",
    "logo": "/media/clubs/tkd-bern.svg"
  },
  "clubs": [
    {
      "id": "tkd-bern",
      "name": "Taekwondo Bern",
      "href": "/c/tkd-bern/",
      "logo": "/media/clubs/tkd-bern.svg",
      "current": true
    },
    {
      "id": "tkd-thun",
      "name": "Taekwondo Club Thun",
      "href": "/c/tkd-thun/",
      "logo": "/media/clubs/tkd-thun.svg"
    }
  ]
})
Markup
<button class="club-switcher" type="button" aria-label="Verein wechseln: Taekwondo Bern" popovertarget="club-menu" aria-haspopup="menu" aria-expanded="false"><span class="avatar" data-size="sm" data-variant="club" aria-hidden="true"><img src="/media/clubs/tkd-bern.svg" alt=""></span>TKD Bern<svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#chevron-down"/></svg></button>
<div class="menu" id="club-menu" popover role="menu" data-behavior="menu"><a class="menu__item" role="menuitem" href="/c/tkd-bern/" data-club="tkd-bern" aria-current="true"><span class="avatar" data-size="sm" data-variant="club" aria-hidden="true"><img src="/media/clubs/tkd-bern.svg" alt=""></span><span class="u-grow">Taekwondo Bern</span><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg></a><a class="menu__item" role="menuitem" href="/c/tkd-thun/" data-club="tkd-thun"><span class="avatar" data-size="sm" data-variant="club" aria-hidden="true"><img src="/media/clubs/tkd-thun.svg" alt=""></span><span class="u-grow">Taekwondo Club Thun</span></a></div>

Single club (static)

TKD Bern
Data
render({
  "current": {
    "name": "Taekwondo Bern",
    "shortName": "TKD Bern"
  }
})
Markup
<span class="club-switcher">TKD Bern</span>

Data contract

What render(data) accepts — validated against this JSON Schema in development and tests. Unknown fields are rejected. The same schema is in components.json.

FieldTypeRequiredDefaultDescription
currentobjectrequired—The active club.
current.namestringrequired—Full club name ("Taekwondo Bern"), used in the accessible label.
min 1 chars
current.shortNamestringoptional—Short visible name when space is tight ("TKD Bern"). Defaults to name.
current.logostringoptional—Logo image URL (src); decorative, square.
menuIdstringoptional"club-menu"Id of the popover menu (popovertarget). Unique on the page.
pattern ^[A-Za-z][\w-]*$
clubsarray of objectsoptional—All clubs of the person (including the current one), from their memberships. The menu is rendered only with 2 or more.
clubs[].idstringrequired—Club id (tenant), written to data-club.
pattern ^[A-Za-z0-9_-]+$
clubs[].namestringrequired—Club name shown in the menu.
min 1 chars
clubs[].hrefstringrequired—Home URL of that club (switching target).
clubs[].logostringoptional—Logo image URL (src).
clubs[].currentbooleanoptionalfalseThis is the active club (check mark, aria-current).
JSON Schema
{
  "type": "object",
  "additionalProperties": false,
  "required": [
    "current"
  ],
  "properties": {
    "current": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "name"
      ],
      "description": "The active club.",
      "properties": {
        "name": {
          "type": "string",
          "minLength": 1,
          "description": "Full club name (\"Taekwondo Bern\"), used in the accessible label."
        },
        "shortName": {
          "type": "string",
          "description": "Short visible name when space is tight (\"TKD Bern\"). Defaults to `name`."
        },
        "logo": {
          "type": "string",
          "description": "Logo image URL (src); decorative, square."
        }
      }
    },
    "menuId": {
      "type": "string",
      "pattern": "^[A-Za-z][\\w-]*$",
      "default": "club-menu",
      "description": "Id of the popover menu (`popovertarget`). Unique on the page."
    },
    "clubs": {
      "type": "array",
      "description": "All clubs of the person (including the current one), from their memberships. The menu is rendered only with 2 or more.",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "name",
          "href"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[A-Za-z0-9_-]+$",
            "description": "Club id (tenant), written to `data-club`."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "description": "Club name shown in the menu."
          },
          "href": {
            "type": "string",
            "description": "Home URL of that club (switching target)."
          },
          "logo": {
            "type": "string",
            "description": "Logo image URL (src)."
          },
          "current": {
            "type": "boolean",
            "default": false,
            "description": "This is the active club (check mark, `aria-current`)."
          }
        }
      }
    }
  }
}

Markup & states

Root: button | span.club-switcher — usable without render() by writing the markup directly.

Attributes

AttributeValuesDefaultDescription
aria-label"Verein wechseln: {name}"—Accessible name of the switch button (states the active club).
popovertarget / aria-haspopup="menu" / aria-expandedmenu id—Opens the club menu (button only).

Parts

PartDescription
.avatar[data-variant="club"][data-size="sm"] > imgClub logo as a 32 px club avatar (--radius-md), decorative (alt="").
textClub (short) name, semibold.
.icon (chevron-down)Indicates the menu (button only, 16 px, muted).
.menu / .menu__itemPopover with one link per club: club avatar (logo) + name; the current club has aria-current="true", semibold text and a trailing check icon.

States

StateDescription
:hoverSurface shift (button, pointer devices).
[aria-expanded="true"]Menu open (set by menu.js).
.menu__item[aria-current="true"]Current club: semibold label, accent-text check icon.

Behaviour & events

Module: js/menu.js

mount(root) activates data-behavior="menu" on the popover: it positions the menu next to the button, focuses the first item and adds arrow keys, Home/End and Tab-to-close; Esc and outside clicks close natively (popover light dismiss).

Events

EventDetailDescription
click on .menu__itemlink `href` + `data-club`Navigates to the chosen club's home (full reload of navigation and data).

Accessibility

  • The button's name states the active club ("Verein wechseln: Taekwondo Bern") even when only the short name is visible.
  • Menu items are links with role="menuitem"; the active club has aria-current="true" and a check icon, not colour alone.
  • Logos are decorative; the club name is always text.

Guidance

DoKeep the active club name visible on every screen.
DoLand on the chosen club's home after switching.
Don'tDon't show the switcher menu for people with one club.
Don'tDon't keep the current page when switching — it belongs to the old club.

Related: App shell · Avatar