TaskList

Stacked status sections for the Tasks list view. Compose TaskCard (and optionally TaskDnd) as children.

Preview#

import {
  TaskCard,
  TaskListRoot,
  TaskSection,
  TaskSectionBadge,
  TaskSectionChevron,
  TaskSectionCount,
  TaskSectionEmpty,
  TaskSectionHeading,
  TaskSectionItems,
  TaskSectionPanel,
  TaskSectionTrigger,
} from "raft-ui";

<TaskListRoot>
  <TaskSection>
    <TaskSectionTrigger>
      <TaskSectionHeading>
        <TaskSectionBadge status="todo" />
        <TaskSectionCount />
      </TaskSectionHeading>
      <TaskSectionChevron />
    </TaskSectionTrigger>
    <TaskSectionPanel>
      <TaskSectionEmpty />
      <TaskSectionItems>
        <TaskCard />
      </TaskSectionItems>
    </TaskSectionPanel>
  </TaskSection>
</TaskListRoot>

Group cards by status with collapsible sections. Collapse and counts are driven by the composition.

Scroll area#

Use one ScrollAreaViewport around all status sections. The list remains a single vertical scroll surface, while TaskListRoot and sections only own their layout and collapse state.

Sparse tasks leave In Review / Done / Closed empty so TaskSectionEmpty shows. List has a single empty host (no DnD idle/allowed/blocked triad).

Keep one shared ScrollAreaViewport, and mount a useVirtualizer stack inside each tall section. Virtualization stays consumer-owned; TaskList does not provide it.

const viewportRef = useRef<HTMLDivElement>(null);

const virtualizer = useVirtualizer({
  count: items.length,
  getScrollElement: () => viewportRef.current,
  getItemKey: (index) => items[index]!.id,
  estimateSize: () => 116,
  gap: 10,
  overscan: 6,
});

<ScrollArea>
  <ScrollAreaViewport ref={viewportRef}>
    <TaskSectionItems>
      <div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
        {virtualizer.getVirtualItems().map((row) => (
          <div
            key={row.key}
            ref={virtualizer.measureElement}
            style={{ position: "absolute", transform: `translateY(${row.start}px)` }}
          >
            {renderItem(items[row.index]!)}
          </div>
        ))}
      </div>
    </TaskSectionItems>
  </ScrollAreaViewport>
</ScrollArea>

API#WIP

...props
useRender.ComponentProps<"div">
List view root for TaskSection children.
open
boolean
Controlled collapsed state for the section.
defaultOpendefault: false
boolean
Initial collapsed state for the section.
...props
Collapsible.Root.Props
List section root. Defaults to a section host via render.
...props
Collapsible.Trigger.Props
Expands or collapses the section.
...props
useRender.ComponentProps<"span">
Heading row for badge and count.
status
"todo" | "in-progress" | "in-review" | "done" | "closed"
Task status that drives the badge surface color.
...props
useRender.ComponentProps<"span">
Section status badge.
...props
useRender.ComponentProps<"span">
Task count in the section heading.
...props
useRender.ComponentProps<"span">
Disclosure chevron for the section trigger.
...props
Collapsible.Panel.Props
Collapsible panel hosting empty state and items.
...props
useRender.ComponentProps<"div">
List of task cards inside the section.
...props
useRender.ComponentProps<"div">
Empty-state host for a section with no tasks.