Tasks & Projects

A task tracker, assembled from the package rather than re-drawn: an editable task grid with a floating bulk-action bar, a detail view you can edit in place, an activity feed with @mentions, a drag-and-drop board, a sub-task checklist, cycle metrics and a roadmap.

Every demo below is wired to real state — sorting sorts, filters filter, a cell edit changes the task, selecting rows raises the bulk bar with the right count, and dragging a card to another column changes that task's status. Only one thing is drawn by hand, because the package ships nothing for it: the roadmap's quarter grid. Everything inside it is still made of StatusPill, AvatarGroup and Progress.

Task list

The screen most task apps are mostly made of — and with ten fields, filters that combine and people who fix a status or a due date in passing, it is a sheet rather than a list: DataGrid. The Filter and Sort menus take several conditions at once — status is any of Todo or In progress, due before Friday, assignees include Wei — and the status chips write the very filter the Filter menu lists. Their counts come from TanStack’s faceting, which counts under every filter except a column’s own, so each chip answers “how many will I get”. Cells edit in place: type over a title, pick a status or an assignee, change a due date, paste a column from a spreadsheet; Key and Comments are locked with meta.readOnly. Status, priority and labels are drawn with the same StatusPill as the detail view, and assignees with an AvatarGroup, through renderValue — so each cell keeps its editor. The key and title columns stay pinned only while the columns that scroll keep more than half of the grid; on a narrow screen they scroll with the rest. The card under the grid follows the focused row and draws what a cell cannot — time and activity — and ticking rows raises an ActionBar at the bottom of the window, centred on the grid.

Add row

16 of 16 tasks

DS-401Re-map muted-foreground so AA holds on cardsIn progressUrgent
Due Aug 28Updated 2comments

A floating bar beats a strip that appears above the grid: the strip pushes every row down, under the pointer that has just ticked one. The bar's contract is small: open follows the grid's selection, and onOpenChange(false) — the ✕, Escape, or an item that has done its job — clears it with table.resetRowSelection(). Choosing an ActionBarItem closes the bar, so Delete calls preventDefault() in onSelect to keep the selection while a ConfirmDialog asks — the same dialog the grid's own delete opens (Ctrl+Backspace, or the context menu): onRowsDelete returns a promise the dialog settles — true once the tasks are deleted, false when the reader keeps them — and the grid keeps the rows ticked until it does. Since 3.3.3 a false leaves them ticked and skips the “N rows deleted” toast; a promise that settles with no value still reads as deleted. The bar portals into the list's own element through portalContainer, so Tab reaches it right after the grid, and anchor="container" centres it on that element: the window's centre sits half a sidebar's width to one side of the grid's. Because the bar closes with the selection, the result — “3 tasks moved to Done” — goes to a role="status" line of its own. When an action or the ✕ closes the bar, the package hands focus back to wherever it came from on the way in.

Task detail

Description on the left, properties on the right — and every property is a real control. InlineEdit renames the task (Enter saves, Esc reverts). The description is a MarkdownEditor in its ghost variant: the document sits straight on the card and is always editable, the way Linear does it — tick the checklist, type / on an empty line for a block, @ to mention a teammate, or select a word for the formatting bubble. It stores Markdown, which is what the description column holds and what the Tasks MCP tool reads, and it lives behind its own entry, @comitor/ui/editor, with Tiptap as optional peers. Status and priority run through Combobox, assignees through MultiCombobox, the due date through DatePicker, and labels through TagsInput — free text rather than a fixed list, which is exactly where it parts from MultiCombobox. Each control sits in a FormField, which turns the caption into a real <label> and hands the control its id; TagsInputLabel does the same for its input. The date picker takes no locale and no labels: since 3.0.0 its format (MM/dd/yyyy here) and its screen-reader strings follow the language set once at the root.

DS-401In progressUrgentTokens
Blocks Mention 2 comments2 attachmentsUpdated

Activity and comments

What happened to a task and what people said about it, in one feed. Timeline draws it — a role="list" with a dot per event and a connector between them — but formats no dates: TimelineTime is a bare <time>, so each one hands the timestamp to RelativeTime through asChild. It has no notion of a log either: activeIndex paints dots completed, active or pending, which is a stepper's vocabulary, so a feed leaves it unset and every dot stays neutral. The composer is Mention: type @ and pick a teammate, and the name lands as one block that Backspace removes whole. Its value is the list of people mentioned — who to notify — with no parsing of the text. Post with ⌘↵ and the comment joins the feed.

Activity

DS-401
Priya Raman created the task
Priya Raman set the priority to Urgent
Priya Raman assigned Wei Chen
Wei Chen moved it from Todo to In progress
Wei Chen commented
Measured 4.1:1 on --card in light mode, so it misses AA by a hair. Dark mode is fine at 6.8:1.
Priya Raman commented
@Wei Chen can you re-run the gate once the token lands? I would like this in the next release.Notified Wei Chen
Commenting as Sofia Duarte

Kanban board

The package's Kanban — drag and drop by mouse, touch and keyboard. The board's data stays one flat task list: the columns are that list grouped by status, and onValueChange writes the column a card lands in back into its status, so the counts, the empty column and the line under the board all follow from one field. What goes inside a card is still the package: the priority is a StatusPill, labels fold into “+N” with BadgeOverflow, the faces are an AvatarGroup, the timestamp is a RelativeTime, and a column with nothing in it is an EmptyState. While a card is in the air, KanbanOverlay draws it above everything with the lifted shadow.

Todo

3
DS-403Medium

Document the labels mechanism end to end

docsi18n
1 comment1 attachment
DS-406High

Replace the hand-rolled tables in Patterns

docstech-debt
2 comments0 attachments
DS-408Medium

Name the column in drag-and-drop announcements

a11ykanbani18n
5 comments2 attachments

In progress

2
DS-401Urgent

Re-map muted-foreground so AA holds on cards

a11ycontrasttokens
2 comments2 attachments
DS-404High

Pin the first DataTable column below md

data-tableresponsivemobile
9 comments3 attachments

In review

2
DS-402High

Ship the async loading state for Combobox

comboboxasync
3 comments0 attachments
DS-405Urgent

Audit every focus ring against 1.4.11

a11yfocuswcag
4 comments1 attachment

Done

2
DS-410High

Publish 0.9.1 with the StatusPill border fix

release
7 comments1 attachment
DS-411Medium

Drop the English label sets from the docs

docsi18ncleanup
2 comments0 attachments

Drag a card by its grip — or Tab to the grip, press Space, and use the arrow keys.

Each card drags by a KanbanItemHandle, not by itself, and that is deliberate. The package's sensors start a drag on the very first mousedown — there is no distance threshold — so a card that is its own handle swallows every click inside it, and these cards hold focusable avatars. The grip is also the only element with touch-none — the handle sets it — so a finger on the card body still scrolls the column. From the keyboard: Tab to a grip, Space or Enter picks the card up, the arrow keys move it within and across columns, Space or Enter drops it, and Escape cancels — the Kanban hands back the value it had at pick-up, so the card really goes home. Every step is spoken through dnd-kit's live region. The default sentences come from the kanban namespace, in the provider's language, and name a column by its key (“in column in-review”) — so this board passes labels that say “In review” and name the task, which makes those four sentences the app's to translate; the keyboard instructions and role descriptions still follow the provider.

Sub-task checklist

Real Checkbox controls with real <label> elements, and a CircularProgress that counts sub-tasks instead of percent: value is how many are done, max how many there are. The ring prints 2/5, and getValueText gives a screen reader “2 of 5 sub-tasks done” where the default would say “40%”. At five of five the range turns text-success-ink — the ink step, never the fill token text-success.

Sub-tasks

DS-401 · Re-map muted-foreground

2/5

Sub-tasks roll up to the parent task's progress.

Cycle progress

Four StatCard tiles plus a completion bar. The numbers are derived from the same task array the list above starts from — hard-coded KPI literals go stale the first time the data moves. Note polarity on the overdue tile: rising overdue work is bad news no matter which way the arrow points, and the delta colour has to say so.

Scope

16

Aug 24 – Sep 6

In flight

6

+2since Monday

Completed

3

+3since Monday

Overdue

2

+1since Monday

Cycle 12

At risk
3 of 16 done · 19%

Roadmap timeline

Quarter-scale planning above the task level. The grid and the bar placement are the app's, but the bar itself is a Progress in a positioned wrapper, the health flag is a StatusPill with a shortLabel, and the crew is an AvatarGroup. The row scrolls inside its own container so a long horizon never scrolls the page. The grid stays hand-drawn on purpose: the package's Timeline puts events in order down a line — it does not place them on a time axis, so it cannot draw a quarter.

Q3 2026Q4 2026Q1 2027Q2 2027
Design system 2.0ON
72%
Shell and display axesRISK
38%
Public component APIPLAN
8%
+1

Best Practices

Do

  • Declare status and priority once as StatusConfig[] and let every surface read from it — grid options, chips, board, activity, detail.
  • Set the language once, at the root, with ComitorLocaleProvider, and pass labels only where the words are your domain's — unitLabel="tasks", “Assignees for DS-401”, column names in the board's announcements.
  • Keep a board's data in one list and derive the columns from each task's status, so a drop is a status change rather than a move between arrays.
  • Hold a grid's filters and sort in app state (state plus onColumnFiltersChange) as soon as anything outside the grid — chips, a URL, a saved view — has to read or write them.
  • Count chips against everything except the filter they control, so each one predicts its own result — faceting does exactly that.
  • Write two empty states — nothing yet, and nothing matched — and branch on the unfiltered set.
  • Give every task a short, stable key (DS-401), pin it and lock it with meta.readOnly in the grid, and use it wherever a task has to be named — grips, announcements, dialogs.
  • Confirm bulk deletes and single deletes with the same dialog, so both read the same weight.

Don't

  • Do not hand-roll a grid or a board. DataGrid already has filters and sorts that combine, editing in the cell, pinning, column visibility, the clipboard and virtualisation; Kanban has mouse, touch and keyboard drag with spoken feedback.
  • Do not build class names from data (bg-${tone}-300) — Tailwind scans statically and purges what it cannot see.
  • Do not colour text with a fill token. text-destructive fails contrast; text-destructive-ink is the text step.
  • Do not nest a ConfirmDialog inside a DropdownMenu or an ActionBar: the menu unmounts before the dialog opens, and the bar goes the moment its selection empties.
  • Do not make a card that holds anything clickable its own drag handle — the Kanban picks it up on the first mousedown and every click inside is lost. Give it a KanbanItemHandle.
  • Do not encode status or priority with colour alone — every chip carries its label too.
  • Do not caption a control with a styled <p>. FormField emits a real <label htmlFor>; a paragraph is not a label, however much it looks like one.
  • Do not swap a column for a custom cell to show a pill or an avatar — it loses the editor, paste and the keyboard. Keep the cell kind and draw its value with renderValue, as Status, Priority, Labels and Assignees do here.

Accessibility

  • Board cards move by keyboard as well as by mouse and touch: Tab to a grip, Space or Enter to pick the card up, the arrow keys to move it, Space or Enter to drop, Escape to put it back. The instructions are attached to every grip through aria-describedby in the language of the kanban namespace, and each step is announced in a live region — in this board's own sentences, which name the column and the task.
  • Every grip is named for its task (“Move DS-404”). The default, “Drag to move card”, is the same on every card, and a screen-reader user tabbing across the board could not tell them apart.
  • The task grid is a role="grid" with aria-rowcount: the arrow keys move between cells, Enter or typing starts an edit and Escape cancels it. Escape once more leaves the cell; from there Tab walks the row checkboxes — Space ticks one — and on out of the grid. The card under it follows the focused row, so the whole task is one arrow key away rather than behind a click.
  • The bulk bar is a role="toolbar" with one tab stop for its actions: the arrow keys move between them and Escape clears the selection. It is portalled into the list, not to the end of the document, so Tab reaches it right after the grid; its results are announced by a role="status" line, because the bar itself has gone by then. When an action or the ✕ closes it, the package hands focus back to the control it came from.
  • Checklist items use the package's Checkbox — a Radix <button role="checkbox"> carrying aria-checked, not an <input> — tied to a real <label htmlFor>, so clicking the text toggles it and a screen reader announces the state. The ring beside them is a progressbar whose aria-valuetext reads “2 of 5 sub-tasks done”.
  • Progress bars carry an aria-label with the percentage; the warning tone adds hazard stripes because gold and yellow are indistinguishable on three of the four palettes.
  • The comment box is a combobox: aria-expanded while suggestions are open, aria-activedescendant on the highlighted teammate, and a listbox named “Suggestions” by default. The activity feed is a list of list items, and every timestamp is a <time> with the exact date in its title.
  • Labels in TagsInput are removed from the keyboard with Backspace or Delete from the input — the ✕ buttons are left out of the tab order on purpose. Moving between labels with ← → is not announced yet, a known limit of the headless core.
  • KeyboardHint is aria-hidden — the glyphs read as noise, so the button it decorates has to name its own shortcut.
  • Avatars in a group are focusable buttons carrying the member's name, so the group is not a row of silent colour swatches.
  • The clear buttons of Combobox and MultiCombobox are icon-only, and their sr-only names default to the generic “Clear selection” and “Clear all selections”. This page names what each one clears with clearLabel — MultiCombobox needs it most, since clearable defaults to true there.

Built from

For the frame these screens sit in — sidebar, app header, workspace switcher, command palette — see the application pattern. For the same grid against a different domain, see CRM and Commerce.