Activity Feed
A chronological list of events, with a connecting timeline rail.
Deployed to production
Build 1420 · mainAll checks passed
24 tests, 0 failuresOpened pull request #482
Add keyboard shortcuts to the command paletteCreated branch feat/shortcuts
From main
Installation
pnpm dlx @dowel-ui/cli add activity-feednpm packages installed: radix-ui.
Accessibility
An ordered list, so position and count are announced — the order is the meaning here. The rail and the indicators are decorative; the item text has to say what happened. ActivityTime requires a machine-readable dateTime, because a relative label like "2 hours ago" is ambiguous outside the moment it was rendered.
Props
ActivityFeed
Plus every attribute of <ol>.
ActivityItem
| Prop | Type | Default |
|---|---|---|
lastDraws no connecting line below. Set this on the last item. | boolean | — |
Plus every attribute of <li>.
ActivityIndicator
| Prop | Type | Default |
|---|---|---|
asChild | boolean | — |
Plus every attribute of <div>.
ActivityContent
Plus every attribute of <div>.
ActivityTitle
Plus every attribute of <p>.
ActivityTime
| Prop | Type | Default |
|---|---|---|
dateTime (required)Machine-readable timestamp. Required — a relative label alone is ambiguous. | string | — |
Plus every attribute of <time>.
ActivityDescription
Plus every attribute of <div>.
Quality
7/7 checks, measured from the source and its tests
- Tested — passes
- axe assertion — passes
- Keyboard tested — does not apply
- Storybook examples — passes
- Accessibility documented — passes
- Semantic tokens only — passes
- Motion from tokens — does not apply
- className merged — passes
- Visible focus — does not apply
- No fixed widths — passes
Used in
Whole screens assembled from this component. Installing one brings this and everything else it needs with it.
Source
This is exactly what dowel add activity-feed writes into your project, with imports rewritten to your own path alias.
import { Slot } from "radix-ui";
import type { ComponentPropsWithRef } from "react";
import { cn } from "@/lib/utils";
/**
* A chronological list of events.
*
* An ordered list underneath, because the order is the meaning: a screen reader
* announces the position and count, and "3 of 12" is information a row of divs
* cannot convey. The connecting line is drawn with a pseudo-element rather than
* a real node so it never appears in the accessibility tree.
*/
export function ActivityFeed({ className, ...props }: ComponentPropsWithRef<"ol">) {
return <ol data-slot="activity-feed" className={cn("flex flex-col", className)} {...props} />;
}
export interface ActivityItemProps extends ComponentPropsWithRef<"li"> {
/** Draws no connecting line below. Set this on the last item. */
last?: boolean;
}
export function ActivityItem({ className, last, ...props }: ActivityItemProps) {
return (
<li
data-slot="activity-item"
data-last={last || undefined}
className={cn(
"relative flex gap-3 pb-6 last:pb-0",
// The rail is a pseudo-element on the item, so it is decorative by
// construction rather than by remembering an aria-hidden.
"before:absolute before:start-[0.6875rem] before:top-7 before:bottom-1 before:w-px before:bg-border",
"last:before:hidden data-[last]:before:hidden",
className,
)}
{...props}
/>
);
}
export interface ActivityIndicatorProps extends ComponentPropsWithRef<"div"> {
asChild?: boolean;
}
/** The marker on the rail. Decorative: the item's text says what happened. */
export function ActivityIndicator({ className, asChild, ...props }: ActivityIndicatorProps) {
const Comp = asChild ? Slot.Root : "div";
return (
<Comp
data-slot="activity-indicator"
aria-hidden="true"
className={cn(
"relative z-[var(--z-base)] grid size-6 shrink-0 place-items-center rounded-full",
"border border-border bg-background text-muted-foreground",
"[&_svg:not([class*='size-'])]:size-3",
className,
)}
{...props}
/>
);
}
export function ActivityContent({ className, ...props }: ComponentPropsWithRef<"div">) {
return (
<div
data-slot="activity-content"
className={cn("flex min-w-0 flex-1 flex-col gap-1 pt-0.5", className)}
{...props}
/>
);
}
export function ActivityTitle({ className, ...props }: ComponentPropsWithRef<"p">) {
return (
<p
data-slot="activity-title"
className={cn("text-sm leading-snug text-foreground", className)}
{...props}
/>
);
}
export interface ActivityTimeProps extends ComponentPropsWithRef<"time"> {
/** Machine-readable timestamp. Required — a relative label alone is ambiguous. */
dateTime: string;
}
/**
* When the event happened.
*
* `dateTime` is required rather than optional: "2 hours ago" is meaningless in
* a page read a day later, or by anything parsing the feed, and a `<time>` with
* no datetime is just a span.
*/
export function ActivityTime({ className, dateTime, ...props }: ActivityTimeProps) {
return (
<time
data-slot="activity-time"
dateTime={dateTime}
className={cn("text-xs text-muted-foreground", className)}
{...props}
/>
);
}
export function ActivityDescription({ className, ...props }: ComponentPropsWithRef<"div">) {
return (
<div
data-slot="activity-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
}