ListPageTemplate

Opinionated layout template for list pages — PageHeader + optional tabs + search + content slot. The canonical shape confirmed in 5 of 6 TimelyCare products. Single universal compound (no variants).

Spec · from metadata

When to use

  • Admin list page with a sortable/paginated DataTable (Visits, Members, Surveys, Campaigns, Users, Clients)
  • Catalog index page with a primary 'Create new X' action
  • Pages that need to toggle between two list modes (Saved / Templates, All / Drafts, etc.)
  • Any list surface where the search input lives at page level (not inside a table toolbar)

When not to use

  • Dashboards / analytics pages — use a custom layout with `MetricCard` + `LineChartCard` instead
  • Detail pages (one entity) — use `PageHeader` directly, no list-page chrome
  • Pages with full Tabs that need separate content panels — use the `Tabs` primitive directly; the template's `tabs` prop is for toggle-style filter switching
  • Pages without a search surface — set `hideSearch` if you must use the template, but most no-search pages don't need it at all

Anti-patterns

Avoid<ListPageTemplate title="Surveys">
  <PageHeader>
    <PageHeaderHeading>Surveys</PageHeaderHeading>
  </PageHeader>
  <DataTable columns={cols} data={rows} />
</ListPageTemplate>
Prefer<ListPageTemplate
  title="Surveys"
  primaryAction={{ label: "+ Create Survey", onClick: handleCreate }}
>
  <DataTable columns={cols} data={rows} />
</ListPageTemplate>

The template already renders PageHeader. Passing another inside children duplicates the heading and breaks the canonical layout.

Avoid<ListPageTemplate
  title="Members"
  primaryAction={{ label: "Add Member", onClick: add }}
>
  <Input placeholder="Search members..." />
  <DataTable columns={cols} data={rows} />
</ListPageTemplate>
Prefer<ListPageTemplate
  title="Members"
  primaryAction={{ label: "Add Member", onClick: add }}
  searchPlaceholder="Search members..."
  searchValue={query}
  onSearchChange={setQuery}
>
  <DataTable columns={cols} data={rows} />
</ListPageTemplate>

Use the template's search props so the search input lives in the canonical toolbar position. Putting a second search inside children creates layout drift across products.

Avoid<ListPageTemplate
  title="Media"
  primaryAction={{ label: "+ Add new Thumbnail", onClick: addThumb }}
>
  <Button>+ Add new Media</Button>
  <DataTable ... />
</ListPageTemplate>
Prefer<ListPageTemplate
  title="Media"
  primaryAction={{ label: "+ Add new Media", onClick: addMedia }}
>
  <DataTable ... />
</ListPageTemplate>

Only one primary per section. If you need a secondary action, render it inside the table toolbar with variant='outline' or use a DropdownMenu to combine related actions.

Accessibility

  • Min touch target48px
  • Screen readerSearch input is type='search'. Tabs render as a ToggleGroup with role='group' and individual buttons. The heading inside PageHeader is the h1 for the route.
  • ContrastInherits color/contrast from PageHeader, ToggleGroup, and Input primitives. All meet WCAG AA on the design-system background.

Token bindings

TokenCategoryUsage
6 (24px)spacingGap between PageHeader, toolbar row, and content
3 (12px)spacingGap between tabs and search inside the toolbar
muted-foregroundcolorSearch icon color
text-smtypographySearch input + ToggleGroup item text
Recent changes
  • new

    Compound components join the catalog

    2026-07-09

[!NOTE] Compound component — an opinionated composition of Helix primitives. Composes: Button, Input, ToggleGroup.

Import

import { ListPageTemplate } from "@timelycare/helix-ui"

Usage

<ListPageTemplate
  title="Surveys"
  description="Create, manage, and browse surveys and survey templates."
  primaryAction={{ label: "+ Create Survey", onClick: handleCreate }}
  tabs={[
    { value: "saved", label: "Saved Surveys" },
    { value: "templates", label: "Survey Templates" },
  ]}
  activeTab={tab}
  onTabChange={setTab}
  searchPlaceholder="Search surveys..."
  searchValue={query}
  onSearchChange={setQuery}
>
  <DataTable columns={columns} data={rows} />
</ListPageTemplate>