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
This commit is contained in:
Danny White authored and GitHub committed 2026-07-22 12:10:07 -04:00
1 parent 71410b187a
commit c8aca8d3a0
45 files changed
+136 -101

No files matched your search

@@ -25,14 +25,68 @@ All interactive page elements should be reachable by keyboard. Given the below i
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 consistent focus styles such as `inset-focus` so users recognize this state instantly.
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.
[Button](components/button) has all of the above 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:
### 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
&:focus-visible {
outline-style: solid
outline-width: 2px
outline-offset: -2px
outline-color: var(--ring)
border-radius: var(--radius-md)
}
```
`outline-hidden` is always on (not `focus-visible:`-prefixed) so mouse click does not show the browser’s default outline; the focus indicator replaces it for keyboard focus only.
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
- Always use the shared color (`ring-ring` / `outline-ring`). Variants (primary, danger, warning) do not change focus colour
- 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 inset-focus"
className="relative cursor-pointer h-16 focus-inset"
onClick={(event) => {
if (event.currentTarget !== event.target) return
handleBucketNavigation(bucket.id, event)
@@ -144,5 +144,6 @@ Inside [Admonition](../fragments/admonition#split-button-with-dropdown) actions,
- Enabled buttons default to `tabIndex={0}` (keyboard accessible)
- Disabled buttons default to `tabIndex={-1}` (removed from tab order)
- You can still override with an explicit `tabIndex` prop when needed
- Keyboard focus uses the shared `focus-ring` utility; variants do not change ring colour
You therefore don't need to manually set `tabIndex`, as Button handles it automatically based on its `disabled` state.
@@ -221,7 +221,7 @@ Avoid adding other actions when using row-level navigation, as multiple interact
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 `inset-focus`
- 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
@@ -70,7 +70,7 @@ export default function TableRowLinkActions() {
{policies.map((policy) => (
<TableRow
key={policy.id}
className="relative cursor-pointer inset-focus"
className="relative cursor-pointer focus-inset"
onClick={(event) => {
if (event.currentTarget !== event.target) return
handlePolicyNavigation(policy.id, event)
@@ -54,7 +54,7 @@ export default function TableRowLink() {
{buckets.map((bucket) => (
<TableRow
key={bucket.id}
className="relative cursor-pointer inset-focus"
className="relative cursor-pointer focus-inset"
onClick={(event) => {
if (event.currentTarget !== event.target) return
handleBucketNavigation(bucket.id, event)
@@ -62,7 +62,7 @@ export const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemP
onAuxClick={handleNavigation}
onKeyDown={handleNavigation}
tabIndex={0}
className="cursor-pointer inset-focus"
className="cursor-pointer focus-inset"
>
<TableCell>
<p className="text-sm text-foreground whitespace-nowrap py-2">{item.name}</p>
@@ -75,7 +75,7 @@ export const ProjectTableRow = ({
return (
<>
<TableRow
className="cursor-pointer hover:bg-surface-200 inset-focus"
className="cursor-pointer hover:bg-surface-200 focus-inset"
onClick={handleNavigation}
onAuxClick={handleNavigation}
onKeyDown={handleNavigation}
@@ -321,7 +321,7 @@ export const PlatformWebhooksEndpointDetails = ({
paginatedDeliveries.map((row) => (
<TableRow
key={row.id}
className="cursor-pointer inset-focus"
className="cursor-pointer focus-inset"
onClick={() => onOpenDelivery(row.original.id)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
@@ -170,7 +170,7 @@ export const PlatformWebhooksEndpointList = ({
return (
<TableRow
key={endpoint.id}
className="relative cursor-pointer inset-focus"
className="relative cursor-pointer focus-inset"
onClick={createNavigationHandler(
`${webhooksHref}/${encodeURIComponent(endpoint.id)}`,
router
@@ -127,7 +127,7 @@ export function JitDbAccessRulesTable({
return (
<TableRow
key={user.id}
className={rowIsInteractive ? 'relative inset-focus cursor-pointer' : undefined}
className={rowIsInteractive ? 'relative focus-inset cursor-pointer' : undefined}
onClick={
rowIsInteractive
? (event) => {
@@ -154,7 +154,7 @@ export const AnalyticsBuckets = () => {
return (
<TableRow
key={bucket.name}
className="relative cursor-pointer h-16 inset-focus"
className="relative cursor-pointer h-16 focus-inset"
onClick={handleBucketNavigation}
onAuxClick={handleBucketNavigation}
onKeyDown={handleBucketNavigation}
@@ -80,7 +80,7 @@ export const BucketTableRow = ({
key={bucket.id}
data-bucket-id={bucket.id}
className={cn(
'relative cursor-pointer h-16 group inset-focus',
'relative cursor-pointer h-16 group focus-inset',
isDisabled && 'opacity-50 [&>td]:hover:bg-transparent cursor-not-allowed'
)}
onClick={handleRowActivate}
@@ -107,7 +107,7 @@ export const BucketTableRow = ({
<BucketTableRow
key={bucket.id}
data-bucket-id={bucket.id}
className="relative cursor-pointer h-16 group inset-focus"
className="relative cursor-pointer h-16 group focus-inset"
onClick={handleBucketNavigation}
onAuxClick={handleBucketNavigation}
onKeyDown={handleBucketNavigation}
@@ -130,7 +130,7 @@ export const VectorsBuckets = () => {
return (
<TableRow
key={id}
className="relative cursor-pointer h-16 inset-focus"
className="relative cursor-pointer h-16 focus-inset"
onClick={handleBucketNavigation}
onAuxClick={handleBucketNavigation}
onKeyDown={handleBucketNavigation}
@@ -195,7 +195,7 @@ export const ProjectNeedsSecuringView = ({
return (
<TableRow
key={getTableKey(table)}
className="relative cursor-pointer inset-focus"
className="relative cursor-pointer focus-inset"
onClick={(event) => {
trackViewPolicies()
handleNavigation(event)
@@ -158,7 +158,7 @@ export const SortableTab = ({
'absolute top-1/2 right-2.5 z-10 -translate-y-1/2',
'flex size-5 items-center justify-center rounded-xs',
'opacity-0 group-hover/tab:opacity-100 group-focus-within/tab:opacity-100 focus-visible:opacity-100',
'hover:bg-200 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
'hover:bg-200 focus-ring',
'cursor-pointer'
)}
>
+1 -1
View File
@@ -259,7 +259,7 @@ export const EditorTabs = () => {
className={cn(
'absolute top-1/2 right-3 z-10 flex -translate-y-1/2 items-center justify-center rounded-xs',
'opacity-0 group-hover/new-tab:opacity-100 group-focus-within/new-tab:opacity-100 focus-visible:opacity-100',
'hover:bg-200 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
'hover:bg-200 focus-ring',
'cursor-pointer'
)}
>
@@ -153,7 +153,7 @@ export function DataTableFilterCheckbox<TData>({
className={cn(
'text-xs text-muted-foreground hover:text-foreground',
'absolute inset-y-0 right-0 hidden bg-surface-100 group-hover:flex group-focus-within:flex items-center cursor-pointer',
'ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2'
'focus-ring'
)}
>
<span className="pl-1 pr-2">Only</span>
@@ -123,7 +123,7 @@ export function DataTableFilterCheckboxAsync<TData>({
className={cn(
'text-xs text-muted-foreground hover:text-foreground',
'absolute inset-y-0 right-0 hidden bg-surface-100 group-hover:flex group-focus-within:flex items-center cursor-pointer',
'ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2'
'focus-ring'
)}
>
<span className="px-2">Only</span>
@@ -187,12 +187,7 @@ export function DataTableSheetRowAction<TData, TFields extends DataTableFilterFi
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
asChild
className={cn(
'rounded-md ring-offset-background',
'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'relative py-0',
className
)}
className={cn('rounded-md', 'focus-ring', 'relative py-0', className)}
onKeyDown={(e) => {
if (e.key === 'ArrowDown') {
// REMINDER: default behavior is to open the dropdown menu
@@ -22,7 +22,7 @@ export const Slider = forwardRef<
</SliderPrimitive.Track>
{initialValue.map((_, index) => (
<Fragment key={index}>
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border-2 border-primary bg-background transition-colors focus-ring disabled:pointer-events-none disabled:opacity-50" />
</Fragment>
))}
</SliderPrimitive.Root>
@@ -85,7 +85,7 @@ export const ProductMenuItem = ({
}
const link = (
<Link href={url} className="block" target={target} onClick={onClick}>
<Link href={url} className="group block" target={target} onClick={onClick}>
{menuItem}
</Link>
)
-1
View File
@@ -1,5 +1,4 @@
import '@/styles/code.css'
import '@/styles/focus.css'
import '@/styles/globals.css'
import '@/styles/graphiql-base.css'
import '@/styles/grid.css'
-1
View File
@@ -2,7 +2,6 @@
import 'react-data-grid/lib/styles.css'
import '@/styles/code.css'
import '@/styles/focus.css'
// Vite-only: defines @font-face for the custom fonts. The Next pipeline
// (pages/_app.tsx) loads these via next/font instead, so this import has no
// counterpart there — but dropping it under Vite makes the browser fall back
-6
View File
@@ -1,6 +0,0 @@
@reference "./globals.css";
/* Focus styles for keyboard navigation on interactive table rows */
.inset-focus {
@apply ease-out duration-100 focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-brand-600 focus-visible:rounded-md transition-all;
}
+26
View File
@@ -180,3 +180,29 @@
-webkit-text-fill-color: currentColor;
}
}
/*
* Canonical keyboard focus rings. Prefer these over inventing local ring/outline
* stacks.
*
* - focus-ring: default offset ring (buttons, inputs, most controls).
* `outline-hidden` stays always-on so mouse click does not show the browser
* default; the ring is `:focus-visible` only.
* - focus-inset: dense/flush surfaces such as interactive <tr>s. Must use
* outline (not ring/box-shadow) — box-shadow on table-row is unreliable,
* especially in Safari. Do NOT pair with outline-hidden: that sets
* outline-style: none and suppresses the focus outline.
*/
@utility focus-ring {
@apply outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
@utility focus-inset {
&:focus-visible {
outline-style: solid;
outline-width: 2px;
outline-offset: -2px;
outline-color: var(--ring);
border-radius: var(--radius-md, 0.375rem);
}
}
@@ -160,7 +160,7 @@ function CommandMenuTrigger({ children }: PropsWithChildren) {
'rounded-md border border-input bg-background',
'text-sm',
'hover:bg-accent hover:text-accent-foreground',
'ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'focus-ring',
'disabled:pointer-events-none disabled:opacity-50',
'transition-colors',
childFromProps.props.className
@@ -182,6 +182,7 @@ function CommandMenuTriggerInput({
<CommandMenuTrigger>
<button
type="button"
tabIndex={0}
className={cn(
'group',
'grow md:min-w-44 xl:min-w-56 h-[30px] rounded-md',
@@ -189,8 +190,8 @@ function CommandMenuTriggerInput({
'flex items-center justify-between',
'bg-surface-100/75 text-foreground-lighter border',
'hover:bg-surface-100/100 hover:border-stronger',
'focus-visible:outline-hidden focus-visible:ring-4 focus-visible:ring-border-strong focus-visible:ring-offset-1 focus-visible:ring-offset-background',
'transition',
'focus-ring',
'transition-colors',
className
)}
>
@@ -585,7 +585,7 @@ const MultiSelectorItem = React.forwardRef<
'peer h-4 w-4 shrink-0 rounded-sm border border-control bg-control/25 ring-offset-background',
'transition-colors duration-150 ease-in-out',
'hover:border-strong',
'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'focus-ring',
'disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-foreground data-[state=checked]:text-background',
isSelected ? 'bg-foreground text-background' : '[&_svg]:invisible'
)}
+2 -25
View File
@@ -20,12 +20,8 @@ const buttonVariants = cva(
ease-out
duration-200
rounded-md
outline-hidden
transition-all
outline-0
focus-visible:outline-solid
focus-visible:outline-4
focus-visible:outline-offset-1
transition-colors
focus-ring
border
`,
{
@@ -37,35 +33,26 @@ const buttonVariants = cva(
text-foreground
border-brand-500/75 dark:border-brand/30
hover:border-brand-600 dark:hover:border-brand
focus-visible:outline-brand-600
data-[state=open]:bg-brand-400/80 dark:data-[state=open]:bg-brand-500/80
data-[state=open]:outline-brand-600
`,
default: `
text-foreground
bg-alternative dark:bg-muted hover:bg-selection
border-strong hover:border-stronger
focus-visible:outline-border-strong
data-[state=open]:bg-selection
data-[state=open]:outline-border-strong
data-[state=open]:border-button-hover
`,
secondary: `
bg-foreground
text-background hover:text-background/80
focus-visible:text-border-control
border-foreground-light hover:border-foreground-lighter
focus-visible:outline-border-strong
data-[state=open]:border-foreground-lighter
data-[state=open]:outline-border-strong
`,
outline: `
text-foreground
bg-transparent
border-strong hover:border-foreground-muted
focus-visible:outline-border-strong
data-[state=open]:border-stronger
data-[state=open]:outline-border-strong
`,
dashed: `
text-foreground
@@ -73,9 +60,7 @@ const buttonVariants = cva(
border-dashed
border-strong hover:border-stronger
bg-transparent
focus-visible:outline-border-strong
data-[state=open]:border-stronger
data-[state=open]:outline-border-strong
`,
link: `
text-brand-600
@@ -83,17 +68,13 @@ const buttonVariants = cva(
border-transparent/0
hover:bg-brand-400
shadow-none
focus-visible:outline-border-strong
data-[state=open]:bg-brand-400
data-[state=open]:outline-border-strong
`,
text: `
text-foreground
hover:bg-accent
shadow-none
focus-visible:outline-border-strong
data-[state=open]:bg-accent
data-[state=open]:outline-border-strong
border-transparent
`,
danger: `
@@ -101,20 +82,16 @@ const buttonVariants = cva(
bg-destructive-300 dark:bg-destructive-400 hover:bg-destructive-400 dark:hover:bg-destructive/50
border-border-destructive hover:border-destructive
hover:text-hi-contrast
focus-visible:outline-destructive
data-[state=open]:border-destructive
data-[state=open]:bg-destructive-400 dark:data-[state=open]:bg-destructive/50
data-[state=open]:outline-destructive
`,
warning: `
text-foreground
bg-warning-300 dark:bg-warning-400 hover:bg-warning-400 dark:hover:bg-warning/50
border-border-warning hover:border-warning
hover:text-hi-contrast
focus-visible:outline-warning
data-[state=open]:border-warning
data-[state=open]:bg-warning-400 dark:data-[state=open]:bg-warning/50
data-[state=open]:outline-warning
`,
},
block: {
+7 -1
View File
@@ -45,7 +45,13 @@ interface ItemProps {
export const menuItemVariants = cva(
cn(
'cursor-pointer flex space-x-3 items-center outline-hidden focus-visible:ring-1 ring-foreground-muted focus-visible:z-10 group'
// focus-ring: when the item itself is focused.
// group-focus-visible: when a wrapping Link/button (e.g. ProductMenu) is the
// focus target — cannot use focus-ring here (utility targets :focus-visible
// on the same element).
'cursor-pointer flex space-x-3 items-center focus-visible:z-10 group',
'focus-ring',
'group-focus-visible:ring-2 group-focus-visible:ring-ring group-focus-visible:ring-offset-2 group-focus-visible:ring-offset-background'
),
{
variants: {
+1 -1
View File
@@ -33,7 +33,7 @@ export const NavMenuItem = forwardRef<HTMLLIElement, NavMenuItemProps>(
aria-selected={active ? 'true' : 'false'}
data-state={active ? 'active' : 'inactive'}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap text-sm ring-offset-background transition-all focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground text-foreground-lighter hover:text-foreground data-[state=active]:border-foreground border-b-2 border-transparent *:py-1.5',
'inline-flex items-center justify-center whitespace-nowrap text-sm transition-colors focus-ring disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground text-foreground-lighter hover:text-foreground data-[state=active]:border-foreground border-b-2 border-transparent *:py-1.5',
className
)}
{...props}
@@ -58,10 +58,10 @@ const RadioGroupCardItem = React.forwardRef<
group-hover:border-foreground-muted
ring-offset-background
group-focus:outline-hidden
group-focus-visible:ring-2 group-focus-visible:ring-ring group-focus-visible:ring-offset-2
group-focus-visible:ring-2 group-focus-visible:ring-ring group-focus-visible:ring-offset-2 group-focus-visible:ring-offset-background
group-disabled:cursor-not-allowed group-disabled:opacity-50
flex items-center justify-center
transition
transition-colors
"
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
@@ -8,7 +8,7 @@ import { cn } from '../../../lib/utils/cn'
import { getExplicitTabIndex } from '../../../lib/utils/getExplicitTabIndex'
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-foreground-muted focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-ring disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
@@ -20,7 +20,7 @@ const Checkbox = React.forwardRef<
'peer flex items-center justify-center h-4 w-4 shrink-0 rounded-sm border border-control bg-control/25 ring-offset-background',
'transition-colors duration-150 ease-in-out',
'hover:border-strong',
'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-background-control focus-visible:ring-offset-2 focus-visible:ring-offset-foreground-muted',
'focus-ring',
'disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-foreground data-[state=checked]:text-background',
className
)}
@@ -55,8 +55,9 @@ function InputGroup({
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
// Focus state.
'has-[[data-slot=input-group-control]:focus-visible]:outline-hidden has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot=input-group-control]:focus-visible]:ring-background-control has-[[data-slot=input-group-control]:focus-visible]:ring-offset-2 has-[[data-slot=input-group-control]:focus-visible]:ring-offset-foreground-muted',
// Focus state. Parent listens via :has() — cannot use the focus-ring utility
// (that utility targets :focus-visible on the element itself).
'has-[[data-slot=input-group-control]:focus-visible]:outline-hidden has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot=input-group-control]:focus-visible]:ring-ring has-[[data-slot=input-group-control]:focus-visible]:ring-offset-2 has-[[data-slot=input-group-control]:focus-visible]:ring-offset-background',
// Error state.
'has-[[data-slot][aria-invalid=true]]:bg-destructive-200 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive-400 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
@@ -12,7 +12,7 @@ export interface InputProps
export const InputVariants = cva(
cn(
'flex h-10 w-full rounded-md border border-control read-only:border-button bg-foreground/[.026] px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-foreground-muted read-only:text-foreground-light',
'focus:ring-background-control focus:border-control focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-background-control focus-visible:ring-offset-2 focus-visible:ring-offset-foreground-muted disabled:cursor-not-allowed disabled:text-foreground-muted',
'focus:border-control focus-ring disabled:cursor-not-allowed disabled:text-foreground-muted',
'aria-[] aria-[invalid=true]:bg-destructive-200 aria-[invalid=true]:border-destructive-400 aria-[invalid=true]:focus:border-destructive aria-[invalid=true]:focus-visible:border-destructive'
),
{
@@ -22,7 +22,7 @@ const RadioGroupItem = React.forwardRef<
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
'relative aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'relative aspect-square h-4 w-4 rounded-full border border-primary text-primary focus-ring disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
@@ -47,7 +47,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex w-full items-center justify-between rounded-md border border-strong hover:border-stronger bg-alternative dark:bg-muted hover:bg-selection text-xs ring-offset-background-control data-[placeholder]:text-foreground-lighter focus:outline-hidden ring-border-control focus:ring-2 focus:ring-background-control focus:ring-offset-2 focus-visible:ring-offset-foreground-muted disabled:cursor-not-allowed disabled:opacity-50 transition-all duration-200',
'flex w-full items-center justify-between rounded-md border border-strong hover:border-stronger bg-alternative dark:bg-muted hover:bg-selection text-xs data-[placeholder]:text-foreground-lighter ring-border-control focus-ring disabled:cursor-not-allowed disabled:opacity-50 transition-colors duration-200',
'data-[state=open]:bg-selection data-[state=open]:border-stronger',
'gap-2',
'[&>span]:truncate text-left', // [kemal] This is to prevent double lines rendering when a string is particularly long.
@@ -41,7 +41,7 @@ function Slider({
<SliderPrimitive.Thumb
data-slot="slider-thumb"
key={index}
className="block h-5 w-5 rounded-full border-2 border-background-surface-100 bg-foreground ring-offset-background transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
className="block h-5 w-5 rounded-full border-2 border-background-surface-100 bg-foreground transition-colors focus-ring disabled:pointer-events-none disabled:opacity-50"
/>
))}
</SliderPrimitive.Root>
@@ -7,7 +7,7 @@ import * as React from 'react'
import { cn } from '../../../lib/utils/cn'
const switchRootVariants = cva(
'peer inline-flex shrink-0 cursor-pointer items-center rounded-full border transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-background-control focus-visible:ring-offset-2 focus-visible:ring-offset-foreground-muted disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-brand data-[state=checked]:hover:bg-brand-600/90 data-[state=unchecked]:bg-control data-[state=unchecked]:hover:bg-border',
'peer inline-flex shrink-0 cursor-pointer items-center rounded-full border transition-colors focus-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-brand data-[state=checked]:hover:bg-brand-600/90 data-[state=unchecked]:bg-control data-[state=unchecked]:hover:bg-border',
{
variants: {
size: {
@@ -26,7 +26,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap py-1.5 text-sm ring-offset-background transition-all focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground data-[state=active]:shadow-xs text-foreground-lighter hover:text-foreground data-[state=active]:border-foreground border-b-2 border-transparent',
'inline-flex items-center justify-center whitespace-nowrap py-1.5 text-sm transition-colors focus-ring disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground data-[state=active]:shadow-xs text-foreground-lighter hover:text-foreground data-[state=active]:border-foreground border-b-2 border-transparent',
'group',
className
)}
@@ -39,14 +39,7 @@ const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
'mt-4 ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
className
)}
{...props}
/>
<TabsPrimitive.Content ref={ref} className={cn('mt-4 focus-ring', className)} {...props} />
))
TabsContent.displayName = TabsPrimitive.Content.displayName
@@ -12,7 +12,7 @@ const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
<textarea
className={cn(
'flex min-h-10 w-full rounded-md border border-control focus:border-control bg-control px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-foreground-muted',
'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-background-control focus-visible:ring-offset-2 focus-visible:ring-offset-foreground-muted disabled:cursor-not-allowed disabled:opacity-50',
'focus-ring disabled:cursor-not-allowed disabled:opacity-50',
...customClasses,
className
)}
@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
className={cn(
'flex min-h-[80px] w-full rounded-md border border-control bg-foreground/[.026] px-3 py-2 text-base md:text-sm ring-offset-background placeholder:text-foreground-muted focus:ring-background-control focus:border-control focus-visible:border-control focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-foreground-muted focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'flex min-h-[80px] w-full rounded-md border border-control bg-foreground/[.026] px-3 py-2 text-base md:text-sm placeholder:text-foreground-muted focus:border-control focus-visible:border-control focus-ring disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
@@ -7,7 +7,7 @@ import * as React from 'react'
import { cn } from '../../../lib/utils/cn'
const toggleVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors text-foreground-light data-[state=on]:bg-accent data-[state=on]:bg-surface-300 data-[state=on]:text-foreground aria-checked:bg-accent aria-checked:bg-surface-300 aria-checked:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ring-offset-background bg-surface-200 hover:bg-surface-300 px-3 py-1 h-auto transition-all',
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors text-foreground-light data-[state=on]:bg-accent data-[state=on]:bg-surface-300 data-[state=on]:text-foreground aria-checked:bg-accent aria-checked:bg-surface-300 aria-checked:text-foreground focus-ring disabled:pointer-events-none disabled:opacity-50 bg-surface-200 hover:bg-surface-300 px-3 py-1 h-auto',
{
variants: {
variant: {
-11
View File
@@ -19,17 +19,6 @@ const defaults = {
alternative: 'border-alternative',
},
placeholder: 'placeholder-border-stronger',
focus: `
outline-hidden
focus:ring-current focus:ring-2
`,
'focus-visible': `
outline-hidden
transition-all
outline-0
focus-visible:outline-4
focus-visible:outline-offset-1
`,
size: {
// buttons, inputs, input labels use these sizes
text: {