From 9a03f3cd9cdbe4c9adb797b32c35002a358456f2 Mon Sep 17 00:00:00 2001
From: Danny White <3104761+dnywh@users.noreply.github.com>
Date: Mon, 28 Sep 2026 10:13:53 +1000
Subject: [PATCH] fix(studio): show keyboard focus on filter bar (#50827)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## 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 |
| --- |
| |
| |
## 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.
## 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.
---
.../content/docs/accessibility.mdx | 4 +-
.../content/docs/fragments/filter-bar.mdx | 29 +++++++
.../default/example/filter-bar-demo.tsx | 27 ++++---
.../default/example/filter-bar-pill-demo.tsx | 32 ++++++++
.../example/filter-bar-segmented-demo.tsx | 30 +++++++
apps/design-system/registry/examples.ts | 12 +++
e2e/studio/features/filter-bar.spec.ts | 4 +-
e2e/studio/utils/filter-bar-helpers.ts | 3 +-
.../src/FilterBar/FilterBar.test.tsx | 78 +++++++++++++++++++
.../ui-patterns/src/FilterBar/FilterBar.tsx | 2 +-
.../src/FilterBar/FilterBarContext.tsx | 2 +
.../src/FilterBar/FilterCondition.tsx | 66 +++++++++++-----
.../ui-patterns/src/FilterBar/FilterGroup.tsx | 2 +-
13 files changed, 255 insertions(+), 36 deletions(-)
create mode 100644 apps/design-system/registry/default/example/filter-bar-pill-demo.tsx
create mode 100644 apps/design-system/registry/default/example/filter-bar-segmented-demo.tsx
diff --git a/apps/design-system/content/docs/accessibility.mdx b/apps/design-system/content/docs/accessibility.mdx
index 93c1d9c13a2..58b03b8fd9a 100644
--- a/apps/design-system/content/docs/accessibility.mdx
+++ b/apps/design-system/content/docs/accessibility.mdx
@@ -28,6 +28,8 @@ Chromium-based browsers and Firefox handle this automatically via the Tab key. S
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:
@@ -75,7 +77,7 @@ transition-property: color, background-color, border-color, ...
Rules:
- Prefer `:focus-visible` over `:focus` so click/tap does not show a focus indicator
-- Never use `outline-none` / `outline-hidden` without a ring or outline replacement
+- 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
diff --git a/apps/design-system/content/docs/fragments/filter-bar.mdx b/apps/design-system/content/docs/fragments/filter-bar.mdx
index 50a5b2a9e2b..669004d818d 100644
--- a/apps/design-system/content/docs/fragments/filter-bar.mdx
+++ b/apps/design-system/content/docs/fragments/filter-bar.mdx
@@ -50,6 +50,34 @@ export function FilterDemo() {
}
```
+## Variants
+
+The default variant joins filters into a segmented bar. Use the pill variant to show each filter as a separate rounded chip:
+
+### Segmented
+
+
+
+### Pill
+
+
+
+```tsx
+
+```
+
## API Reference
### FilterProperty
@@ -95,6 +123,7 @@ interface FilterCondition {
| onFreeformTextChange | (text: string) => void | Callback when free-form text changes |
| actions | FilterBarAction[]? | Optional custom actions to show in the menu |
| isLoading | boolean? | If true, dims the bar while work is in progress |
+| variant | 'default' \| 'pill'? | Filter appearance. Defaults to 'default' |
## Custom actions (e.g. AI)
diff --git a/apps/design-system/registry/default/example/filter-bar-demo.tsx b/apps/design-system/registry/default/example/filter-bar-demo.tsx
index 456f71a7945..604043a5cbd 100644
--- a/apps/design-system/registry/default/example/filter-bar-demo.tsx
+++ b/apps/design-system/registry/default/example/filter-bar-demo.tsx
@@ -118,19 +118,28 @@ const initialFilters: FilterGroup = {
conditions: [],
}
-export default function FilterBarDemo() {
+function FilterBarExample({ variant }: { variant: 'default' | 'pill' }) {
const [filters, setFilters] = useState(initialFilters)
const [freeformText, setFreeformText] = useState('')
return (
-