WebberUI

Agenda Tracks Grid

A time × track conference agenda grid — the timeline column freezes to the left, session cards reflow into place with FLIP when tracks or speakers are filtered, and clicking a session expands the speaker and summary in place.

Loading preview…
npx shadcn@latest add https://webberui.com/r/agenda-tracks-grid.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

240
68
152
<AgendaTracksGrid />

Installation

npx shadcn@latest add https://webberui.com/r/agenda-tracks-grid.json

Or, once registries are configured in components.json, install it as @webberui/agenda-tracks-grid.

Usage

Tracks go in through tracks (which determines the columns) and sessions through sessions. Each intersection of a trackId and a start maps to at most one session. The toolbar's track toggles and speaker filter are managed inside the component by default, and can also be driven from outside through controlled props.

import {
  AgendaTracksGrid,
  type AgendaTrack,
  type AgendaSession,
} from "@/components/ui/agenda-tracks-grid";

const tracks: AgendaTrack[] = [
  { id: "keynote", label: "Main stage" },
  { id: "workshop", label: "Workshop" },
];

const sessions: AgendaSession[] = [
  {
    id: "s1",
    trackId: "keynote",
    start: "09:00",
    title: "Opening: animation-first interfaces",
    speaker: "Angela Lin",
    summary: "From first principles: how motion carries an information hierarchy.",
  },
  {
    id: "s2",
    trackId: "workshop",
    start: "09:00",
    title: "Hands-on: building FLIP reflow",
    speaker: "Tzu-Hsuan Chou",
    summary: "Using layout animation to build a grid that reflows the moment it switches.",
  },
];

<AgendaTracksGrid tracks={tracks} sessions={sessions} defaultExpandedId="s1" />

Props

PropTypeDefaultDescription
tracksAgendaTrack[]List of tracks, determining the columns' content and their left-to-right order
sessionsAgendaSession[]List of sessions; each track + time intersection holds at most one
timesstring[]Order of the time rows; when omitted, they are collected from the sessions and sorted lexicographically
activeTracksstring[]Controlled: array of track ids currently shown
defaultActiveTracksstring[]allTracks shown initially in uncontrolled mode
onActiveTracksChange(ids: string[]) => voidFires when the shown tracks change
speakerstring | nullControlled: speaker filter (null means all)
defaultSpeakerstring | nullnullInitial speaker filter in uncontrolled mode
onSpeakerChange(speaker: string | null) => voidFires when the speaker filter changes
expandedIdstring | nullControlled: id of the currently expanded session
defaultExpandedIdstring | nullnullSession expanded initially in uncontrolled mode
onExpandedChange(id: string | null) => voidFires when the expanded session changes
showControlsbooleantrueWhether to show the built-in track / speaker filter toolbar
maxHeightnumber360Maximum height of the scroll container (px)
timeColumnWidthnumber68Width of the timeline column (px)
trackMinWidthnumber152Minimum column width per track (px); too many columns triggers horizontal scrolling
classNamestringAppended to the outermost container's className

AgendaTrack

FieldTypeDescription
idstringUnique track id, matching AgendaSession.trackId
labelstringTrack display name (column header)

AgendaSession

FieldTypeDescription
idstringUnique session id
trackIdstringId of the track it belongs to
startstringStart-time label; must match one of the time rows
titlestringSession title
speakerstringSpeaker name (used for filtering and shown on expand)
summaryReact.ReactNodeSummary content shown once expanded

Animation details

  • Frozen headers: the timeline column is pinned to the left with sticky left and the track headers to the top with sticky top, with the top-left corner intersection stacked above both; when the columns exceed the container width it scrolls horizontally and the timeline column stays visible throughout.
  • FLIP reflow: toggling a track removes a whole column, and filtering by speaker automatically collapses tracks with no matching sessions; exiting session cards are frozen in place by popLayout and fade out while the rest reflow smoothly with layout="position", avoiding text scaling distortion.
  • In-place expansion: clicking a session card expands the speaker and summary right where it sits, growing that time row's height while the rows below are pushed down by the same spring; clicking it again or clicking another card collapses it (only one expands at a time).
  • Speaker filter: non-matching session cards fade into a disabled state, preserving the time-aligned context.

Accessibility

  • When the user has "reduce motion" enabled at the system level, the FLIP, the enter and exit, and the expansion animations are disabled and a switch lands directly; the DOM structure does not change, and the swap only happens after mount to avoid a hydration gap
  • The grid uses role="grid" together with aria-rowcount / aria-colcount, with rows and cells marked as columnheader, rowheader, and gridcell and given their matching indexes
  • Track toggles are aria-pressed toggle buttons; the speaker filter is a single-select role="radiogroup"; session cards are buttons carrying aria-expanded / aria-controls, and the expanded content is marked role="region"
  • Every decorative icon and placeholder is marked aria-hidden, and everything is keyboard focusable with a focus-visible outline

On this page