Task Inbox
betaOne calm list of tasks, grouped into Needs you, Running, and Done, with filter tabs and live counts. Use it for approvals, requests, and background jobs. A row opens to its last steps and the action its state needs.
Built from Accordion, Badge, Button, Card, Motion Timing, Skeleton, Spinner, Status Text, Tabs, and Timeline.
Preview
3 need you, 1 running.
own words
1 needs you, 0 running.
loading
Loading tasks
empty
0 need you, 0 running.
Requests sent to you show up here.
narrow container
2 need you, 1 running.
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the block. It also adds every component it uses. Its own files go in your
componentsfolder, and they are yours to edit.npx shadcn@latest add @txbrendan/task-inbox
Use it for
- Show people the work that waits on them or runs for them, sorted by who has to act next: expense reports, refunds, time off, imports, exports.
- Each task is one row: what it is, where it stands, how long it has taken, and a small badge, like who asked or the team.
- Tasks that wait for a yes or no and tasks that stopped both sit under Needs you, where people look first.
- Give a failed task a short reason, like "card declined", so people can tell whether Retry will help.
- Pass approveLabel and rejectLabel when Approve and Reject are not the right words, like Sign off and Send back.
- Pass approveTo="done" when a yes finishes the task, like a sign-off. Left out, an approved task starts running.
- Pass elapsed in seconds. Running and waiting rows count on from there, once a second.
- Without handlers, Approve, Reject, and Retry change the task inside the block. Pass a handler to own the change in your own state.
Avoid it for
- A queue of more than a few dozen tasks. Use a DataTable with paging and search.
- A task's full history. Link to it with href, and show it on its own page.
- One job with its details, on its own. Use JobCard.
- Agent tasks with a model on each row. Use the Agent Inbox block.
Props
- tasks
One row per task, in order inside each group. ids must be unique. steps are oldest first, and a row shows the last four. href puts an Open link on running and done tasks. A new list, by value, replaces changes made inside the block.
{ id: string; title: string; summary?: string; badge?: string; status: "waiting" | "running" | "done" | "failed"; elapsed?: number; reason?: string; steps?: { label: string; time?: string }[]; href?: string }[] · default []
- onApprove
Approve on a waiting task. Without it, the task moves to Running.
(task: InboxTask) => void
- onReject
Reject on a waiting task. Without it, the task fails with "Rejected" as its reason.
(task: InboxTask) => void
- onRetry
Retry on a failed task. Without it, the task moves to Running and its clock starts over.
(task: InboxTask) => void
- onOpen
Shows Open on running and done tasks that have no href.
(task: InboxTask) => void
- approveTo
Where an approved task goes without onApprove. "running" for work that starts on a yes, like an import. "done" for a sign-off, like an expense report.
"running" | "done" · default "running"
- approveLabel
The yes button on a waiting task, like Accept or Sign off.
string · default "Approve"
- rejectLabel
The no button on a waiting task, like Decline or Send back.
string · default "Reject"
- loading
Shows placeholder rows and hides the counts.
boolean · default false
- loadingText
What the status line tells screen readers while loading.
string · default "Loading tasks"
- emptyText
Shown when there are no tasks and nothing is loading.
string · default "No tasks yet."
- label
Names the filter tabs for screen readers.
string · default "Tasks"
- className
Extra classes on the outer div, which holds the container query.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| tasks | { id: string; title: string; summary?: string; badge?: string; status: "waiting" | "running" | "done" | "failed"; elapsed?: number; reason?: string; steps?: { label: string; time?: string }[]; href?: string }[] | [] | One row per task, in order inside each group. ids must be unique. steps are oldest first, and a row shows the last four. href puts an Open link on running and done tasks. A new list, by value, replaces changes made inside the block. |
| onApprove | (task: InboxTask) => void | - | Approve on a waiting task. Without it, the task moves to Running. |
| onReject | (task: InboxTask) => void | - | Reject on a waiting task. Without it, the task fails with "Rejected" as its reason. |
| onRetry | (task: InboxTask) => void | - | Retry on a failed task. Without it, the task moves to Running and its clock starts over. |
| onOpen | (task: InboxTask) => void | - | Shows Open on running and done tasks that have no href. |
| approveTo | "running" | "done" | "running" | Where an approved task goes without onApprove. "running" for work that starts on a yes, like an import. "done" for a sign-off, like an expense report. |
| approveLabel | string | "Approve" | The yes button on a waiting task, like Accept or Sign off. |
| rejectLabel | string | "Reject" | The no button on a waiting task, like Decline or Send back. |
| loading | boolean | false | Shows placeholder rows and hides the counts. |
| loadingText | string | "Loading tasks" | What the status line tells screen readers while loading. |
| emptyText | string | "No tasks yet." | Shown when there are no tasks and nothing is loading. |
| label | string | "Tasks" | Names the filter tabs for screen readers. |
| className | string | - | Extra classes on the outer div, which holds the container query. |
Accessibility
- The filters are tabs. Arrow keys move between them, and each tab says its count.
- Each row is a button inside a heading, with aria-expanded. Enter or Space opens it to the last steps and the actions.
- Each group is a labeled group in the All tab, so screen readers say Needs you, Running, or Done.
- Status is words with a color, like Waiting for you or Failed: and a reason. Color never carries it alone.
- After Approve, Reject, or Retry, focus moves to the row in its new place. When the open filter hides it, focus goes to the tab.
- One status line outside the lists reads out each change: which task, what happened, and the new counts. The clocks never speak.
- With reduced motion, rows take their new place at once, and the running spinner stays still.
Examples
Approvals and jobs
<TaskInbox
tasks={[
{
id: "expenses",
title: "Approve Ana's expense report",
summary: "Client trip to Lisbon, $1,284 across 9 receipts.",
badge: "Ana Ruiz",
status: "waiting",
elapsed: 5400,
steps: [
{ label: "Submitted 9 receipts", time: "2h ago" },
{ label: "Waiting for your sign-off", time: "1h ago" },
],
},
{ id: "import", title: "Import March orders", badge: "Shop", status: "running", elapsed: 221, href: "/jobs/import" },
{ id: "refund", title: "Refund order #4821", badge: "Support", status: "failed", reason: "card declined", elapsed: 34 },
]}
/>Your own state owns each change
"use client";
function RequestInbox() {
const { data: tasks } = useRequests();
return (
<TaskInbox
tasks={tasks ?? []}
loading={tasks === undefined}
approveLabel="Sign off"
rejectLabel="Send back"
onApprove={(task) => signOff(task.id)}
onReject={(task) => sendBack(task.id)}
onRetry={(task) => retry(task.id)}
onOpen={(task) => openRequest(task.id)}
/>
);
}With nothing to show yet
<TaskInbox tasks={[]} emptyText="Requests sent to you show up here." />