AZMARA PLATFORM
Examples

Data table page

Search, sortable columns, row actions, paging and an empty state, from the Table, Pagination and menu components.

View as Markdown for coding assistants, or see llms.txt.

A customer list the way most apps need one: search it, sort it, page through it and act on a row.

Preview theme
Preview density
Customers, sorted by name
Actions
Anika RaoBusinessActive$240.00
Aroha NgataBusinessActive$240.00
Hana CooperStarterOverdue$49.00
Hemi Te RangiEnterpriseActive$1,800.00
Kiri ParataBusinessActive$240.00

Showing 1–5 of 12 customers

<div className="flex flex-col gap-4">
  <div className="flex max-w-sm flex-col gap-2">
    <label htmlFor={searchId} className="text-sm font-medium">
      Search customers
    </label>
    <div className="relative">
      <Icon
        icon={Search}
        className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-muted-foreground"
      />
      <Input
        id={searchId}
        type="search"
        className="pl-9"
        placeholder="Name, plan or status"
        value={query}
        onChange={(event) => {
          setQuery(event.target.value);
          setPage(1);
        }}
      />
    </div>
  </div>

  {shown.length === 0 ? (
    <EmptyState
      icon={<Icon icon={SearchX} size="xl" />}
      title="No customers match"
      description={`Nothing matches “${query}”. Check the spelling or clear the search.`}
      action={
        <Button size="sm" variant="outline" onClick={() => setQuery("")}>
          Clear search
        </Button>
      }
    />
  ) : (
    <Table hoverable>
      <TableCaption>Customers, sorted by {sort.key}</TableCaption>
      <TableHeader>
        <TableRow>
          {header("name", "Name")}
          {header("plan", "Plan")}
          {header("status", "Status")}
          {header("monthly", "Monthly", "right")}
          <TableHead align="right">
            <span className="sr-only">Actions</span>
          </TableHead>
        </TableRow>
      </TableHeader>
      <TableBody>
        {shown.map((customer) => (
          <TableRow key={customer.name}>
            <TableHead scope="row" className="font-normal text-foreground">
              {customer.name}
            </TableHead>
            <TableCell>{customer.plan}</TableCell>
            <TableCell>
              <Badge variant={STATUS_TONE[customer.status]}>{customer.status}</Badge>
            </TableCell>
            <TableCell numeric>{nzd.format(customer.monthly)}</TableCell>
            <TableCell align="right">
              <DropdownMenu>
                <DropdownMenuTrigger
                  render={
                    <IconButton
                      icon={<Icon icon={MoreHorizontal} />}
                      label={`Actions for ${customer.name}`}
                      variant="ghost"
                      size="sm"
                    />
                  }
                />
                <DropdownMenuContent>
                  <DropdownMenuItem onClick={() => toast(`Opened ${customer.name}`)}>
                    Open
                  </DropdownMenuItem>
                  <DropdownMenuItem onClick={() => toast("Email copied")}>
                    Copy email
                  </DropdownMenuItem>
                </DropdownMenuContent>
              </DropdownMenu>
            </TableCell>
          </TableRow>
        ))}
      </TableBody>
    </Table>
  )}

  <div className="flex flex-wrap items-center justify-between gap-3">
    <p className="text-sm text-muted-foreground" aria-live="polite">
      {rows.length === 0
        ? "0 customers"
        : `Showing ${(current - 1) * PAGE_SIZE + 1}–${(current - 1) * PAGE_SIZE + shown.length} of ${rows.length} customers`}
    </p>
    {pageCount > 1 && (
      <Pagination page={current} pageCount={pageCount} onPageChange={setPage} />
    )}
  </div>
</div>
Open as text

What it shows

  • Filter, then sort, then page. Each step works on the result of the one before, and changing the search or the sort returns to page one so the reader is never left on a page that no longer exists.
  • Sortable columns that say so. Each sortable header is a Button inside the th, and the sorted column carries aria-sort. Only that column has it.
  • A row header. The name cell is a th scope="row", so a screen reader announces the person's name with each value in the row. See Table.
  • Row actions with a name. Each menu button is an IconButton labelled "Actions for" and the person's name, so the buttons are distinguishable in a list of buttons. See Menu.
  • Search and paging that agree. The search box is an Input with a visible label, and Pagination is told the page count of the filtered rows, so its buttons are disabled at the ends. Choosing a row action raises a toast.
  • Results announced. "Showing 1–5 of 12 customers" is in a polite live region, so the effect of a search is heard.
  • An empty state with a way out. A search with no matches shows EmptyState with a Clear search button, not an empty table.

Limits

The data is a constant array and sorting happens in the browser. For a server-side table, keep the same query, sort and page state and fetch with it; the markup does not change. A table with many columns needs the horizontal scrolling shown on the Table page.