Files
supabase/apps/design-system/content/docs/components/table.mdx
T
Danny White c8aca8d3a0 chore(design-system): standardise keyboard focus rings (#41575)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

UI / design-system consistency (accessibility).

## What is the current behavior?

Keyboard focus rings are inconsistent across Studio and `packages/ui`:

- Custom Button uses thick `outline` with per-variant colours (brand /
grey / destructive / warning)
- Form controls use muted grey rings (`ring-background-control`)
- Tabs / NavMenu / Radio use soft brand `ring-ring`
- Studio `.inset-focus` uses dark green `outline-brand-600`

Related: [DEPR-354](https://linear.app/supabase/issue/DEPR-354).

## What is the new behavior?

One shared focus recipe, exposed as Tailwind `@utility` classes in
`packages/config/css/utilities.css`:

| Utility | Use when |
| --- | --- |
| `focus-ring` | Buttons, inputs, most controls (offset ring) |
| `focus-inset` | Dense/flush surfaces such as interactive table rows
(renamed from `inset-focus`) |

```txt
# focus-ring
outline-hidden
focus-visible:ring-2
focus-visible:ring-ring
focus-visible:ring-offset-2
focus-visible:ring-offset-background
```

Applied on Button, shadcn form controls, Menu/NavMenu, Command palette
trigger, Studio table rows, and related call sites. Documented in the
design-system accessibility docs. Variants do not change focus ring
colour.

When the ring must appear on a different element than the focused one
(e.g. Menu + ProductMenu `Link` via `group-focus-visible`, or InputGroup
via `:has()`), keep an explicit ring stack. The utilities bake in
`:focus-visible` on the same element.

## Additional context

**Out of scope**

- Full `packages/ui` / Studio / www sweep
- Legacy Studio form-group green box-shadow cleanup
- ESLint rule for bare `outline-none`

## Test plan

Prefer Safari (“hard mode” for `tabIndex`). Expect one soft brand ring
everywhere: not grey, not solid green outline.

### Design system

- [ ]
[Accessibility](https://design-system-git-dnywh-choreimprove-tab-focus-styles-supabase.vercel.app/design-system/docs/accessibility):
recipe docs match what you see
- [ ]
[Button](https://design-system-git-dnywh-choreimprove-tab-focus-styles-supabase.vercel.app/design-system/docs/components/button):
Tab primary / default / danger; same ring colour
- [ ] [Table → Row-level
navigation](https://design-system-git-dnywh-choreimprove-tab-focus-styles-supabase.vercel.app/design-system/docs/components/table#row-level-navigation):
Tab an interactive row; inset outline (`focus-inset`) sits inside the
row

### Studio

- [ ] **Org home → table view** (`/organizations/_` or org projects):
switch to the table layout, Tab onto a project row; inset outline sits
inside the row (list/card view uses CardButton, not `focus-inset`)
- [ ] **Project sidebar** (Database, Auth, Storage, …): Tab the main
product nav links; ring follows the focused item (not the nested section
menus like Tables / Roles)
- [ ] **Storage → Files**: Tab a bucket row; same inset outline as org
table rows
- [ ] **Project Settings → General** (or Compute and Disk): Tab through
inputs, checkboxes, switches, selects; same offset ring, no ring on
mouse click
- [ ] **Header ⌘K** (desktop width): Tab to the search control after
Feedback; same soft brand `focus-ring` (was a thicker
`ring-border-strong` before)
- [ ] **Table Editor or SQL Editor tabs**: focus a tab, Tab to × if
active; close shows a ring
- [ ] **Light + dark**: ring stays visible against both backgrounds
2026-07-22 12:10:07 -04:00

242 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Table
description: A responsive table component for presenting data inline.
component: true
source:
shadcn: true
---
Table is an opinionated `<table>` element for basic tabular data presentation. It is usually presented within a [Card](../components/card).
<ComponentPreview name="table-demo" peekCode wide />
## Usage
```tsx
import {
Card,
Table,
TableBody,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from 'ui'
```
```tsx
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead>Column 1</TableHead>
<TableHead>Column 2</TableHead>
<TableHead>Column 3</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Value 1</TableCell>
<TableCell>Value 2</TableCell>
<TableCell>Value 3</TableCell>
</TableRow>
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={2}>Total</TableCell>
<TableCell>Value</TableCell>
</TableRow>
</TableFooter>
</Table>
</Card>
```
## Layout
Columns will naturally take the width of their content unless otherwise specified. Specific column widths may be useful for controlling layout shift or for aligning multiple Table instances vertically.
Table Head includes a `whitespace-nowrap` utility class to prevent its text from wrapping. Table Cell must have any custom width specified, as there are legitimate use cases for both wrapping (or truncated) text.
The Table component is wrapped in a `ShadowScrollArea` by default, which provides horizontal scrolling on smaller screens when the table content exceeds the viewport width.
## Structure
Table is comprised of the following [Shadcn primitives](https://ui.shadcn.com/docs/components/table) which map directly to their HTML counterparts:
| Component | HTML element |
| ------------- | ------------------------------------------------------------------------------------------- |
| Table | [`<table>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/table) |
| Table Body | [`<tbody>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/tbody) |
| Table Caption | [`<caption>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/caption) |
| Table Cell | [`<td>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/td) |
| Table Footer | [`<tfoot>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/tfoot) |
| Table Head | [`<th>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/th) |
| Table Header | [`<thead>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/thead) |
| Table Row | [`<tr>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/tr) |
Table and its child components follow their HTML element counterparts for semantics, accessibility, and best practices.
### Table Caption
Table Caption describes a table’s purpose or content. Think of it like the label of a diagram. It should only contain a single text node.
Just like its `<caption>` counterpart, Table Caption should be the first child of a Table element (before a Table Body) irrespective of its final visual positioning.
### Table Footer
Table Footer is a semantic grouping for the final rows of a table, typically used for column summaries, totals, or footnotes that relate to the data in the body of the table.
Table Footer typically contains the same child elements as a Table Body, such as Table Row and Table Cell, and is a sibling of Table Body.
## Examples
### Empty state
When displaying an empty state within a table, dim the Table Head text to indicate that no data is present. Use consistent zero results presentation across your application to maintain a cohesive user experience.
<ComponentPreview name="empty-state-zero-items-table" />
To prevent the empty state row from being highlighted on hover, apply the `[&>td]:hover:bg-inherit` class to the Table Row containing the empty state message.
```tsx showLineNumbers {1}
<TableRow className="[&>td]:hover:bg-inherit">
<TableCell colSpan={3}>
<p className="text-sm text-foreground">No results found</p>
<p className="text-sm text-foreground-lighter">
Your search for “test” did not return any results
</p>
</TableCell>
</TableRow>
```
See [Empty States](../ui-patterns/empty-states) for more information.
### Sortable columns
Use `TableHeadSort` inside Table Head to enable column sorting. This component provides visual indicators for sort state (ascending, descending, or unsorted) and handles click interactions.
```tsx
import { TableHeadSort } from 'ui'
```
| Prop | Type | Description |
| -------------- | -------------------------- | ------------------------------------------------------------------ |
| `column` | `string` | Unique identifier for the column |
| `currentSort` | `string` | Current sort state in format `"column:order"` (e.g., `"name:asc"`) |
| `onSortChange` | `(column: string) => void` | Callback fired when column header is clicked |
| `children` | `ReactNode` | The label text for the column header |
| `className` | `string` | Optional additional CSS classes |
```tsx showLineNumbers {2}
<TableHead>
<TableHeadSort column="name" currentSort={sort} onSortChange={handleSortChange}>
Name
</TableHeadSort>
</TableHead>
```
The component displays:
- An up arrow when the column is sorted ascending
- A down arrow when the column is sorted descending
- A chevrons icon when the column is not currently sorted (visible on hover)
<ComponentPreview name="table-sort" />
For TanStack tables, prefer the shared adapter instead of reimplementing the `TableHeadSort` bridge in each table.
```tsx
import { TanStackTableHeadSort } from 'ui-patterns/Table'
```
```tsx showLineNumbers
const columns: ColumnDef<Row>[] = [
{
accessorKey: 'name',
header: ({ column }) => <TanStackTableHeadSort column={column}>Name</TanStackTableHeadSort>,
},
]
```
This keeps TanStack tables aligned with the same `TableHeadSort` visual treatment and sorting cycle used by manual tables.
### Row icons
When adding icon columns to your table, use [Accessibility](../accessibility) markup by including a screen reader-only label in the corresponding Table Head using the `sr-only` class. This ensures that assistive technologies can properly identify the column's purpose. Remove these icon cells when loading or displaying zero results to maintain a clean and consistent table structure.
<ComponentPreview name="table-icons" />
```tsx showLineNumbers {4, 5, 13, 14}
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-1">
<span className="sr-only">Icon</span>
</TableHead>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className="w-1">
<IconName size={16} className="text-foreground-muted" />
</TableCell>
<TableCell>Value</TableCell>
<TableCell>Value</TableCell>
</TableRow>
</TableBody>
</Table>
```
### Actions
Action should be placed in the last column of each Table Row to maintain a logical reading flow. Users scan table data from left to right, so positioning actions on the right ensures they encounter the primary information first before reaching interactive controls.
For compact controls in action cells, add the `hit-area-2` utility to increase the tap target by `8px` on each side without changing visual layout. When actions sit next to each other, keep at least `gap-x-2` spacing: with two adjacent `hit-area-2` buttons, hit areas meet at the midpoint but do not overlap.
#### Multiple actions
When multiple actions are available for a row, display one primary action as a button and place additional options in an overflow menu. This keeps the table clean and scannable while providing access to all necessary actions without overwhelming the user.
<ComponentPreview name="table-actions" />
#### Cross-links
You may need to link to related resources from within table cells. In these cases, use the `text-link-table-cell` CSS class to provide a visual hint that the text is interactive while maintaining its appearance as tertiary content.
<ComponentPreview name="table-cross-link" />
Avoid making the entire row interactive when using cross-links. Multiple interactive areas within a single row increases the chance of mis-taps and creates ambiguity about which action will be triggered. Keep cross-links as discrete, scannable elements within their respective cells, and maintain a dedicated action column for primary row-level actions.
#### Row-level navigation
The entire Table Row may be tappable. This pattern is most effective when navigation is the sole or primary action for each row, as it provides a large, easy-to-target interaction area.
Avoid adding other actions when using row-level navigation, as multiple interactive areas within a single row create competing affordances and increase interaction complexity.
<ComponentPreview name="table-row-link" />
When implementing row-level navigation, pay close attention to [Accessibility](/accessibility#focus-management) requirements. The row must be keyboard accessible with proper focus management. Also consider these affordances:
- Handle `Enter` and `Space` key presses for activation
- Provide visual focus indicators using classes like `focus-inset`
- Support modifier keys (`Ctrl`/`Cmd`, middle-click) for opening links in new tabs
- Consider using the shared `createNavigationHandler` function to handle modifier keys
- Avoid bubbling up action events from _within_ the row
#### Row navigation with actions
If you must combine row-level navigation with additional actions, nest all secondary actions in an overflow menu and maintain a [ChevronRight](https://lucide.dev/icons/chevron-right) visual indicator to signal that the row is navigable. This pattern preserves the primary navigation affordance while keeping secondary actions accessible but unobtrusive.
<ComponentPreview name="table-row-link-actions" />
This hybrid approach requires careful attention to event handling. Ensure that taps on action buttons stop event propagation to prevent triggering row navigation, and maintain clear visual separation between the navigable row area and the action controls.
## Related
Use the Table component for simple, static tabular data presentation with a fixed number of rows. Consider using [Data Table](../ui-patterns/tables#data-table) for more complex cases as it provides built-in sorting, filtering, and pagination capabilities.
Refer to [Tables](../ui-patterns/tables) for broader guidance and best practices on presenting tabular data.