Data & Search Patterns

Search, filter, sort, and data state patterns — empty, loading, and error — for lists and tables.

Overview

Every list, table, or data view goes through the same lifecycle: loadingpopulated (or empty) → possibly error. This pattern covers how to compose Input, Button, Table, Spinner, Skeleton, Alert, Badge, and Pagination to handle all four states with a consistent UX — and how to add search, filter, and sort on top.

The Four Data States

StateTriggerComponentBehaviour
Loading (first paint)Page / view mounts, data fetch startsSkeletonShows the shape of content before it arrives — reduces layout shift and perceived wait
Loading (subsequent)Filter change, pagination, refreshSpinnerIndicates an in-progress action without hiding existing content
EmptyQuery returned zero results, or no items exist yet<div> + illustration + message + ButtonA friendly, hand-drawn-style empty state with a clear next action
ErrorNetwork failure, server error, permission deniedAlert variant="danger" + retry ButtonExplains what happened and offers a direct path to recovery

Skeleton vs Spinner Decision Tree

Is there existing content on screen?
├── No (first paint) → Skeleton
└── Yes (refresh / filter) → Spinner

Skeleton uses variant="text", "heading", "avatar", or "block" to mirror the page’s layout. A typical data table skeleton renders 5–8 rows of alternating text + block shapes. See the Skeleton component page for variant examples.

Empty State

A hand-drawn empty state is an opportunity to express GHDS’s personality. Pair a sketchy illustration with a warm message and a clear action:

  ┌──────────────────────────────────────┐
  │        (hand-drawn illustration)      │
  │                                      │
  │   No search results for "xyz"       │
  │   Try a different term or clear      │
  │   the filter.                        │
  │                                      │
  │          [ Clear filters ]           │
  └──────────────────────────────────────┘
// React — empty state
<Card>
  <div style={{ textAlign: 'center', padding: 'var(--sys-spacing-xl)' }}>
    <Icon name="search" size="lg" />
    <p>No results for "{query}".</p>
    <Button variant="neutral" onClick={clearSearch}>Clear search</Button>
  </div>
</Card>

Search, Filter & Sort

A search input paired with a Button (icon or text). Debounce the input (300–400ms) before firing queries — don’t search on every keystroke.

// React — search with debounce
<Input
  placeholder="Search items..."
  value={query}
  onChange={(e) => setQuery(e.target.value)}
/>

For Web Components, use gh-input with a debounced input event listener.

Filter Chips / Badges

Active filters are displayed as Badge components with a dismiss action. A “Clear all” link or button resets the filter set. The Badge component’s sketchy style integrates naturally.

// React — filter chips
{filters.map((f) => (
  <Badge key={f} variant="neutral" onDismiss={() => removeFilter(f)}>
    {f}
  </Badge>
))}

Sorting

Column headers in Table are clickable to toggle ascending / descending sort. The Table component accepts a sort prop ({ column, direction }) and calls onSort on header click. A sorted column shows an arrow indicator ( / ).

Search + Filter + Sort Together

┌──────────────────────────────────────────────────────┐
│  [ 🔍 Search input...           ]  [ Sort by: Name ▾ ] │
│  [ ✕ Electronics ] [ ✕ In stock ]  [ Clear all ]     │
├──────────────────────────────────────────────────────┤
│  Name              │ Price  │ Status                  │
│  ──────────────────┼────────┼─────────────────────────│
│  Wireless Mouse    │ $29.99 │ In stock                │
│  USB-C Hub         │ $49.99 │ Low stock               │
│  ──────────────────┴────────┴─────────────────────────│
│           ← 1  2  3  4  5 →                           │
└──────────────────────────────────────────────────────┘

Live Demo

React @ghds/react

Web Components @ghds/web-components

// React Native — data states
import { Alert } from '@ghds/react-native/alert';
import { Button } from '@ghds/react-native/button';
import { Skeleton } from '@ghds/react-native/skeleton';
import { Spinner } from '@ghds/react-native/spinner';
import { Table } from '@ghds/react-native/table';

// Loading
<Skeleton variant="block" />;

// Error
<Alert variant="danger" title="Failed to load">
  Check your connection. <Button label="Retry" onPress={refetch} />
</Alert>;

// Empty
<Text>No items yet. <Button label="Add one" variant="neutral" /></Text>;

Accessibility

Key Action
Tab Move between search input, filter chips, table headers, and pagination.
Enter / Space Toggle a sortable column header; activate a pagination button; dismiss a filter chip.