mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
## Problem - Filter fields could not be re-entered with Tab, and remove buttons were previously skipped. - Earlier focus rings flashed or crowded controls. Segmented filters and design system examples also had uneven layout. ## Solution - Tab now reaches each filter’s property, operator, value and remove button. Editable fields use the caret; read-only values and remove buttons keep a visible focus cue. - Refined spacing and sizing. The segmented highlight is inset and clears when focus enters a control. Both variants and the focus guidance are documented on the [design system page](https://design-system-git-dnywh-fix-filter-bar-focus-supabase.vercel.app/design-system/docs/fragments/filter-bar). |After | | --- | | <img width="462" height="126" alt="CleanShot 2026-09-24 at 15 04 23@2x" src="https://github.com/user-attachments/assets/2ee8bee2-f4fa-40f4-ada5-67bfe32384e7" /> | | <img width="362" height="96" alt="CleanShot 2026-09-24 at 15 04 47@2x" src="https://github.com/user-attachments/assets/9f42054a-f432-493d-b417-f10892676c96" /> | ## Review instructions The easiest way to test this is to open Table Editor on both production/staging and on this PR’s [deploy preview](https://studio-staging-git-dnywh-fix-filter-bar-focus-supabase.vercel.app/). Try navigating by keyboard (tab, left/right arrow) within the Filter Bar. Compare the two. - In Studio’s Table Editor or the [design system example](https://design-system-git-dnywh-fix-filter-bar-focus-supabase.vercel.app/design-system/docs/fragments/filter-bar), add a filter. Tab through its property, operator, value and remove button, then Shift+Tab back into editing. - Check that the segmented and pill examples fill their preview width and that focus cues do not collide with neighbouring controls. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary * **New Features** * Added a pill appearance for the Filter Bar, with usage guidance and interactive examples for pill and segmented presentations. * **Accessibility** * Improved keyboard navigation through filter controls, including removing a filter with the keyboard. * Clarified that an editable text field’s insertion caret can serve as its visible focus indicator; read-only fields and controls without a caret still need another visible indicator. * **Style** * Refined Filter Bar spacing and focus styling across appearances. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
219 lines
11 KiB
Plaintext
219 lines
11 KiB
Plaintext
---
|
||
title: Accessibility
|
||
description: Make Supabase work for everyone.
|
||
---
|
||
|
||
Accessibility is about making an interface work for as many people as possible across as many circumstances as possible. All of us lean on affordances that accessible experiences provide:
|
||
|
||
- Keyboard navigation
|
||
- Legible and resizable elements
|
||
- Large tap targets
|
||
- Clear and simple language
|
||
|
||
## Checklist
|
||
|
||
About to push some code? At a minimum, check your work against this list:
|
||
|
||
- Are interactive page elements [keyboard-focusable](#focus-management)?
|
||
- Are unavailable actions [discoverable and explained](#disabled-controls) for keyboard users?
|
||
- Are all elements announcable by a [screen reader](#screen-reader-support)?
|
||
- Are textual elements legible and scalable?
|
||
- Can I use this on a smaller and/or older device?
|
||
|
||
## Focus management
|
||
|
||
All interactive page elements should be reachable by keyboard. Native buttons, links with `href`, and form inputs are keyboard accessible by default. Add `tabIndex={0}` only to bespoke interactive elements. For controls using native `disabled`, tie `tabIndex` to that state (disabled controls default to `tabIndex={-1}`). Controls that use `aria-disabled` to stay discoverable should remain at `tabIndex={0}`. See [Disabled controls](#disabled-controls).
|
||
|
||
Chromium-based browsers and Firefox handle this automatically via the Tab key. Safari, by default, requires the Option key to also be held down. Enabling _Keyboard navigation_ on macOS Settings [removes this requirement](https://mayank.co/blog/safari-focus/#keyboard-navigation) but makes links non-tabbable as a result.
|
||
|
||
Interactive page elements should also provide visual feedback upon selection via a `focus-visible` state. We use one shared focus ring so users recognize this state instantly. Its color follows the theme's brighter primary hue in both themes. See [Primary and brand colors](../docs/color-usage#primary-and-brand-colors).
|
||
|
||
An editable text field can use its visible insertion caret to show focus. This can be useful for inputs embedded within a compact control, where another ring would obscure nearby elements. Check that the caret clearly identifies the active field. Read-only fields and controls without a caret still need a visible focus indicator.
|
||
|
||
### Focus ring recipe
|
||
|
||
Prefer the shared utilities over inventing local styles:
|
||
|
||
| Utility | Use when |
|
||
| ------------- | -------------------------------------------------------------------------- |
|
||
| `focus-ring` | Buttons, inputs, and most controls (offset **ring**) |
|
||
| `focus-inset` | Dense or flush surfaces such as interactive table rows (inset **outline**) |
|
||
|
||
```tsx
|
||
className = 'focus-ring'
|
||
// or
|
||
className = 'relative cursor-pointer focus-inset'
|
||
```
|
||
|
||
These expand to:
|
||
|
||
**`focus-ring`**
|
||
|
||
```txt
|
||
outline-hidden
|
||
focus-visible:ring-2
|
||
focus-visible:ring-ring
|
||
focus-visible:ring-offset-2
|
||
focus-visible:ring-offset-background
|
||
```
|
||
|
||
**`focus-inset`**
|
||
|
||
Uses `outline` (not `ring`) so it paints reliably on interactive `<tr>`s. Tailwind `ring` is `box-shadow`, which browsers often skip on `display: table-row` (notably Safari). Do not put `focus-ring` or raw `ring-*` on a `<tr>`, and do not add `outline-hidden` alongside `focus-inset`. `outline-hidden` sets `outline-style: none` and will hide the indicator.
|
||
|
||
```txt
|
||
outline: 2px solid transparent
|
||
outline-offset: -2px
|
||
transition-property: color, background-color, border-color, ...
|
||
|
||
&:focus-visible {
|
||
outline-color: var(--ring)
|
||
border-radius: var(--radius-md)
|
||
}
|
||
```
|
||
|
||
`focus-ring` keeps `outline-hidden` always on so mouse clicks do not show the browser’s default outline. `focus-inset` reserves a transparent outline instead. Its transition property list deliberately excludes outline properties so the keyboard focus indicator appears immediately, even when a call site uses `transition-all`.
|
||
|
||
Rules:
|
||
|
||
- Prefer `:focus-visible` over `:focus` so click/tap does not show a focus indicator
|
||
- Never use `outline-none` / `outline-hidden` without a visible focus indicator, such as a ring, outline, or insertion caret in an editable text field
|
||
- Always use the shared color (`ring-ring` / `outline-ring`). Variants (primary, danger, warning) do not change focus color. The shared color derives from `--primary` with enough lightness to remain visible in light mode.
|
||
- Do not animate the focus indicator; avoid `transition-all` / `transition` on controls that show one (prefer `transition-colors`)
|
||
- Prefer `focus-ring` / `focus-inset` over copy-pasting the class stack
|
||
- On interactive `<tr>`s, use `focus-inset` only. `focus-ring` will look fine in some browsers and invisible in others
|
||
|
||
When the focused element is not the thing that should show the ring (e.g. a wrapping `Link` with `group`, or an `InputGroup` parent using `:has()`), keep the explicit `group-focus-visible:ring-*` / `has-[…]:focus-visible:ring-*` stack. The utilities bake in `:focus-visible` on the same element and do not compose as `group-focus-visible:focus-ring`.
|
||
|
||
[Button](components/button) has focus, `tabIndex`, and the shared ring built-in. The same explicit `tabIndex` default is also baked into Checkbox, Switch, Select Trigger, Toggle, Accordion Trigger, Collapsible Trigger, Dropdown Menu Trigger, Popover Trigger, Dialog Trigger, Sheet Trigger, Alert Dialog Trigger, and the Sidebar Menu and action buttons. Bespoke interactive elements however, such as the below interactive [Table Row](components/table#examples), require these props to be added manually:
|
||
|
||
```tsx showLineNumbers {4-14}
|
||
<TableRow
|
||
key={id}
|
||
className="relative cursor-pointer h-16 focus-inset"
|
||
onClick={(event) => {
|
||
if (event.currentTarget !== event.target) return
|
||
handleBucketNavigation(bucket.id, event)
|
||
}}
|
||
onKeyDown={(event) => {
|
||
if (event.currentTarget !== event.target) return
|
||
if (event.key === 'Enter' || event.key === ' ') {
|
||
event.preventDefault()
|
||
handleBucketNavigation(bucket.id, event)
|
||
}
|
||
}}
|
||
tabIndex={0}
|
||
>
|
||
<TableCell>{name}</TableCell>
|
||
</TableRow>
|
||
```
|
||
|
||
Consider also affordances like `ctrl` and `meta` key support for opening in a new tab. Anything that you can do with a mouse input should be replicable by keyboard.
|
||
|
||
See the examples within [Table](components/table#examples) for more.
|
||
|
||
### Radio groups
|
||
|
||
Single-select option groups such as `<input type="radio">` should behave as a single control. Only the first item of radio groups should become focused with the Tab key. The next Tab should move focus from the group to the next focusable control.
|
||
|
||
Individual options inside of a group can be reached by arrow keys (↑ ↓ ← →). Space is the canonical key to activate radio options, with Enter being a secondary affordance.
|
||
|
||
### Jumping ahead
|
||
|
||
Some keyboard-navigable content may contain hundreds or thousands of items. Help users jump to specific content with:
|
||
|
||
- Search and filtering
|
||
- Pagination or virtualization
|
||
- Skip links and “jump to” shortcuts
|
||
|
||
Apps with persistent header and sidebar chrome should expose a skip link as the first focusable element. Use the shared [Skip to Content](fragments/skip-to-content) fragment which owns the component API, usage sample, and target landmark contract.
|
||
|
||
## Disabled controls
|
||
|
||
Native `disabled` controls are removed from the tab order. When users need to focus a disabled button to discover the action or understand why it is unavailable, add `focusableWhenDisabled`.
|
||
|
||
### Focusable when disabled
|
||
|
||
Use `disabled` with `focusableWhenDisabled` when an action is unavailable for a reason that is not obvious, especially when you show a tooltip explaining why:
|
||
|
||
- Permission gates
|
||
- Plan or infrastructure restrictions
|
||
- Business rules that block an otherwise visible action
|
||
|
||
`focusableWhenDisabled` changes how the disabled state is implemented. It sets `aria-disabled="true"`, keeps the control in the tab order, and applies disabled styling without `pointer-events-none`. Guard handlers are built into [Button](components/button). See also [MDN: aria-disabled](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-disabled).
|
||
|
||
Tab to the example below with the keyboard. The disabled button stays focusable and exposes its tooltip.
|
||
|
||
<ComponentPreview name="disabled-focusable" />
|
||
|
||
Implementation checklist for focusable disabled buttons:
|
||
|
||
- Add `focusableWhenDisabled` when the reason for disabling the control is not obvious
|
||
- Pair with a tooltip when you need to explain why
|
||
- Guard `onClick` and keyboard activation (`Enter` / `Space`) (`Button` does this automatically)
|
||
- Keep `tabIndex={0}` (`Button` does this automatically)
|
||
- Do **not** use `pointer-events-none` on the control (it blocks hover and tooltips)
|
||
|
||
```tsx showLineNumbers
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<Button disabled={unavailable} focusableWhenDisabled>
|
||
Pause project
|
||
</Button>
|
||
</TooltipTrigger>
|
||
{unavailable && <TooltipContent>{reason}</TooltipContent>}
|
||
</Tooltip>
|
||
```
|
||
|
||
In Studio, `ButtonTooltip` adds `focusableWhenDisabled` to disabled buttons with tooltip text automatically.
|
||
|
||
### Page-level context
|
||
|
||
Tooltips alone are not enough for significant restrictions. Pair focusable disabled controls with visible page context (for example: an [Admonition](fragments/admonition), empty state, or inline copy) so the reason is available even without hover or focus.
|
||
|
||
<ComponentPreview name="disabled-unavailable-with-notice" />
|
||
|
||
## Screen readers
|
||
|
||
Textual elements are supported out-of-the-box by screen readers.
|
||
|
||
### Imagery
|
||
|
||
Images should have their contents described with an `alt` attribute. Write an objective description of the content rather than its context. For example:
|
||
|
||
```tsx showLineNumbers {2}
|
||
// Correct: painting a picture with words
|
||
<img src="beagle.png" alt="A tricolor beagle galloping through a grassy field, ears in the air" />
|
||
|
||
// Incorrect: Unhelpful context
|
||
<img src="beagle.png" alt="Our logo" />
|
||
```
|
||
|
||
Icons and other visual elements that aren’t strictly images should use the `aria-label` attribute. For example:
|
||
|
||
```tsx showLineNumbers {2}
|
||
<BucketTableCell>
|
||
<BucketIcon aria-label="bucket icon" size={16} />
|
||
</BucketTableCell>
|
||
```
|
||
|
||
Visual elements that are _purely_ visual aids may be removed from the accessibility tree via the `aria-hidden` attribute. For example:
|
||
|
||
```tsx showLineNumbers {2}
|
||
<BucketTableCell>
|
||
<ChevronRight aria-hidden={true} size={14} />
|
||
</BucketTableCell>
|
||
```
|
||
|
||
Never use `aria-hidden={true}` on focusable elements, since these are critical pieces of functionality.
|
||
|
||
### Scaffolding
|
||
|
||
Some scaffolding elements only make sense visually, in the context of surrounding visual content. For example: a table column for actions may not have a visual _Actions_ label because its purpose is obvious (by nearby contents) to a sighted person. For everyone else’s sake, this column should be titled with `sr-only` text:
|
||
|
||
```tsx showLineNumbers {2}
|
||
<TableHead>
|
||
<span className="sr-only">Actions</span>
|
||
</TableHead>
|
||
```
|