Skip to content

HabList ​

Rows of one kind of thing, inside one shared surface. A list, not a table.

  • TranscriptionEnabled for 4 languages
  • RedactionNames and addresses
  • RetentionRecordings are deleted automatically90 days
    Policy
One continuous surface of rows you operate — controls live in each row
vue
<HabList label="Workspace features" bordered>
	<HabListItem
		label="Transcription"
		secondary="Enabled for 4 languages"
		:icon="mdiCogOutline"
	>
		<template #trailing>
			<HabSwitch
				v-model="on"
				aria-label="Transcription"
			/>
		</template>
	</HabListItem>
</HabList>

No column alignment, no header, no sort. Reach for HabDataTable the moment a second field needs to line up down the page — and for HabListRow when each row is a thing you open rather than operate.

HabListItem's leading slot is a bare icon or a small node, deliberately lighter than HabListRow's tile: these rows sit shoulder to shoulder, and a tile on each would stripe the panel.

Two forms ​

Slot children — HabListItem elements — for full control, which is what rows carrying their own controls need.

items for data-driven rows. Dragging requires this form: a row can only be moved if the list knows where it currently sits, and a slot child has no index to offer. Rather than guess at VNode order — which stops matching the DOM the moment the consumer reorders — the component warns and does nothing.

active is not selected ​

Both tint a row, and they answer different questions:

  • active is where the user is — the current route or target. A neutral fill.
  • selected is what they have chosen — multi-select, a picker. An accent tint.

A row can be both, and selected wins, because it is the more specific claim about that row.

Density ​

default is 44px, compact is 36px, comfortable is 52px — the same ladder as HabAccordion and HabDataTable, so a list, a table and an accordion stack without the page reading as three different rhythms. Set it on the list; a single row can override it.

Reach for comfortable when the rows carry two lines — a name over a note. That content measures 35px, which sits in the 44px default with 4.5px of air, a third of what a one-line row gets. It is not a roomier-by-taste setting: a one-line row at comfortable just looks lost.

Dragging, and reordering ​

Drag a row, or focus one and press Alt+Up / Alt+Down

draggable turns on native HTML5 drag so a row can leave the list — a drop target elsewhere reads dragType off the dataTransfer, alongside a text/plain flavour so a target that does not know our type still receives something meaningful.

reorderable adds shuffling within the list, on top of that. Without it rows drag out but do not move.

The list reorders nothing itself. reorder reports { from, to } and the consumer applies it to their own array — the same shape as everything else here.

It works from the keyboard ​

Native HTML5 drag is pointer-only. The design contract's reorderable list therefore cannot be reordered without a mouse at all, which is why this one adds Alt+ArrowUp / Alt+ArrowDown on a focused row.

Alt because the rows are buttons and bare arrows belong to the browser's own caret movement; not Ctrl or Cmd because both are already page or window navigation.

Every move is announced in a live region. A reorder is otherwise invisible to a screen reader: the DOM changes and nothing says what happened, so the user has to re-read the list to find out whether their keypress did anything.

The grip only appears when reordering is on — a grip promises the row can be put in a new position, so a drag-out-only list must not show one.

Accessibility ​

  • label names the list. Without it the list is announced as a bare row count with nothing to say what it holds, so the component warns.
  • Rows are real <li> elements inside a <ul>, so the list's length is announced.
  • active sets aria-current; selected on an interactive row sets aria-pressed.
  • Not asserted in the specs: the pointer drag itself. happy-dom cannot produce a real drag sequence, and a fake one would assert nothing about what a user gets — see architecture.md. The keyboard path is asserted, which is the half that a test can honestly cover.

API ​

Props ​

PropTypeDefaultDescription
borderedbooleanfalseWraps the list in a card border. Use it when not already inside a HabPanel.
density"default" | "comfortable" | "compact""default"—
dividedbooleantrueHairlines between rows.
draggablebooleanfalseTurns on native HTML5 drag, so a row can be dragged out of the list onto a target elsewhere. Requires items.
dragTypestring"application/x-habitus-list-item"The dataTransfer type an external drop target reads.
emptyLabelstringundefinedShown in place of the rows when there are none. No default: pass it, or fill the empty slot, or the empty list renders a blank band.
itemsHabListEntry[]undefinedData-driven rows. Omit and pass HabListItem children for full control. Dragging needs this form. A row can only be moved if the list knows where it currently sits, and slot children have no index to give it.
labelstringundefinedAccessible name for the list. A list of rows with no name is announced as a bare count.
modelValuestringundefinedThe id of the selected row, matched against each entry's id.
reorderablebooleanfalseAllows rows to be reordered within the list, on top of draggable. Without it rows drag out but do not shuffle.

Events ​

EventPayloadDescription
dragItemStartpayload: { item: HabListEntry; index: number; event: DragEvent; }—
reorderpayload: { from: number; to: number; }—
update:modelValueid: string—

Slots ​

SlotBindingsDescription
defaultanyHabListItem children, for full control. Ignored when items is given.
emptyanyShown instead of the rows when there are none. Wins over emptyLabel.

HabListItem ​

Props ​

PropTypeDefaultDescription
activebooleanfalseThe current route or target. A neutral fill. Distinct from selected: active is where the user is, selected is what they have chosen. A row can be both.
density"default" | "comfortable" | "compact"undefinedOverrides the parent list's density for this row alone.
disabledbooleanfalse—
dragHandlebooleanundefinedShows the drag grip. The parent list sets this while reordering is on.
iconstringundefinedSVG path data (mdiPencil from @mdi/js), at the start of the row.
interactivebooleanfalseMakes the row a real button. Omit for a static row.
labelstringundefinedPrimary row text.
metastringundefinedShort right-aligned value before the trailing slot — count, size, date.
secondarystringundefinedSecond line — path, description, timestamp. One line, truncated.
selectedbooleanfalseChosen by the user — multi-select, a picker. An accent tint.

Slots ​

SlotBindingsDescription
defaultanyPrimary text, in place of label.
leadinganyAnything at the start — a HabAvatar, a HabCheckbox, a color dot. Wins over icon.
trailinganyAnything at the end — an icon button, a HabSwitch, a HabChip, a chevron.

Proprietary — internal use only.