Property pickers
StableThe Tasks tool’s pickers for status, priority, assignee, labels and estimate, as a chip, a row or an icon.
Each one pairs a trigger with a Picker in a popover and already knows its options and words: the trigger is named “Status: In progress”, and 1–9 choose at once (labels take several values instead). Three trigger variants fit a toolbar, a details panel or a dense row. They use the sample workspace vocabulary; the Notes tool’s TagPicker is built the same way from PickerButton.
On a task
import { useState } from 'react'import { labels as sampleLabels } from '@rchursin/product/data/sample'import { SampleBadge } from '@rchursin/product/shell/layout'import { AssigneePicker, EstimatePicker, LabelsPicker, PriorityPicker, StatusPicker, type Label, type PriorityId, type StatusId } from '@rchursin/product/tasks'
/** * A task's properties as chips. Each opens a filterable list: type to filter, ↑ ↓ and Enter, * or 1–9 to choose at once. Labels take several values and can create new ones. */export default function PropertyPickersTask() { const [status, setStatus] = useState<StatusId>('in_progress') const [priority, setPriority] = useState<PriorityId>(1) const [assignee, setAssignee] = useState<string | null>('me') const [labels, setLabels] = useState<string[]>(['skill', 'bug']) const [allLabels, setAllLabels] = useState<Label[]>(sampleLabels) const [estimate, setEstimate] = useState<number | null>(3)
function createLabel(name: string): Label { const label = { id: name.toLowerCase().replace(/\s+/g, '-'), name } setAllLabels((all) => (all.some((l) => l.id === label.id) ? all : [...all, label])) return label }
return ( <div className="flex w-full max-w-lg flex-col gap-3"> <div className="flex items-center justify-between gap-3"> <span className="font-mono text-[11px] text-fg-3">RC-128</span> <SampleBadge /> </div> <p className="text-[15px] font-semibold tracking-[-0.01em] text-balance text-fg">Fix test-fixer: fails more than half its runs</p> <div className="flex flex-wrap gap-1.5"> <StatusPicker value={status} onChange={setStatus} /> <PriorityPicker value={priority} onChange={setPriority} /> <AssigneePicker value={assignee} onChange={setAssignee} /> <LabelsPicker value={labels} onChange={setLabels} labels={allLabels} onCreate={createLabel} /> <EstimatePicker value={estimate} onChange={setEstimate} /> </div> </div> )}Usage
import { StatusPicker, PriorityPicker, AssigneePicker, LabelsPicker, EstimatePicker } from '@rchursin/product/tasks'<StatusPicker value={status} onChange={setStatus} /><PriorityPicker value={priority} onChange={setPriority} variant="icon" />Examples
Property and icon variants
variant="property" for a details panel; variant="icon" for a dense row, with the value as its name and tooltip.
import { useState } from 'react'import { labels } from '@rchursin/product/data/sample'import { AssigneePicker, EstimatePicker, LabelsPicker, PriorityPicker, StatusPicker, type PriorityId, type StatusId } from '@rchursin/product/tasks'
/** * variant="property" fills a row of a details panel; variant="icon" is a 24px square for dense * lists, with the value as its name and tooltip. */export default function PropertyPickersVariants() { const [status, setStatus] = useState<StatusId>('in_review') const [priority, setPriority] = useState<PriorityId>(2) const [assignee, setAssignee] = useState<string | null>('tp') const [taskLabels, setTaskLabels] = useState<string[]>(['cost', 'agent']) const [estimate, setEstimate] = useState<number | null>(2)
const rows = [ ['Status', <StatusPicker variant="property" value={status} onChange={setStatus} />], ['Priority', <PriorityPicker variant="property" value={priority} onChange={setPriority} />], ['Assignee', <AssigneePicker variant="property" value={assignee} onChange={setAssignee} />], ['Labels', <LabelsPicker variant="property" value={taskLabels} onChange={setTaskLabels} labels={labels} />],Note tags
The Notes tool’s TagPicker: PickerButton with multiple and onCreate. Type a new word and press Enter to create a tag.
import { useState } from 'react'import { TagPicker } from '@rchursin/product/notes'import { Chip } from '@rchursin/product/ui'
const allTags = ['agents', 'budget', 'meeting', 'reading', 'release', 'skills']
/** * The Notes tool builds its tag picker from the same parts: PickerButton with `multiple` and * `onCreate`. Type a new word and press Enter to create a tag. */export default function PropertyPickersNoteTags() { const [tags, setTags] = useState(['meeting', 'agents'])
return ( <div className="flex flex-wrap items-center justify-center gap-1.5"> {tags.map((t) => ( <Chip key={t} size="xs" variant="muted" onRemove={() => setTags(tags.filter((x) => x !== t))} removeLabel={`Remove tag ${t}`}> #{t} </Chip> ))} <TagPicker tags={tags} allTags={allTags} onChange={setTags} /> </div> )}API
Props of all five pickers
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | V | – | StatusId for status, PriorityId (0 none, 1 urgent … 4 low) for priority, a person id or null for assignee, label ids for labels, points or null for estimate. |
onChangerequired | (value: V) => void | – | Called with the new value. Single pickers close after a choice; Labels stays open. |
variant | 'chip' | 'icon' | 'property' | 'chip' | chip: a 28 pixel pearl button for toolbars. property: a full-width row for a details panel. icon: a 24 pixel square with a tooltip. |
className | string | – | Extra classes on the trigger. |
LabelsPicker props
| Prop | Type | Default | Description |
|---|---|---|---|
labelsrequired | Label[] | – | Every label to choose from, as { id, name }. |
onCreate | (name: string) => Label | – | Offers “Create …” for a new name. Return the new label and it is added to the value. |
TagPicker props (Notes)
| Prop | Type | Default | Description |
|---|---|---|---|
tagsrequired | string[] | – | The note’s tags. |
allTagsrequired | string[] | – | Every tag in use, offered as options. |
onChangerequired | (tags: string[]) => void | – | Called with the new tags. A created tag loses a leading #, is lower-cased and has its spaces turned into dashes. |
className | string | – | Extra classes on the trigger. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| EnterSpace | On a trigger: opens its list with focus in the search. |
| Down arrowUp arrow | Moves the highlight. |
| Enter | Chooses the highlighted option, or toggles a label. |
| 1–9 | Status, priority, assignee and estimate: chooses that option at once. |
| Esc | Closes the list; focus returns to the trigger. |
- Each trigger is a button named by its property and value: “Status: In progress”, “Assignee: none”. The icon variant shows the same words as a tooltip.
- The list remounts on every open, so the search starts empty with the first option highlighted.
- Icons in the lists are decorative; the option text carries the meaning, and the current choice has a check mark.
- Assignees can be found by name, initials, role or “me”; you come first, right after “No assignee”.
Guidelines
Do
- Use chips in toolbars and create dialogs, property rows in a details panel, and icons in dense lists.
- Keep the same order everywhere: status, priority, assignee, labels, estimate.
Don’t
- Don’t use the icon variant where people need to read the value at a glance; it shows only the icon.
- Don’t mix variants in one row.
In use
Related
- PickerA filterable list of options, on its own or in a popover: type to filter, arrows and Enter to choose, or 1–9 to pick at once.
- Status & priorityTask glyphs told apart by shape: a square that fills as work moves on, priority bars, assignee, labels and estimate.
- Due dateRelative due dates for task rows (“Today”, “Fri”, “Oct 30”) with overdue shown by shape, and a picker with quick choices.
- Task detailEverything about one task: an editable title and description, and each property as a picker button.
- Note editorA note’s title, a Markdown block body, tags, word count and an honest save status; it saves 800 ms after typing stops.
Source
The component’s own files, exactly as they run on this site.
/** * Property pickers for tasks. Each is a button that opens a filterable list (combobox + * listbox): type to filter, ↑ ↓ to move, Enter to choose, 1–9 for the first nine, Esc to close. * * <StatusPicker value={status} onChange={setStatus} /> chip (default) * <PriorityPicker value={p} onChange={setP} variant="icon" /> icon only, 24px * <DueDatePicker value="2026-10-14" onChange={setDue} /> quick choices + the month grid * * The due date uses the Calendar kit's CalendarPanel for "any other day", so dates are picked the * same way everywhere in the app (value: ISO date | null). */import { useRef, useState, type KeyboardEvent, type ReactNode } from "react"import { cx } from "../../components/cx"import { ME, people, priorities, statuses, type PriorityId, type StatusId } from "../data/sample"import { Icon } from "../icons/Icon"import { addDays, dtf, formatDayLong, startOfDay, startOfWeek, toISODate } from "../lib/dates"import { Avatar } from "../ui/misc"import { Picker, type PickerOption } from "../ui/picker"import { Popover, usePopover } from "../ui/popover"import { CalendarPanel } from "../calendar/DatePicker"