Project Views
betaShows one set of projects as a Board, a List, or a Timeline, switched by tabs, with search, filters, and a sheet for each project. The name column stays still between List and Timeline, and only the dates side changes.
Built from Avatar Stack, Badge, Board, Button, Gantt Chart, Glide Highlight, Input, Menu, Motion Timing, Notice, Progress, Rolling Number, Search Field, Sheet, Skeleton, Tabs, Toast, and Tooltip.
Preview
Projects
14 projects across 4 stages
starts on the timeline, in another locale
Projects
14 projects across 4 stages
loading
Projects
empty
Projects
0 projects
No projects yet.
error, then retry
Projects
Not loaded
no matches for a search
Projects
0 projects
No projects match “Roadmap”.
No projects match “Roadmap”.
narrow container
Projects
7 projects across 2 stages
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/project-views
Use it for
- Work that moves through stages, like projects, when people want a board, a list, and dates on one screen.
- Pass defaultProjects for the block to own the list, or projects with onProjectsChange to own it yourself.
- Give each project a stage id from stages. The default stages are To do, In progress, In review, and Done.
- Pass today as an ISO day from the server. It marks the timeline and dates new projects, and keeps the first render the same on the server and in the browser.
- Dates are ISO days, like "2026-11-06". start and due draw the timeline bar.
- Move and Remove apply at once and offer Undo in a toast for 8 seconds, so place one Toaster on the page.
- Set loading while the projects load, and error with onRetry when they fail.
Avoid it for
- A single list with no stages. Use Table or List Row.
- Only a timeline, with no board or list. Use Gantt Chart.
- Dragging cards between lanes. Cards do not drag. Each card's menu moves it.
Props
- projects
The projects, in order inside each stage. Use with onProjectsChange to own them. Each id is unique, and stage is a stage's id.
{ id: string; name: string; kind?: string; stage: string; description?: string; priority?: "high" | "medium" | "low"; urgent?: boolean; tasks?: { done: number; total: number }; people?: string[]; comments?: number; files?: number; start?: string; due?: string }[]
- defaultProjects
The projects to start with, when the block owns them.
Project[] · default []
- onProjectsChange
Called with the whole new list after an add, a move, a remove, or an undo.
(projects: Project[]) => void
- view
Which view shows. Use with onViewChange.
"board" | "list" | "timeline"
- defaultView
The view to start on.
"board" | "list" | "timeline" · default "board"
- onViewChange
Called when a tab picks another view.
(view: ProjectView) => void
- defaultSearch
The search to start with.
string · default ""
- stages
The stages, in order. Each is a lane on the board and a group in the list.
{ id: string; title: string; tone?: "neutral" | "brand" | "danger" | "success" | "warning" }[] · default To do, In progress, In review, Done
- today
Today as an ISO day, passed from the server.
string
- locale
The locale for dates.
string · default "en-US"
- timeZone
The time zone the timeline reads days in.
string · default "UTC"
- loading
Each view holds its space with placeholders, and the count waits.
boolean · default false
- error
Says what went wrong in place of the views.
string
- onRetry
Shows Retry beside the error.
() => void
- className
Extra classes on the outer section, which holds the container query.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| projects | { id: string; name: string; kind?: string; stage: string; description?: string; priority?: "high" | "medium" | "low"; urgent?: boolean; tasks?: { done: number; total: number }; people?: string[]; comments?: number; files?: number; start?: string; due?: string }[] | - | The projects, in order inside each stage. Use with onProjectsChange to own them. Each id is unique, and stage is a stage's id. |
| defaultProjects | Project[] | [] | The projects to start with, when the block owns them. |
| onProjectsChange | (projects: Project[]) => void | - | Called with the whole new list after an add, a move, a remove, or an undo. |
| view | "board" | "list" | "timeline" | - | Which view shows. Use with onViewChange. |
| defaultView | "board" | "list" | "timeline" | "board" | The view to start on. |
| onViewChange | (view: ProjectView) => void | - | Called when a tab picks another view. |
| defaultSearch | string | "" | The search to start with. |
| stages | { id: string; title: string; tone?: "neutral" | "brand" | "danger" | "success" | "warning" }[] | To do, In progress, In review, Done | The stages, in order. Each is a lane on the board and a group in the list. |
| today | string | - | Today as an ISO day, passed from the server. |
| locale | string | "en-US" | The locale for dates. |
| timeZone | string | "UTC" | The time zone the timeline reads days in. |
| loading | boolean | false | Each view holds its space with placeholders, and the count waits. |
| error | string | - | Says what went wrong in place of the views. |
| onRetry | () => void | - | Shows Retry beside the error. |
| className | string | - | Extra classes on the outer section, which holds the container query. |
Accessibility
- The block is a section named by its heading. On a page with more than one, give each an aria-label, which then names it instead. The views are tabs, and the arrow keys move between them.
- Each card and each row has one button named by the project, which opens its sheet. The rest of a row opens it with a pointer.
- Each project's "…" button is named "More for" and the project, and opens Open, Move to, and Remove.
- After a move or a remove, focus goes to the project in its new place, or to the one beside where it was.
- List and Timeline are one table, with column headers, a row header for each project, and a row group for each stage.
- Each lane is a group named by its stage and count. Each "+" button is named for its stage, and its tooltip says Add project.
- The view that leaves is hidden from screen readers and from Tab while it fades.
- The count line and each stage's count roll to new numbers. Reduced motion takes out the slide, and motion off shows each view at once.
Examples
Projects the block keeps
<ProjectViews
today="2026-10-30"
defaultProjects={[
{ id: "payments", name: "Payments page", kind: "Web", stage: "doing", priority: "high", urgent: true, tasks: { done: 9, total: 20 }, people: ["AK", "BL"], start: "2026-10-19", due: "2026-11-06" },
{ id: "help", name: "Help center", kind: "Web", stage: "review", tasks: { done: 18, total: 20 }, start: "2026-10-12", due: "2026-10-31" },
]}
/>Your own state owns the projects and the view
"use client";
function TeamProjects({ initial, today }: { initial: Project[]; today: string }) {
const [projects, setProjects] = useState(initial);
const [view, setView] = useState<ProjectView>("list");
return (
<ProjectViews
projects={projects}
onProjectsChange={(next) => {
setProjects(next);
saveProjects(next);
}}
view={view}
onViewChange={setView}
today={today}
/>
);
}Your own stages
<ProjectViews
today="2026-10-30"
stages={[
{ id: "ideas", title: "Ideas" },
{ id: "building", title: "Building", tone: "brand" },
{ id: "shipped", title: "Shipped", tone: "success" },
]}
defaultProjects={[{ id: "search", name: "Search", stage: "ideas" }]}
/>While loading, and when loading fails
<ProjectViews
projects={data ?? []}
loading={isLoading}
error={failed ? "Projects could not load." : undefined}
onRetry={refetch}
/>