List

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

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
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üller
    6. Kup
  • NKNoah Keller
    7. 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

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.

FieldTypeRequiredDefaultDescription
labelstringoptional—Accessible name of the list (aria-label), e.g. "Offene Anmeldungen".
flushbooleanoptionalfalseNo 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.
swipeHintobjectoptional—One-time hint explaining the swipe gestures (athlete app pattern). Shown until dismissed; remembered per device under id.
swipeHint.idstringrequired—Storage key for "Nicht mehr anzeigen", e.g. "training-swipe".
pattern ^[a-z0-9-]+$
swipeHint.titlestringrequired—Hint title.
swipeHint.itemsarray of stringrequired—One line per gesture.
min 1 items · max 4 items
swipeHint.dismissLabelstringoptional"Nicht mehr anzeigen"Label of the dismiss button.
itemsarray of objectsrequired—Rows in display order.
items[].idstringrequired—Stable id of the thing (member id, registration id). Used for data-id and menu ids.
pattern ^[A-Za-z0-9_-]+$
items[].titlestringrequired—Main text (name, title).
min 1 chars
items[].metastringoptional—One secondary line ("Kinder 7–10 · seit 2024").
items[].hrefstringoptional—Primary action: link to the detail page. Without href the row is static.
items[].avatarstringoptional—Initials shown in an avatar circle ("LM").
max 3 chars
items[].beltobjectoptional—Optional grade shown in the aside (see Belt badge).
items[].belt.labelstringrequired—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.shortbooleanoptionalfalseCompact form for dense tables: the label should then be the short grade ("6. Kup"); the full name goes into title.
items[].belt.titlestringoptional—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[].badgeobjectoptional—Optional status badge in the aside.
items[].badge.labelstringrequired—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[].currentbooleanoptionalfalseSelected row (e.g. shown in the detail pane next to the list).
items[].quickActionobjectoptional—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.actionstringrequired—Action key (also in actions).
items[].quickAction.labelstringrequired—Button text, a verb. Becomes the accessible name when the button is icon-only.
items[].quickAction.iconstringoptional—Icon; required for gestures: primary lists, where the button is icon-only on phones.
items[].swipeobjectoptional—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.startobjectoptional—Layer uncovered by dragging towards the end side (to the right in LTR): the side where text starts.
items[].swipe.start.actionstringrequired—Action key; must also be in actions (swipe is a shortcut, never the only way).
items[].swipe.start.labelstringrequired—Text on the uncovered layer, for example "Übung fertig".
items[].swipe.start.iconstringoptional—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.endobjectoptional—Layer uncovered by dragging towards the start side (to the left in LTR).
items[].swipe.end.actionstringrequired—Action key; must also be in actions (swipe is a shortcut, never the only way).
items[].swipe.end.labelstringrequired—Text on the uncovered layer, for example "Übung fertig".
items[].swipe.end.iconstringoptional—Icon id on the layer.
items[].swipe.end.tone"neutral" | "success" | "accent" | "danger"optional"neutral"Colour of the layer; turns solid when armed.
items[].progressobjectoptional—Set segments under the title (exercise with several sets).
items[].progress.doneintegerrequired—Sets done.
≥ 0
items[].progress.totalintegerrequired—Sets in total.
≥ 1 · ≤ 12
items[].donebooleanoptionalfalseCompleted/inactive item: muted and struck through.
items[].actionsarray of objectsoptional—Secondary actions for the overflow menu. The page handles clicks via data-action.
items[].actions[].actionstringrequired—Action key written to data-action ("edit", "remove").
items[].actions[].labelstringrequired—Menu text, a verb ("Bearbeiten").
items[].actions[].iconstringoptional—Icon id.
items[].actions[].dangerbooleanoptionalfalseDestructive 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

AttributeValuesDefaultDescription
role="list"——Keeps list semantics.
aria-labeltext—List name.
data-flush——Borderless variant.

Parts

PartDescription
.rowOne item (li) with data-id.
.row__mainPrimary action — <a href> (or a div when static). Contains avatar and text.
.row__text / .row__title / .row__metaTitle and the one secondary line, truncated with ellipsis.
.row__asideBelt, status badge (.badge[data-status]), overflow menu button.
.menuPopover 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__quickThe visible quick-action button.
.segmentsSet progress under the title.
.alert[data-status="neutral"][data-behavior="dismiss-once"]The one-time swipe hint above the list.

States

StateDescription
.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

EventDetailDescription
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

DoKeep rows scannable: title, one meta line, one status.
DoSort by what the user needs next (e.g. oldest open registration first).
Don'tNo inline buttons per row besides the menu, except one quickAction when it is the main job of the row (e.g. "Fertig").
Don'tNever commit irreversible actions or CRM writes by swipe; use reveal mode.
Don'tDon't put form controls inside reading lists (audit F2).

Related: Table · Menu · Belt badge · Status badge · Avatar