Data displaystable.list
List
A vertical list of items, each row with one primary action and optional secondary actions in a menu.
The default way to show collections on phones and in admin screens: members, trainings, applicants, documents. One row = one thing; the whole row (href) is the primary action — usually "open details".
Secondary actions go into the row's overflow menu (⋯). Never put more than one visible button in a row; that was the audit's "row toolbox" anti-pattern.
At most one secondary line (meta). Status goes into the aside as a status badge; grades as a belt badge.
Examples
Members with grade and menu
- LMLea Müller6. Kup
- NKNoah Keller7. Kup
- EYElif YilmazInaktiv
Data
render({
"label": "Mitglieder Kinder 7–10",
"items": [
{
"id": "m-1042",
"title": "Lea Müller",
"meta": "Kinder 7–10 · seit 2024",
"href": "#m-1042",
"avatar": "LM",
"belt": {
"label": "6. Kup",
"color": "green"
},
"actions": [
{
"action": "edit",
"label": "Bearbeiten",
"icon": "edit"
},
{
"action": "mail",
"label": "E-Mail senden",
"icon": "mail"
},
{
"action": "remove",
"label": "Aus Gruppe entfernen",
"icon": "trash",
"danger": true
}
]
},
{
"id": "m-1077",
"title": "Noah Keller",
"meta": "Jugend 11–15",
"href": "#m-1077",
"avatar": "NK",
"belt": {
"label": "7. Kup",
"color": "yellow",
"tip": "green"
}
},
{
"id": "m-0981",
"title": "Elif Yilmaz",
"meta": "abgemeldet per 31.12.",
"avatar": "EY",
"done": true,
"badge": {
"label": "Inaktiv"
}
}
]
})Markup
<ul class="list" role="list" aria-label="Mitglieder Kinder 7–10"><li class="row" data-id="m-1042"><a class="row__main" href="#m-1042"><span class="avatar">LM</span><span class="row__text"><span class="row__title">Lea Müller</span><span class="row__meta">Kinder 7–10 · seit 2024</span></span></a><div class="row__aside"><span class="belt" style="--belt-color: var(--belt-green)">6. Kup</span>
<button class="button" type="button" data-variant="ghost" data-icon-only aria-label="Weitere Aktionen für Lea Müller" popovertarget="menu-m-1042" aria-haspopup="menu" aria-expanded="false"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#more"/></svg></button>
<div class="menu" id="menu-m-1042" popover role="menu" data-behavior="menu"><button class="menu__item" type="button" role="menuitem" data-action="edit"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#edit"/></svg>Bearbeiten</button><button class="menu__item" type="button" role="menuitem" data-action="mail"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#mail"/></svg>E-Mail senden</button><button class="menu__item" type="button" role="menuitem" data-action="remove" data-variant="danger"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#trash"/></svg>Aus Gruppe entfernen</button></div></div></li><li class="row" data-id="m-1077"><a class="row__main" href="#m-1077"><span class="avatar">NK</span><span class="row__text"><span class="row__title">Noah Keller</span><span class="row__meta">Jugend 11–15</span></span></a><div class="row__aside"><span class="belt" style="--belt-color: var(--belt-yellow); --belt-tip: var(--belt-green)">7. Kup</span></div></li><li class="row" data-id="m-0981" data-state="done"><div class="row__main"><span class="avatar">EY</span><span class="row__text"><span class="row__title">Elif Yilmaz</span><span class="row__meta">abgemeldet per 31.12.</span></span></div><div class="row__aside"><span class="badge">Inaktiv</span></div></li></ul>Training: swipe as the primary gesture (athlete app)
gestures: primary, commit mode. Right = exercise done, left = one set done. The check button is the visible tap alternative; both swipe actions are also in the menu.
So geht es
- Nach rechts wischen: Übung fertig
- Nach links wischen: ein Satz geschafft
- Menü (⋯): Details und Zurücksetzen
- Dollyo Chagi an Pratze
- Plank
- Seilspringen
Data
render({
"label": "Deine Übungen",
"gestures": "primary",
"swipeHint": {
"id": "training-swipe",
"title": "So geht es",
"items": [
"Nach rechts wischen: Übung fertig",
"Nach links wischen: ein Satz geschafft",
"Menü (⋯): Details und Zurücksetzen"
]
},
"items": [
{
"id": "ex-1",
"title": "Dollyo Chagi an Pratze",
"meta": "4×15 · Pause 60 s",
"href": "#ex-1",
"progress": {
"done": 2,
"total": 4
},
"quickAction": {
"action": "done",
"label": "Fertig",
"icon": "check"
},
"swipe": {
"mode": "commit",
"start": {
"action": "done",
"label": "Übung fertig",
"icon": "check",
"tone": "success"
},
"end": {
"action": "set",
"label": "Satz geschafft",
"icon": "plus",
"tone": "accent"
}
},
"actions": [
{
"action": "done",
"label": "Übung fertig",
"icon": "check"
},
{
"action": "set",
"label": "Ein Satz geschafft",
"icon": "plus"
},
{
"action": "details",
"label": "Details",
"icon": "info"
}
]
},
{
"id": "ex-2",
"title": "Plank",
"meta": "3×45 s",
"href": "#ex-2",
"progress": {
"done": 0,
"total": 3
},
"quickAction": {
"action": "done",
"label": "Fertig",
"icon": "check"
},
"swipe": {
"mode": "commit",
"start": {
"action": "done",
"label": "Übung fertig",
"icon": "check",
"tone": "success"
},
"end": {
"action": "set",
"label": "Satz geschafft",
"icon": "plus",
"tone": "accent"
}
},
"actions": [
{
"action": "done",
"label": "Übung fertig",
"icon": "check"
},
{
"action": "set",
"label": "Ein Satz geschafft",
"icon": "plus"
}
]
},
{
"id": "ex-3",
"title": "Seilspringen",
"meta": "3×120 s",
"done": true,
"progress": {
"done": 3,
"total": 3
},
"actions": [
{
"action": "reset",
"label": "Zurücksetzen",
"icon": "undo"
}
]
}
]
})Markup
<div class="stack" data-gap="sm"><div class="alert" data-status="neutral" role="note" data-behavior="dismiss-once" data-dismiss-key="training-swipe"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#info"/></svg><div class="alert__body"><p class="alert__title">So geht es</p><ul><li>Nach rechts wischen: Übung fertig</li><li>Nach links wischen: ein Satz geschafft</li><li>Menü (⋯): Details und Zurücksetzen</li></ul><div class="button-row" data-align="start"><button type="button" class="button" data-size="sm" data-dismiss>Nicht mehr anzeigen</button></div></div></div><ul class="list" role="list" aria-label="Deine Übungen" data-gestures="primary" data-behavior="swipe-peek"><li class="row" data-id="ex-1" data-behavior="swipe" data-swipe-mode="commit" data-swipe-start="done" data-swipe-end="set"><div class="row__swipe" data-side="start" data-tone="success" aria-hidden="true"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg>Übung fertig</div><div class="row__swipe" data-side="end" data-tone="accent" aria-hidden="true"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#plus"/></svg>Satz geschafft</div><a class="row__main" href="#ex-1"><span class="row__text"><span class="row__title">Dollyo Chagi an Pratze</span><span class="row__meta">4×15 · Pause 60 s</span><span class="segments" role="img" aria-label="2 von 4 Sätzen"><span data-done></span><span data-done></span><span></span><span></span></span></span></a><div class="row__aside"><button type="button" class="button row__quick" data-size="sm" data-action="done" aria-label="Fertig"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg><span class="row__quick-label">Fertig</span></button>
<button class="button" type="button" data-variant="ghost" data-icon-only aria-label="Weitere Aktionen für Dollyo Chagi an Pratze" popovertarget="menu-ex-1" aria-haspopup="menu" aria-expanded="false"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#more"/></svg></button>
<div class="menu" id="menu-ex-1" popover role="menu" data-behavior="menu"><button class="menu__item" type="button" role="menuitem" data-action="done"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg>Übung fertig</button><button class="menu__item" type="button" role="menuitem" data-action="set"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#plus"/></svg>Ein Satz geschafft</button><button class="menu__item" type="button" role="menuitem" data-action="details"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#info"/></svg>Details</button></div></div></li><li class="row" data-id="ex-2" data-behavior="swipe" data-swipe-mode="commit" data-swipe-start="done" data-swipe-end="set"><div class="row__swipe" data-side="start" data-tone="success" aria-hidden="true"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg>Übung fertig</div><div class="row__swipe" data-side="end" data-tone="accent" aria-hidden="true"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#plus"/></svg>Satz geschafft</div><a class="row__main" href="#ex-2"><span class="row__text"><span class="row__title">Plank</span><span class="row__meta">3×45 s</span><span class="segments" role="img" aria-label="0 von 3 Sätzen"><span></span><span></span><span></span></span></span></a><div class="row__aside"><button type="button" class="button row__quick" data-size="sm" data-action="done" aria-label="Fertig"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg><span class="row__quick-label">Fertig</span></button>
<button class="button" type="button" data-variant="ghost" data-icon-only aria-label="Weitere Aktionen für Plank" popovertarget="menu-ex-2" aria-haspopup="menu" aria-expanded="false"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#more"/></svg></button>
<div class="menu" id="menu-ex-2" popover role="menu" data-behavior="menu"><button class="menu__item" type="button" role="menuitem" data-action="done"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#check"/></svg>Übung fertig</button><button class="menu__item" type="button" role="menuitem" data-action="set"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#plus"/></svg>Ein Satz geschafft</button></div></div></li><li class="row" data-id="ex-3" data-state="done"><div class="row__main"><span class="row__text"><span class="row__title">Seilspringen</span><span class="row__meta">3×120 s</span><span class="segments" role="img" aria-label="3 von 3 Sätzen"><span data-done></span><span data-done></span><span data-done></span></span></span></div><div class="row__aside">
<button class="button" type="button" data-variant="ghost" data-icon-only aria-label="Weitere Aktionen für Seilspringen" popovertarget="menu-ex-3" aria-haspopup="menu" aria-expanded="false"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#more"/></svg></button>
<div class="menu" id="menu-ex-3" popover role="menu" data-behavior="menu"><button class="menu__item" type="button" role="menuitem" data-action="reset"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#undo"/></svg>Zurücksetzen</button></div></div></li></ul></div>Attendance: swipe to reveal (Trainer app)
gestures: shortcut, reveal mode. The swipe only uncovers the button, because it writes to the presence list in the CRM.
- LMLea Müller6. Kup
- NKNoah Keller7. Kup
Data
render({
"label": "Kinder 7–10",
"items": [
{
"id": "m-1042",
"title": "Lea Müller",
"avatar": "LM",
"belt": {
"label": "6. Kup",
"color": "green"
},
"swipe": {
"mode": "reveal",
"start": {
"action": "move",
"label": "Verschieben",
"icon": "users"
},
"end": {
"action": "absent",
"label": "War nicht da",
"icon": "x",
"tone": "danger"
}
},
"actions": [
{
"action": "absent",
"label": "War nicht da",
"icon": "x"
},
{
"action": "move",
"label": "In andere Klasse verschieben",
"icon": "users"
}
]
},
{
"id": "m-1077",
"title": "Noah Keller",
"avatar": "NK",
"belt": {
"label": "7. Kup",
"color": "yellow",
"tip": "green"
},
"swipe": {
"mode": "reveal",
"start": {
"action": "move",
"label": "Verschieben",
"icon": "users"
},
"end": {
"action": "absent",
"label": "War nicht da",
"icon": "x",
"tone": "danger"
}
},
"actions": [
{
"action": "absent",
"label": "War nicht da",
"icon": "x"
},
{
"action": "move",
"label": "In andere Klasse verschieben",
"icon": "users"
}
]
}
]
})Markup
<ul class="list" role="list" aria-label="Kinder 7–10"><li class="row" data-id="m-1042" data-behavior="swipe" data-swipe-mode="reveal" data-swipe-start="move" data-swipe-end="absent"><div class="row__swipe" data-side="start" aria-hidden="true"><button type="button" class="button" data-size="sm" tabindex="-1" data-action="move"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#users"/></svg>Verschieben</button></div><div class="row__swipe" data-side="end" data-tone="danger" aria-hidden="true"><button type="button" class="button" data-size="sm" tabindex="-1" data-action="absent" data-variant="danger"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#x"/></svg>War nicht da</button></div><div class="row__main"><span class="avatar">LM</span><span class="row__text"><span class="row__title">Lea Müller</span></span></div><div class="row__aside"><span class="belt" style="--belt-color: var(--belt-green)">6. Kup</span>
<button class="button" type="button" data-variant="ghost" data-icon-only aria-label="Weitere Aktionen für Lea Müller" popovertarget="menu-m-1042" aria-haspopup="menu" aria-expanded="false"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#more"/></svg></button>
<div class="menu" id="menu-m-1042" popover role="menu" data-behavior="menu"><button class="menu__item" type="button" role="menuitem" data-action="absent"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#x"/></svg>War nicht da</button><button class="menu__item" type="button" role="menuitem" data-action="move"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#users"/></svg>In andere Klasse verschieben</button></div></div></li><li class="row" data-id="m-1077" data-behavior="swipe" data-swipe-mode="reveal" data-swipe-start="move" data-swipe-end="absent"><div class="row__swipe" data-side="start" aria-hidden="true"><button type="button" class="button" data-size="sm" tabindex="-1" data-action="move"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#users"/></svg>Verschieben</button></div><div class="row__swipe" data-side="end" data-tone="danger" aria-hidden="true"><button type="button" class="button" data-size="sm" tabindex="-1" data-action="absent" data-variant="danger"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#x"/></svg>War nicht da</button></div><div class="row__main"><span class="avatar">NK</span><span class="row__text"><span class="row__title">Noah Keller</span></span></div><div class="row__aside"><span class="belt" style="--belt-color: var(--belt-yellow); --belt-tip: var(--belt-green)">7. Kup</span>
<button class="button" type="button" data-variant="ghost" data-icon-only aria-label="Weitere Aktionen für Noah Keller" popovertarget="menu-m-1077" aria-haspopup="menu" aria-expanded="false"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#more"/></svg></button>
<div class="menu" id="menu-m-1077" popover role="menu" data-behavior="menu"><button class="menu__item" type="button" role="menuitem" data-action="absent"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#x"/></svg>War nicht da</button><button class="menu__item" type="button" role="menuitem" data-action="move"><svg class="icon" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#users"/></svg>In andere Klasse verschieben</button></div></div></li></ul>Registrations with status
- LGLuca GerberFamilie
- JRJonas RothMöglicher Doppeleintrag
Data
render({
"label": "Offene Anmeldungen",
"items": [
{
"id": "r-17",
"title": "Luca Gerber",
"meta": "9 Jahre · eingegangen gestern",
"href": "#r-17",
"avatar": "LG",
"current": true,
"badge": {
"label": "Familie",
"tone": "info"
}
},
{
"id": "r-18",
"title": "Jonas Roth",
"meta": "12 Jahre · vor 5 Tagen",
"href": "#r-18",
"avatar": "JR",
"badge": {
"label": "Möglicher Doppeleintrag",
"tone": "warning"
}
}
]
})Markup
<ul class="list" role="list" aria-label="Offene Anmeldungen"><li class="row" data-id="r-17" aria-current="true"><a class="row__main" href="#r-17"><span class="avatar">LG</span><span class="row__text"><span class="row__title">Luca Gerber</span><span class="row__meta">9 Jahre · eingegangen gestern</span></span></a><div class="row__aside"><span class="badge" data-status="info"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#info"/></svg>Familie</span></div></li><li class="row" data-id="r-18"><a class="row__main" href="#r-18"><span class="avatar">JR</span><span class="row__text"><span class="row__title">Jonas Roth</span><span class="row__meta">12 Jahre · vor 5 Tagen</span></span></a><div class="row__aside"><span class="badge" data-status="warning"><svg class="icon" data-size="sm" aria-hidden="true"><use href="../../packages/ui/icons/icons.svg#alert-triangle"/></svg>Möglicher Doppeleintrag</span></div></li></ul>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 |
|---|---|---|---|---|
label | string | optional | — | Accessible name of the list (aria-label), e.g. "Offene Anmeldungen". |
flush | boolean | optional | false | No border/background (list inside a card). |
gestures | "shortcut" | "primary" | optional | "shortcut" | How prominent swipes are. shortcut: swipes are extras for people who know them. primary: swiping is the main way to work this list (e.g. ticking exercises): the hint is shown, the first open row peeks once to show it can move, and quick actions become icon-only to give the gesture room. Only on phones; desktop always behaves like shortcut. |
swipeHint | object | optional | — | One-time hint explaining the swipe gestures (athlete app pattern). Shown until dismissed; remembered per device under id. |
swipeHint.id | string | required | — | Storage key for "Nicht mehr anzeigen", e.g. "training-swipe". pattern ^[a-z0-9-]+$ |
swipeHint.title | string | required | — | Hint title. |
swipeHint.items | array of string | required | — | One line per gesture. min 1 items · max 4 items |
swipeHint.dismissLabel | string | optional | "Nicht mehr anzeigen" | Label of the dismiss button. |
items | array of objects | required | — | Rows in display order. |
items[].id | string | required | — | Stable id of the thing (member id, registration id). Used for data-id and menu ids.pattern ^[A-Za-z0-9_-]+$ |
items[].title | string | required | — | Main text (name, title). min 1 chars |
items[].meta | string | optional | — | One secondary line ("Kinder 7–10 · seit 2024"). |
items[].href | string | optional | — | Primary action: link to the detail page. Without href the row is static. |
items[].avatar | string | optional | — | Initials shown in an avatar circle ("LM"). max 3 chars |
items[].belt | object | optional | — | Optional grade shown in the aside (see Belt badge). |
items[].belt.label | string | required | — | Localised grade name from the grading system, e.g. "6. Kup · Grüngurt". Shown as text. min 1 chars · e.g. "6. Kup · Grüngurt", "1. Dan" |
items[].belt.color | "white" | "yellow" | "green" | "blue" | "red" | "black" | required | — | Main belt colour; maps to the token --belt-{color}. |
items[].belt.tip | "white" | "yellow" | "green" | "blue" | "red" | "black" | optional | — | Optional second colour for striped/tipped belts (e.g. yellow-green). Maps to --belt-{tip}. |
items[].belt.short | boolean | optional | false | Compact form for dense tables: the label should then be the short grade ("6. Kup"); the full name goes into title. |
items[].belt.title | string | optional | — | Full grade name as tooltip and accessible description when short is used. |
items[].belt.size | "sm" | "md" | "lg" | optional | "md" | Swatch size (design guide §9.7): sm in list rows, md (24×8 px) in cards and tables, lg on profiles, exam results and TV screens. |
items[].badge | object | optional | — | Optional status badge in the aside. |
items[].badge.label | string | required | — | Status text ("Wartet auf Freigabe"). |
items[].badge.tone | "neutral" | "info" | "success" | "warning" | "danger" | optional | "neutral" | Status (see Status badge): written as data-status with a leading status icon; neutral has neither. |
items[].current | boolean | optional | false | Selected row (e.g. shown in the detail pane next to the list). |
items[].quickAction | object | optional | — | One visible button for the main job of the row when that job is not "open details", for example "Fertig" on an exercise. Also the tap alternative to a swipe. Must be in actions. |
items[].quickAction.action | string | required | — | Action key (also in actions). |
items[].quickAction.label | string | required | — | Button text, a verb. Becomes the accessible name when the button is icon-only. |
items[].quickAction.icon | string | optional | — | Icon; required for gestures: primary lists, where the button is icon-only on phones. |
items[].swipe | object | optional | — | Swipe shortcuts (js/swipe.js). commit: releasing past the threshold performs the action; only for safe or reversible actions. reveal: the swipe uncovers a button that performs it; for writes to external systems. |
items[].swipe.mode | "commit" | "reveal" | optional | "commit" | Gesture model (see description). |
items[].swipe.start | object | optional | — | Layer uncovered by dragging towards the end side (to the right in LTR): the side where text starts. |
items[].swipe.start.action | string | required | — | Action key; must also be in actions (swipe is a shortcut, never the only way). |
items[].swipe.start.label | string | required | — | Text on the uncovered layer, for example "Übung fertig". |
items[].swipe.start.icon | string | optional | — | Icon id on the layer. |
items[].swipe.start.tone | "neutral" | "success" | "accent" | "danger" | optional | "neutral" | Colour of the layer; turns solid when armed. |
items[].swipe.end | object | optional | — | Layer uncovered by dragging towards the start side (to the left in LTR). |
items[].swipe.end.action | string | required | — | Action key; must also be in actions (swipe is a shortcut, never the only way). |
items[].swipe.end.label | string | required | — | Text on the uncovered layer, for example "Übung fertig". |
items[].swipe.end.icon | string | optional | — | Icon id on the layer. |
items[].swipe.end.tone | "neutral" | "success" | "accent" | "danger" | optional | "neutral" | Colour of the layer; turns solid when armed. |
items[].progress | object | optional | — | Set segments under the title (exercise with several sets). |
items[].progress.done | integer | required | — | Sets done. ≥ 0 |
items[].progress.total | integer | required | — | Sets in total. ≥ 1 · ≤ 12 |
items[].done | boolean | optional | false | Completed/inactive item: muted and struck through. |
items[].actions | array of objects | optional | — | Secondary actions for the overflow menu. The page handles clicks via data-action. |
items[].actions[].action | string | required | — | Action key written to data-action ("edit", "remove"). |
items[].actions[].label | string | required | — | Menu text, a verb ("Bearbeiten"). |
items[].actions[].icon | string | optional | — | Icon id. |
items[].actions[].danger | boolean | optional | false | Destructive action (red text); still needs undo or confirm. |
JSON Schema
{
"type": "object",
"additionalProperties": false,
"required": [
"items"
],
"properties": {
"label": {
"type": "string",
"description": "Accessible name of the list (`aria-label`), e.g. \"Offene Anmeldungen\"."
},
"flush": {
"type": "boolean",
"default": false,
"description": "No border/background (list inside a card)."
},
"gestures": {
"type": "string",
"enum": [
"shortcut",
"primary"
],
"default": "shortcut",
"description": "How prominent swipes are. `shortcut`: swipes are extras for people who know them. `primary`: swiping is the main way to work this list (e.g. ticking exercises): the hint is shown, the first open row peeks once to show it can move, and quick actions become icon-only to give the gesture room. Only on phones; desktop always behaves like `shortcut`."
},
"swipeHint": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"title",
"items"
],
"description": "One-time hint explaining the swipe gestures (athlete app pattern). Shown until dismissed; remembered per device under `id`.",
"properties": {
"id": {
"type": "string",
"pattern": "^[a-z0-9-]+$",
"description": "Storage key for \"Nicht mehr anzeigen\", e.g. \"training-swipe\"."
},
"title": {
"type": "string",
"description": "Hint title."
},
"items": {
"type": "array",
"minItems": 1,
"maxItems": 4,
"description": "One line per gesture.",
"items": {
"type": "string",
"description": "For example \"Nach rechts wischen: Übung fertig\"."
}
},
"dismissLabel": {
"type": "string",
"default": "Nicht mehr anzeigen",
"description": "Label of the dismiss button."
}
}
},
"items": {
"type": "array",
"description": "Rows in display order.",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"title"
],
"properties": {
"id": {
"type": "string",
"pattern": "^[A-Za-z0-9_-]+$",
"description": "Stable id of the thing (member id, registration id). Used for `data-id` and menu ids."
},
"title": {
"type": "string",
"minLength": 1,
"description": "Main text (name, title)."
},
"meta": {
"type": "string",
"description": "One secondary line (\"Kinder 7–10 · seit 2024\")."
},
"href": {
"type": "string",
"description": "Primary action: link to the detail page. Without `href` the row is static."
},
"avatar": {
"type": "string",
"maxLength": 3,
"description": "Initials shown in an avatar circle (\"LM\")."
},
"belt": {
"type": "object",
"additionalProperties": false,
"required": [
"label",
"color"
],
"properties": {
"label": {
"type": "string",
"minLength": 1,
"description": "Localised grade name from the grading system, e.g. \"6. Kup · Grüngurt\". Shown as text.",
"examples": [
"6. Kup · Grüngurt",
"1. Dan"
]
},
"color": {
"type": "string",
"enum": [
"white",
"yellow",
"green",
"blue",
"red",
"black"
],
"description": "Main belt colour; maps to the token `--belt-{color}`."
},
"tip": {
"type": "string",
"enum": [
"white",
"yellow",
"green",
"blue",
"red",
"black"
],
"description": "Optional second colour for striped/tipped belts (e.g. yellow-green). Maps to `--belt-{tip}`."
},
"short": {
"type": "boolean",
"default": false,
"description": "Compact form for dense tables: the label should then be the short grade (\"6. Kup\"); the full name goes into `title`."
},
"title": {
"type": "string",
"description": "Full grade name as tooltip and accessible description when `short` is used."
},
"size": {
"type": "string",
"enum": [
"sm",
"md",
"lg"
],
"default": "md",
"description": "Swatch size (design guide §9.7): `sm` in list rows, `md` (24×8 px) in cards and tables, `lg` on profiles, exam results and TV screens."
}
},
"description": "Optional grade shown in the aside (see Belt badge)."
},
"badge": {
"type": "object",
"additionalProperties": false,
"required": [
"label"
],
"description": "Optional status badge in the aside.",
"properties": {
"label": {
"type": "string",
"description": "Status text (\"Wartet auf Freigabe\")."
},
"tone": {
"type": "string",
"enum": [
"neutral",
"info",
"success",
"warning",
"danger"
],
"default": "neutral",
"description": "Status (see Status badge): written as `data-status` with a leading status icon; `neutral` has neither."
}
}
},
"current": {
"type": "boolean",
"default": false,
"description": "Selected row (e.g. shown in the detail pane next to the list)."
},
"quickAction": {
"type": "object",
"additionalProperties": false,
"required": [
"action",
"label"
],
"description": "One visible button for the main job of the row when that job is not \"open details\", for example \"Fertig\" on an exercise. Also the tap alternative to a swipe. Must be in `actions`.",
"properties": {
"action": {
"type": "string",
"description": "Action key (also in `actions`)."
},
"label": {
"type": "string",
"description": "Button text, a verb. Becomes the accessible name when the button is icon-only."
},
"icon": {
"type": "string",
"description": "Icon; required for `gestures: primary` lists, where the button is icon-only on phones."
}
}
},
"swipe": {
"type": "object",
"additionalProperties": false,
"description": "Swipe shortcuts (js/swipe.js). commit: releasing past the threshold performs the action; only for safe or reversible actions. reveal: the swipe uncovers a button that performs it; for writes to external systems.",
"properties": {
"mode": {
"type": "string",
"enum": [
"commit",
"reveal"
],
"default": "commit",
"description": "Gesture model (see description)."
},
"start": {
"type": "object",
"additionalProperties": false,
"required": [
"action",
"label"
],
"description": "Layer uncovered by dragging towards the end side (to the right in LTR): the side where text starts.",
"properties": {
"action": {
"type": "string",
"description": "Action key; must also be in `actions` (swipe is a shortcut, never the only way)."
},
"label": {
"type": "string",
"description": "Text on the uncovered layer, for example \"Übung fertig\"."
},
"icon": {
"type": "string",
"description": "Icon id on the layer."
},
"tone": {
"type": "string",
"enum": [
"neutral",
"success",
"accent",
"danger"
],
"default": "neutral",
"description": "Colour of the layer; turns solid when armed."
}
}
},
"end": {
"type": "object",
"additionalProperties": false,
"required": [
"action",
"label"
],
"description": "Layer uncovered by dragging towards the start side (to the left in LTR).",
"properties": {
"action": {
"type": "string",
"description": "Action key; must also be in `actions` (swipe is a shortcut, never the only way)."
},
"label": {
"type": "string",
"description": "Text on the uncovered layer, for example \"Übung fertig\"."
},
"icon": {
"type": "string",
"description": "Icon id on the layer."
},
"tone": {
"type": "string",
"enum": [
"neutral",
"success",
"accent",
"danger"
],
"default": "neutral",
"description": "Colour of the layer; turns solid when armed."
}
}
}
}
},
"progress": {
"type": "object",
"additionalProperties": false,
"required": [
"done",
"total"
],
"description": "Set segments under the title (exercise with several sets).",
"properties": {
"done": {
"type": "integer",
"minimum": 0,
"description": "Sets done."
},
"total": {
"type": "integer",
"minimum": 1,
"maximum": 12,
"description": "Sets in total."
}
}
},
"done": {
"type": "boolean",
"default": false,
"description": "Completed/inactive item: muted and struck through."
},
"actions": {
"type": "array",
"description": "Secondary actions for the overflow menu. The page handles clicks via `data-action`.",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"action",
"label"
],
"properties": {
"action": {
"type": "string",
"description": "Action key written to `data-action` (\"edit\", \"remove\")."
},
"label": {
"type": "string",
"description": "Menu text, a verb (\"Bearbeiten\")."
},
"icon": {
"type": "string",
"description": "Icon id."
},
"danger": {
"type": "boolean",
"default": false,
"description": "Destructive action (red text); still needs undo or confirm."
}
}
}
}
}
}
}
}
}Markup & states
Root: ul.list — usable without render() by writing the markup directly.
Attributes
| Attribute | Values | Default | Description |
|---|---|---|---|
role="list" | — | — | Keeps list semantics. |
aria-label | text | — | List name. |
data-flush | — | — | Borderless variant. |
Parts
| Part | Description |
|---|---|
.row | One item (li) with data-id. |
.row__main | Primary action — <a href> (or a div when static). Contains avatar and text. |
.row__text / .row__title / .row__meta | Title and the one secondary line, truncated with ellipsis. |
.row__aside | Belt, status badge (.badge[data-status]), overflow menu button. |
.menu | Popover with [data-action] items (see Menu). |
.row__swipe[data-side="start|end"] | Action layer uncovered by a swipe; holds the label (commit) or a button (reveal). aria-hidden: the menu is the accessible path. |
.row__quick | The visible quick-action button. |
.segments | Set progress under the title. |
.alert[data-status="neutral"][data-behavior="dismiss-once"] | The one-time swipe hint above the list. |
States
| State | Description |
|---|---|
.row[aria-current="true"] | Selected / "you" row: accent-subtle background and a 1 px accent outline. |
.row[data-state="done"] | Completed/inactive. |
.list[data-gestures="primary"] | Swipe is the main interaction (phones): peek on first open row, icon-only quick actions. |
.row[data-swipe-armed="start|end"] | Commit swipe past the threshold: the layer turns solid; releasing performs the action. |
.row[data-swipe-open="start|end"] | Reveal swipe: the row rests open, the button is reachable; Esc or a tap elsewhere closes it. |
Behaviour & events
Module: js/menu.js, js/swipe.js
Menus, swipes and the hint need mount(root) (data-behavior "menu", "swipe", "dismiss-once"). A commit swipe clicks the matching menu item, so menu, quick action and swipe all arrive as the same click; in screens the engine then applies the risk class of the action. Handle actions with one delegated listener: list.addEventListener("click", e => { const a = e.target.closest("[data-action]"); … a.dataset.action, a.closest(".row").dataset.id }).
Events
| Event | Detail | Description |
|---|---|---|
click on [data-action] | action key + row `data-id` | A menu item, the quick action, a revealed swipe button, or a commit swipe (which clicks the menu item). |
Accessibility
- The primary action is a real link; the overflow button has a specific label ("Weitere Aktionen für Lea Müller").
- Status and grade are text, not only colour.
- Menu keyboard support (arrows, Home/End, Esc) comes from the menu behaviour.
- Every swipe and quick action must also be in
actions(render throws otherwise), so keyboard, switch-access and screen-reader users reach it without gestures. - Vertical scrolling always wins over a swipe; swipes never start on buttons or form controls; with reduced motion the row does not animate.
Guidance
quickAction when it is the main job of the row (e.g. "Fertig").Related: Table · Menu · Belt badge · Status badge · Avatar