lab · exploring

Rows

A spike for the selectable row list a store's orders and the webmail list share: one column template that every row subgrids (Table-like alignment), free markup per row, keyboard selection with Shift ranges, a stretched primary link, hover actions, sticky groups and a two-line fold keyed on the list's own width.

frameworkssvelte
Exploringgrid-listtablelistcheckboxbuttonbadgeavatar
scenarioOrders

Order number, status, total and date line up across every row and section (one template, every row a subgrid of it) while the customer cell holds free content: an avatar, the name over the email. Unpaid orders read strong. Section titles carry a stamp and, while packing or shipping, a Print labels action that counts the selection. Hover a row for its checkbox; once one is checked, all show. Focus a row: ↑/↓ or J/K move, X or Space select, Shift extends, ⌘A all, Esc clears, Enter opens. Drag the frame's corner under 640px: each row folds to a two-line card. Each fulfilment section is its own drop list: drag an order under another section's title and it takes that status (the badge follows); Alt+↑/↓ crosses into the next section at a section's end. Ungrouped, the same drag only reorders.

scenarioInbox

The email lab's row on the same prototype: unread rows strong, day groups, and the date swapping for archive/delete/read on hover or focus. Both sit in one grid area through visibility, so the row never re-flows as the pointer crosses it. Narrow the frame for the split-pane fold.

scenarioDrag and drop (board)

Three lists sharing one drag context over @dnd-kit/dom. Drag a card from anywhere on it: reorder within a column or move it to another, which changes its status; Shipped starts empty and still takes a drop. Keyboard: focus a card, Alt+↑/↓ moves it within the column, Alt+←/→ to the next column (focus follows it); X, arrows and Enter keep selecting, moving focus and opening. Hover a card and its avatar turns into the checkbox — no empty column at the start.

To pack 4
Ready to ship 2
Shipped 0

scenarioPhoto library

Dense square tiles by day with selectionBehavior="replace": a click selects only that photo, ⌘/Ctrl-click adds or removes one, Shift-click takes the range from the last pick. A selected photo shows a check badge, no checkbox. Arrows move the selection with the focus (Shift extends it, ⌘+arrow moves the focus alone). Hold a photo for half a second (or tap it on a touch screen): the list switches to toggling, every photo shows an empty ring and clicks add or remove, until the selection is empty again.

scenarioSettings

look="inset": each group is a rounded card on the surface with its title above it, outside the card. Rules separate the rows from the label on (past the icon stamp — the second column by default, insetStart names another) to the card's edge; a card's last row has none. Value and chevron are row content. Switch the look to compare; turn groups off for one card.

scenarioFindings so far
  • Not List, not Table. List is a static key/value and Banner-row list; ListBox picks options;Table is column-defined data (TanStack, headers, sorting, inline cells — a spreadsheet-like task list is a grouped Table). This is the third thing: a collection of entities you select, act on and open, whose row content is the consumer's markup.
  • The issue-tracker reference does exactly this. Each issue row is a grid whose child is grid-template-columns: subgrid of one named-line template ([checkbox] [priority] [identifier] [status] [title] [createdAt]), with cells tagged by column name. It flagsfirst-selected / last-selected (the selection reads as one block: see Plain), first-in-group,keyboard-active apart from hover, and renders ~20 rows at a time (virtualized).
  • Placement as vars, not recipe rules. A cell's bag carries --cw (wide column), --cn /--rn (narrow column / row). The recipe has one rule per mode and never names a consumer's cell, so any column set folds. Named lines (the reference's way) would let the narrow placements readtitle instead of 2 / -1.
  • Fold is a container query on the list. Fixed at 640px in the spike: Panda can't take a breakpoint from a prop, so a realfoldBelow needs a few presets (sm/md/lg) or a ResizeObserver flag (with an SSR first paint question).
  • Row element. The reference makes the whole row an<a> and fakes its inner controls with divs, since buttons can't nest in links. The spike keeps a focusablerole="row" and a stretched primary link (Enter clicks it), so the checkbox, star and status stay real buttons.
  • Selection belongs here, the bar doesn't. The state is the bulk-actions lab's row-selection.ts plus Space, Home/End, Esc and Enter; the BulkBar reads it throughidSelection, so wiring the two is one line.
  • Drag and drop over @dnd-kit/dom 0.5.1 works.@pindoba/core-drag (the spike's drag.ts) is the only package importing dnd-kit, exposing sortableItem,dropZone and one onMove({ id, from, to }). Checked in the browser: pointer reorder within a list, moves between lists, a drop into an empty list, keyboard pick-up / move / drop / Escape, no duplicate rows, and the DOM matching state after each drop (dnd-kit's optimistic sorting and Svelte's keyed list agree).
  • No grip, no checkbox column. A pointer or a finger drags the card from anywhere; dnd-kit's pointer sensor waits for a few pixels, so a click still opens the order and a checkbox click still selects. It refuses to start on interactive elements by default, and the row's stretched link covers the card — so elements markeddata-drag-through pass the drag on, while real controls never start one. Links and images lose their native drag in draggable rows. The checkbox covers the avatar (selectOver) instead of waiting in an empty column.
  • The keyboard path is ours. dnd-kit's keyboard sensor would claim Space / Enter / arrows, which the row already uses, and it nudges 10px per ←/→ (≈18 presses per column). Rows take Alt+↑/↓ (one slot) and Alt+←/→ (the next list, same row number), focus follows the row into the other list, and our own live region speaks "Moved order #3180 to position 1 in Ready to ship." The row advertises it with aria-keyshortcuts. With dnd-kit's accessibility plugin removed it adds no attributes at all: the row keepsrole="row", aria-selected and its tabindex.
  • Costs and fixes. 36 KB gzip (108 KB min) for manager + sortable + plugins — load it only for lists that enable drag. A liftedsubgrid row loses its parent's tracks (squeezed cells), socore-drag freezes the parent's resolved tracks on the row at pick-up and clears them on drop. A drop's list and index come from onedestination(), so a drop on an empty list lands there.
  • Tiles are a layout, not a look.layout="grid" swaps the column template forrepeat(auto-fill, minmax(var(--gl-tile), 1fr)); a tile is one gridcell holding the row snippet's free markup, so the role tree stays a grid. No header and no fold (a tile has no cells to move). The looks carry over where they mean something — plain and cards fill tiles, divided draws as plain — and a selected run never joins: every tile is its own block. ↑/↓ read the tiles' measured boxes (a pure core helper takes the rects, the framework measures), so wraps, gaps and group titles between lines just work.
  • Two selection behaviours. toggle (the default) is the checkbox model every list above uses;replace is the file-manager / photo-library model, where a plain click or arrow replaces the selection. Touch has no modifier keys, so a tap or a long press flips a replace list to toggling until the selection empties — the long-press timer lives in core so every framework shares it.
  • The inset rule is placed by the grid. The separator is the row's ::after, absolutely positioned but with a grid-column from the inset column to the last line, so its containing block starts exactly where the leading cell ends — whatever its width — and a negative inline end carries it through the row's padding to the card's edge.
  • Groups as drop lists. drag.groups makes each group of a grouped list its own list in the drag context: the group's title row heads a section (data-drag-section), the pointer picks the section by height, and onMovereports the group as to.list (its position within the group, counting earlier pages) with the list's id asto.zone, so the consumer changes the item's group. One drop zone, no wrapper per group: the rows stay direct subgrid children and every look's corner rules are untouched.
  • Tiles drop in two dimensions. Alayout: "grid" zone picks the line of tiles at the pointer's height, then the nearest edge in it; the marker stands upright halfway between two tiles (--drop-x,--drop-h), and the lifted tile glides to the cell it will take. By keyboard, Alt+←/→ steps along the list (reading on into the next one) and Alt+↑/↓ takes the place of the tile above or below, across group titles.
  • Pointer drags speak too. The context's own live region says "Picked up …", "Moved … to position 2 in …" once the pointer rests on a new slot for 500ms (only a real change, never the slot it started in), and "Dropped …" or "Cancelled. … is back at …" — the keyboard path's words, from labels, never dnd-kit's accessibility plugin.
  • Open. ARIA: grid wants arrow keys to move into cells (←/→ across the row's controls) and in-row controls attabindex=-1; today they are still in the Tab order. Right-click context menu; virtualization; a sticky column header; drop-target highlight. An empty group can't take a drop (a group exists only while it has items) and a collapsed one takes none. Each drag context has its own live region, so a page with several contexts has several regions. dnd-kit's auto-scroll moves a scrolling frame under a pointer resting near its edge, so the slot keeps changing there and no "moved to" is spoken until it stops.