Super-AI-Components

Date Section

Date-grouped section header for lists and grids.

Today

Brand video script

Yesterday

Logo explorations

What it is

The bucket header that groups a list by when — a small label above the rows it names, wrapping them in a real group rather than floating above them as a heading. That is the entire component: a `label` string, whatever children you put inside, and `role="group"` with `aria-labelledby` pointing at the label. It does no date maths, no localization and no grouping; you decide which bucket a row belongs to and what that bucket is called.

Why it matters

Every list an AI product accumulates is ordered by recency — threads in Manus and Claude, generations in Midjourney, scenario runs in Make — and recency is the one axis people navigate from memory. Nobody remembers a thread title, but everybody remembers that they were working on it yesterday. A date bucket turns that memory into a place to look, at the cost of one line of 12px text, which is why all four of those products reach for it before they reach for search. The group semantics are the second half of the argument: because the rows sit inside a labelled group instead of after a heading, a screen reader announces the bucket name on entering the rows, so the temporal context travels with the items for everyone rather than only for people who can see the header above them.

Observed in: Manus · Claude · Midjourney · Make

Anatomy

  1. 1date-sectionThe group wrapper. role="group" named by the label through aria-labelledby, so everything inside is announced as belonging to this bucket.
  2. 2date-section-labelThe bucket name. Deliberately a <p>, not a heading — it names a group of rows, it does not open a document section.

How to use it

Reach for it whenever a list is ordered by time and long enough that a reader scans it rather than reads it: a thread sidebar, a generation history, a run log. Keep relative buckets while they still describe something — Today, Yesterday, Last 7 days — and switch to absolute labels (March 2026) only past the point where a relative phrase stops being useful; that crossover is a decision your grouping function makes once, in one place, not per render. Pass a single already-formatted string as `label`, ideally straight out of `Intl.DateTimeFormat`, and put the rows in as children so they end up inside the group rather than beside it. If what you need is a count beside the name, a collapse toggle or a view-all link, that is `section-header` — see the pitfalls below for why those are not options here.

Do

  • Order buckets newest first and keep the relative ones at the top; an absolute month earns its place only once a relative phrase would stop describing anything.

    Today

    Brand video script

    Rewrite the onboarding email

    Last 7 days

    Summarize the Q3 board deck

    March 2026

    Storyboard for the launch film

  • Pin the label from the call site if the list scrolls — the component does not do it for you, and a bucket name you have already scrolled past has stopped answering the question it exists for.

Don't

  • Do not use it for a non-temporal group such as Pinned or Starred, even when that group sits in the same list — those rows normally also appear under their real date, and this component claims the label names where its rows belong.

    Pinned

    Brand video script

    Today

    Brand video script

    Rewrite the onboarding email

  • Do not fold a count into the label to stand in for the count slot: it becomes part of the group name every row is announced under, and it sits in the flow of the text rather than in a column that lines up bucket to bucket.

    Today (12)

    Brand video script

    Rewrite the onboarding email

    Yesterday (3)

    Logo explorations

Accessibility

Keyboard
  • Zero tab stops of its own. The wrapper is a `<div role="group">` and the label is a `<p>` — neither takes focus, so every tab stop inside a date section belongs to the rows you passed as children.
  • There are no keys, no collapse and nothing to activate. Arrow keys, Enter, Space and Escape all fall through to whatever is inside, which means the group adds no keyboard behaviour that could conflict with a row list, a link list or a menu placed in it.
  • Because there is no collapse trigger, there is no way to skip a long bucket from the keyboard: 40 rows under "Today" is 40 tab stops before "Yesterday" begins. `section-header` is the component with the fold.
Screen reader
  • `role="group"` plus `aria-labelledby` pointing at the label is the whole accessibility argument for this component: the rows are inside a named group rather than after a heading, so the bucket name travels with the items instead of being a line you had to have seen.
  • The label is a `<p>`, not a heading, so date buckets are deliberately absent from the heading outline. Jumping bucket to bucket is by group, not by heading — and a sidebar of `<h3>Today</h3>` would pollute the outline of the page beside it.
  • The label's `id` comes from `React.useId()`, so it is unique per instance and several sections in one list cannot cross-wire their names.
  • `...props` spreads after the component's own attributes, so passing your own `role` or `aria-labelledby` silently replaces the group semantics and orphans the label paragraph. If you need a different name, change `label`.
  • An empty `label` leaves `aria-labelledby` pointing at an empty paragraph, which computes to no accessible name — an unnamed group with a mystery gap above it. Skip the bucket rather than rendering it nameless.
  • Nothing announces regrouping. When "Today" becomes "Yesterday" at midnight, or a row moves between buckets, the DOM changes with no live region, so the new grouping is only discovered by re-reading the list.

Watch out for

  • The catalog lists a count variant; the component does not implement one. There is no count prop and no count slot, so the only way to show one today is inside the label string — which folds it into the group's accessible name and gives up the tabular alignment a real count column would have. section-header has the implemented version.
  • Same for the collapsible variant: there is no collapsible, open, onOpenChange or defaultOpen, no trigger button, no aria-expanded and no data-state. Nothing inside a date section can be folded away. section-header implements that contract, controlled pair included.
  • The sticky behaviour the spec calls for is not implemented either: the label sets no sticky, no top-0 and no surface of its own, so in a scroll container the bucket name leaves with its rows. You can retrofit it from the call site — the label is a direct child, so [&>[data-slot=date-section-label]]:sticky and friends reach it — but give it a background at the same time, or the rows will scroll underneath the text.
  • label is typed as a required string and nothing guards an empty one. An empty label renders an empty paragraph that aria-labelledby still points at, so the group quietly loses its accessible name while keeping the label's vertical padding: a mystery gap in the list, with an unnamed group under it. Skip the bucket entirely rather than rendering it nameless.
  • The label is text-muted-foreground and paints no background of its own, so it inherits whatever surface you drop it onto. On bg-muted, bg-accent or bg-secondary that lands at 4.34:1 and fails the contrast minimum — and the token gate cannot catch it, because the pairing happens in your container rather than in this file.
  • The wrapper spreads ...props after its own attributes, so passing a data-slot overwrites date-section and hides the fact that this is what rendered the group. Wrap it in your own element if you need a hook of your own, the way thread-list does.
  • The label is a paragraph, not a heading, so date buckets do not appear in the heading outline a screen-reader user can jump between. That is deliberate — a sidebar full of <h3>Today</h3> pollutes the document outline of the page beside it — but it means bucket-to-bucket navigation is by group, not by heading.

Installation

npx shadcn@latest add https://super-ai-components.vercel.app/r/date-section.json

Requires a shadcn app on Base UI (style base-nova). Radix-based styles such as new-york fail to typecheck: these components use Base UI's render prop and callbacks like onOpenChangeComplete, which Radix primitives do not have.