To-do list

A personal checklist you tick off, rename in place, and add to with Enter.

Installation
$Terminal
npx shadcn@latest add https://sdk-components.thesqd.com/r/todo-list.json
Usage
Give it items, or none, and listen to onChange — ticking, renaming, adding, removing and folding are all internal.
TSImport
import { TodoList, type TodoItem } from "@/components/blocks/todo-list";
TSExample
"use client";

import { TodoList } from "@/components/blocks/todo-list";

export function Example() {
  return (
    <TodoList
      title="Tasks"
      defaultItems={[{ id: "1", label: "Call the venue" }]}
      onChange={(items) => console.log(items)}
    />
  );
}
Composition
Anatomy of the card, its header roll-up, and one row.
TodoList                         (single component — pass everything as props)
└── Card type="toolbar" collapsible
    ├── band                     (records-toolbar — click to fold the list)
    │   ├── icon chip            (records-database-icon; duotone ListChecks by default)
    │   ├── title
    │   ├── CircularProgress     (done / total ring — hidden until the first tick)
    │   ├── Badge                ("1/5" NumberReel — coral < 40%, amber 40%+,
    │   │                         green at 100%)
    │   └── CaretDown            (flips when open)
    └── CollapsibleContent
        ├── emptyState           (no items — Empty with Inky by default)
        ├── item[]
        │   ├── Checkbox         (round, sm — strikes the label when ticked;
        │   │                     Check / Minus icons when readOnly)
        │   ├── label            (one of three:)
        │   │   ├── <a>          (item has an href — the label is the link)
        │   │   ├── InvisibleInput (rename in place; Backspace on empty removes)
        │   │   └── <span>       (readOnly)
        │   ├── Badge[]          (item.badge — one or a list, in order, right-aligned)
        │   └── Button × ghost   (remove, shown on hover/focus)
        └── add row
            ├── Plus
            └── InvisibleInput   (Enter or blur adds)
Default
New tasks open their row and rise in out of a blur. Tick one and the strike draws across it; an item with an `href` is a link — click the label to open it, and `badge` adds right-aligned metadata; the ring and the count in the header follow. Click any label to rename it, press Enter in the add row to append, and backspace a task empty (or hit its ×) to remove it. The header folds the list away.
Label header
`header="label"` swaps the shaded band for the project viewer's quiet header — a small muted name beside a brand-coloured icon, no fill and no divider. It still folds the list.
Fill
`fill` sizes the card to the space it's given instead of to its tasks — for a dashboard widget or grid cell the reader sized. The list scrolls inside, and folding still shrinks it to the header.
Empty
With no items Inky shows up with a nudge to add one, and steps aside once the first task lands. Replace it with `emptyState`, or pass `null` for just the add row.

Nothing to do

Add a task below to get started.

Custom empty state
`empty` tunes the default without rebuilding it: give Inky an emotion or any of his props — a costume, a preset, a shape — and change the title, description or add an action.

Nothing left to plan

Enjoy the party — or add one more thing.

Controlled
Pass `items` and `onChange` to own the state — every edit comes back as the full new list, ready to save.

4 left — state lives in the parent.

Read only
`readOnly` is display only — no ticking, renaming, adding or removing. The checkboxes become Phosphor status icons (a check mark when done, a horizontal line when not, cross-fading between the two), and links still open.
Ref API
Pass a `ref` to get `TodoListHandle` — add, update (any field of an item), toggle, remove, clearCompleted, setItems, open/close and focusAdd. Calls go through `onChange` like any edit, so a controlled list still owns its state, and `onItemChange` reports each one per item. A label changed from code swaps in with a blur instead of snapping. Here the list is `readOnly`, so the buttons are the only way to change it — `readOnly` locks the person out, not the ref.

onItemChange

Use the buttons or the list.

API Reference
Props for the list, the shape of one item, and the ref handle.

TodoList

PropTypeDefaultDescription
titleReactNode"To do"Header label.
header"band" | "label""band"`band` is the shaded toolbar band; `label` is the project viewer's quiet header — a small muted name beside a brand-coloured icon. Both fold the list.
fillbooleanfalseFill the height and width the parent gives it — a grid cell, a dashboard widget — instead of sizing to the tasks; the list scrolls inside. Folded, it still shrinks to its header.
iconComponentType<{ className?: string }> | falseListChecks (duotone)Icon component for the chip beside the title. `false` hides it.
itemsTodoItem[]—Controlled items — pair with `onChange`.
defaultItemsTodoItem[][]Starting items when uncontrolled.
onChange(items: TodoItem[]) => void—Fires with the whole new list on every tick, rename, add and remove.
onItemChange(change: TodoChange) => void—Fires once per item that changed — add, update or remove — with the item (and `previous` on update), so a save can PATCH one record.
emptyPick<EmptyProps, "inky" | "title" | "description" | "action">—Tunes the default empty state — Inky as an emotion or any Inky props (`{ costume, preset, shape, … }`), plus title, description and an action. Unset fields keep their defaults.
emptyStateReactNode—Replaces the empty state outright with your own node, or `null` for none. Wins over `empty`.
placeholderstring"Add a task"Placeholder of the add row.
readOnlybooleanfalseDisplay only — no ticking, renaming, adding or removing. Links still work, and the ref API still changes it.
defaultOpenbooleantrueWhether the list starts unfolded.
collapsiblebooleantrueWhether the header folds the list away (caret on the right). `false` draws a plain header that always shows the list — for a board where folding is not wanted.
iconTone"muted" | "coral"—The header icon's tone, passed to the Card: `coral` while something waits, `muted` for an empty list.
soundbooleantrueCuelume cues on ticks — `select` on check, `error` on uncheck, `success` instead of `select` when the tick finishes the list. Ref API ticks play them too.
classNamestring—Override the card's width or layout classes.
refRef<TodoListHandle>—Drive the list from code — see TodoListHandle.

TodoItem

PropTypeDefaultDescription
idstring—Stable key.
labelstring—The task text.
donebooleanfalseTicked.
hrefstring—Makes the label a link. New-tab links get the global ↗ after the text. Linked labels aren't renamed inline — use the ref's `update`.
badgeTodoBadge | TodoBadge[]—Extra metadata — an owner, a due date, a department — as small badges right-aligned on the row. One badge or a list, drawn in the order given. `TodoBadge` is `{ label: ReactNode; color?: BadgeColor }`; `muted` by default.
targetstring"_blank" for http(s), else same tabLink target.

TodoListHandle

PropTypeDefaultDescription
getItems() => TodoItem[]—The current list.
setItems(items: TodoItem[]) => void—Replaces the whole list.
add(item: Omit<TodoItem, "id"> & { id?: string }) => string—Appends an item; returns its id.
update(id: string, patch: Partial<TodoItem> | (item: TodoItem) => Partial<TodoItem>) => void—Changes any field of one item — label, done, href, target. A function patch derives from the current item.
toggle(id: string, done?: boolean) => void—Flips `done`, or sets it when passed.
remove(id: string) => void—Removes one item.
clearCompleted() => void—Removes every ticked item.
open / close() => void—Unfolds or folds the list.
focusAdd() => void—Opens the list and puts the cursor in the add row.

TodoChange

PropTypeDefaultDescription
type"add" | "update" | "remove"—What happened.
itemTodoItem—The item after the change (or the removed item).
previousTodoItem—Update only — the item before the change.