Once UI Handbook
Beginner Guides
What is Once UI?
Install & config
Page skeleton
Your first page
Build And Launch
How to launch a portfolio site that actually gets you hired
How to build a documentation site with MDX and Next.js
How to ship a landing page and dashboard with authentication in Next.js
How to launch a social app with Next.js and Supabase
Vibe Coding
Introduction to vibe coding
Set up your local dev environment
Essential tools for vibe coding
Build your first Once UI app
Common Patterns
Hero section
Static panel
Form layout
Highlighted card
Responsive stacking
Decoration layers
Dialog & modal
Toast & feedback
Tables & lists
Loading states
App shell navigation
Dashboard & charts
Chat & messaging
Auth & verification
Settings & split panels
Data filters & toolbar
Command palette
Pricing & plans
Accordion & FAQ
Media & uploads
Empty & error states
SEO, Open Graph, and structured data
Documentation code blocks with live preview
Carousels, galleries, and before/after comparisons
Date pickers and scheduling
Context menus and dropdown actions
Tags, chips, and multi-value inputs
Roadmap & kanban
Form inputs & controls
Progress, status & badges
Scrolling & feeds
Profile, avatars & identity
Social proof & logo clouds
Masonry & media grids
Table pagination, search & bulk selection
Banners & announcements
Footer layouts
Onboarding & first run
Waitlist & coming soon
Design Tips
Color & surfaces
Harness overview
Row, Column & Grid
Spacing & rhythm
Icons & buttons
Reveal & motion
Theme tokens, FOUC-free init, and runtime style controls
Tooltips and hover cards
Block updates
Block updates changelog
Agent Resources
Column, not Flex
Semantics over CSS
Typography scale
Common gotchas
Block anchors
Validate generated code
Pro block registry
Match task bundles
TrademarkTrademark
Ctrl k
Search...
Sign up
Once UI Handbook
Beginner Guides
What is Once UI?
Install & config
Page skeleton
Your first page
Build And Launch
How to launch a portfolio site that actually gets you hired
How to build a documentation site with MDX and Next.js
How to ship a landing page and dashboard with authentication in Next.js
How to launch a social app with Next.js and Supabase
Vibe Coding
Introduction to vibe coding
Set up your local dev environment
Essential tools for vibe coding
Build your first Once UI app
Common Patterns
Hero section
Static panel
Form layout
Highlighted card
Responsive stacking
Decoration layers
Dialog & modal
Toast & feedback
Tables & lists
Loading states
App shell navigation
Dashboard & charts
Chat & messaging
Auth & verification
Settings & split panels
Data filters & toolbar
Command palette
Pricing & plans
Accordion & FAQ
Media & uploads
Empty & error states
SEO, Open Graph, and structured data
Documentation code blocks with live preview
Carousels, galleries, and before/after comparisons
Date pickers and scheduling
Context menus and dropdown actions
Tags, chips, and multi-value inputs
Roadmap & kanban
Form inputs & controls
Progress, status & badges
Scrolling & feeds
Profile, avatars & identity
Social proof & logo clouds
Masonry & media grids
Table pagination, search & bulk selection
Banners & announcements
Footer layouts
Onboarding & first run
Waitlist & coming soon
Design Tips
Color & surfaces
Harness overview
Row, Column & Grid
Spacing & rhythm
Icons & buttons
Reveal & motion
Theme tokens, FOUC-free init, and runtime style controls
Tooltips and hover cards
Block updates
Block updates changelog
Agent Resources
Column, not Flex
Semantics over CSS
Typography scale
Common gotchas
Block anchors
Validate generated code
Pro block registry
Match task bundles
TrademarkTrademark
Once UIDocumentationBlog
© Once UI. All rights reserved.
Built with Aveiro
Common patterns

Table pagination, search & bulk selection

Table1 Pro block patterns — search, page size, pagination footer, and bulk action bars.
Updated 21d ago
Masonry & media grids
Banners & announcements
7 min · Pattern
Data tables need search, page size, pagination controls, and optional bulk selection. The Table1 Pro block shows the full toolbar pattern — wire state in the parent, not inside Table.

What this pattern covers

Basic Table renders headers and rows. Production admin tables add:
  • Search filter above the grid
  • Items-per-page control
  • Previous / next pagination
  • Row checkboxes with “select all on page”
  • Bulk action bar when selection is non-empty
Gold reference: packages/core/ai/examples/blocks/Table1.tsx — canonical implementation for dashboard and settings task bundles.

State you own in the parent

Keep four pieces of state outside the table component:
State
Purpose
`search`Filter string
`itemsPerPage`Page size (5, 25, 50, 100)
`currentPage`1-based page index
`selectedIds`Set of row identifiers
Derive filtered rows with useMemo, then slice for the current page: Describe in prose: filtered = filterFn ? data.filter(...) : data, then paginated = filtered.slice(start, end). Reset currentPage to 1 when search or page size changes.

Toolbar layout

Stack heading row and search:
<Column
  fillWidth
  radius="l"
  border="neutral-alpha-weak"
  background="surface"
>
  <Row
    fillWidth
    horizontal="between"
    vertical="center"
    paddingY="12"
    paddingX="24"
    gap="16"
    s={{ direction: "column" }}
  >
    <Heading variant="heading-strong-s">Projects</Heading>
    <Input
      id="table-search"
      placeholder="Search…"
      value={search}
      onChange={(e) => setSearch(e.target.value)}
    />
  </Row>
  {/* table body */}
</Column>
Use fillWidth on Input inside the toolbar Row so search expands on desktop.

Pagination footer

Place page controls in a footer Row below the table:
<Row
  fillWidth
  horizontal="between"
  vertical="center"
  paddingY="12"
  paddingX="24"
  gap="16"
  s={{ direction: "column" }}
>
  <Text variant="body-default-xs" onBackground="neutral-weak">
    Showing {startIndex + 1}–{endIndex} of {filtered.length}
  </Text>
  <Row gap="8" vertical="center">
    <IconButton
      icon="chevronLeft"
      variant="secondary"
      size="s"
      disabled={currentPage === 1}
      aria-label="Previous page"
    />
    <Text variant="body-default-s">
      Page {currentPage} of {totalPages}
    </Text>
    <IconButton
      icon="chevronRight"
      variant="secondary"
      size="s"
      disabled={currentPage === totalPages}
      aria-label="Next page"
    />
  </Row>
</Row>
Wire onClick on IconButtons to decrement/increment currentPage with bounds checks.

Page size control

SegmentedControl works well for page sizes when options are few:
Place beside pagination or in the toolbar Row. Avoid dropdowns when three options suffice.

Bulk selection bar

When selectedIds.size > 0, swap the heading for an action row:
Describe checkbox columns in prose: Table1 renders a Checkbox in the first column when selectable is true. “Select all” toggles only the current page ids — not the entire filtered dataset unless you explicitly implement that.

Table body

Pass paginated rows to Table:
Status cells should use Tag — see Progress, status & badges.

Empty search results

When filtered.length === 0, skip the table and show Empty & error states inside the same surface panel.

Agent notes

  • Task bundles: dashboard.json, settings.json
  • Do not embed pagination inside Table — it is presentational
  • Stable row ids: pass getId logic when data can reorder between pages
  • Validate with pnpm --filter @once-ui-system/core validate-ai-code after codegen

Write this / not this

✅ Once UI way
❌ Common mistake
Parent-owned filter + page stateTable with internal search API
Reset page on filter changeStuck on page 5 of empty results
`Tag` in status cellsRaw colored spans
Bulk actions only when selection &gt; 0Always-visible delete bar

Check yourself

  • Search filters before pagination slice
  • Footer shows accurate range text
  • Previous/next disabled at boundaries
  • Empty and loading states handled — Loading states

Related

→ Tables & lists → Data filters & toolbar → Dashboard & charts → Pro block: packages/core/ai/examples/blocks/Table1.tsx
<SegmentedControl
  buttons={[
    { value: "25", label: "25" },
    { value: "50", label: "50" },
    { value: "100", label: "100" },
  ]}
  value={String(itemsPerPage)}
  onChange={(v) => setItemsPerPage(Number(v))}
/>
<Row fillWidth horizontal="between" vertical="center" gap="16">
  <Text variant="body-strong-s">
    {selectedIds.size} selected
  </Text>
  <Row gap="8">
    <Button size="s" variant="secondary">Export</Button>
    <Button size="s" variant="danger">Delete</Button>
  </Row>
</Row>
<Table fillWidth data={{ headers, rows: paginatedRows }} />