Navigationstable.club-switcher
Club switcher
Shows the active club (logo + name) and, for people in several clubs, opens a menu to switch.
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)
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.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
current | object | required | — | The active club. |
current.name | string | required | — | Full club name ("Taekwondo Bern"), used in the accessible label. min 1 chars |
current.shortName | string | optional | — | Short visible name when space is tight ("TKD Bern"). Defaults to name. |
current.logo | string | optional | — | Logo image URL (src); decorative, square. |
menuId | string | optional | "club-menu" | Id of the popover menu (popovertarget). Unique on the page.pattern ^[A-Za-z][\w-]*$ |
clubs | array of objects | optional | — | All clubs of the person (including the current one), from their memberships. The menu is rendered only with 2 or more. |
clubs[].id | string | required | — | Club id (tenant), written to data-club.pattern ^[A-Za-z0-9_-]+$ |
clubs[].name | string | required | — | Club name shown in the menu. min 1 chars |
clubs[].href | string | required | — | Home URL of that club (switching target). |
clubs[].logo | string | optional | — | Logo image URL (src). |
clubs[].current | boolean | optional | false | This 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
| Attribute | Values | Default | Description |
|---|---|---|---|
aria-label | "Verein wechseln: {name}" | — | Accessible name of the switch button (states the active club). |
popovertarget / aria-haspopup="menu" / aria-expanded | menu id | — | Opens the club menu (button only). |
Parts
| Part | Description |
|---|---|
.avatar[data-variant="club"][data-size="sm"] > img | Club logo as a 32 px club avatar (--radius-md), decorative (alt=""). |
text | Club (short) name, semibold. |
.icon (chevron-down) | Indicates the menu (button only, 16 px, muted). |
.menu / .menu__item | Popover with one link per club: club avatar (logo) + name; the current club has aria-current="true", semibold text and a trailing check icon. |
States
| State | Description |
|---|---|
:hover | Surface 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
| Event | Detail | Description |
|---|---|---|
click on .menu__item | link `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 hasaria-current="true"and a check icon, not colour alone. - Logos are decorative; the club name is always text.