Files
Saxon FletcherandDanny White cd77bebafd chore(ui): refresh shared button styles (#50197)
## What kind of change does this PR introduce?

UI polish / design system: refreshed button styles, related token
tweaks, and a shared floating-button plate.

Resolves DEPR-652.

## What is the current behavior?

Default, primary, and secondary buttons use older fills, borders, and
hover treatments. Primary still leans on brand scale utilities. Default
fills don’t always read as raised chrome across surfaces, and floating
copy / expand / scroll controls can let busy content show through
translucent fills. Call sites hand-roll `rounded-* bg-background`
wrappers for that.

## What is the new behavior?

Refreshes primary, default, and secondary buttons with medium-weight
labels, subtle shadows and inset edges, and smoother transitions.
Light-mode default buttons use a raised fill with an accent hover state,
primary text is brighter, and inline keyboard shortcuts inherit the
button’s colour.

Adds `FloatingPlate`: an opaque `bg-popover` shell for floating default
buttons (and small clusters). Migrates Studio, Docs-related patterns,
www, and `ui-patterns` floaters onto it so busy content no longer shows
through translucent fills. Positioning, z-index, and hover/focus reveal
stay on the plate’s `className`. Use `rounded="full"` for pills.

Also:

- Moves primary onto semantic `--primary` / `--primary-hover` (with a
light-theme override) instead of brand utility fills
- Tokenises button shadows as `--button-shadow-drop` /
`--button-shadow-raised` / `--button-shadow-default` on the Button base
- Aligns hover direction: darken on light mode, lighten on dark mode for
both default and primary
- Default fill stays opaque `bg-card` in light (occlusion) and
translucent `bg-muted` in dark (adapts to the local surface)
- Documents fills and `FloatingPlate` on the design-system Button page
(with a live example)
- Scales shared radius tokens in Studio and www; medium+ Button sizes
use a proportionally softer radius
- Fixes www nav CTA centering (`lg:inline-flex` instead of `lg:block`)
- Query detail Expand/Collapse wires `aria-expanded` / `aria-controls`

| Before | After |
| --- | --- |
| <img width="1074" height="438" alt="CleanShot 2026-09-18 at 15 52
51@2x"
src="https://github.com/user-attachments/assets/ef43da21-b053-4b7e-9ac4-ab8b428228ab"
/> | <img width="1090" height="464" alt="CleanShot 2026-09-18 at 15 50
59@2x"
src="https://github.com/user-attachments/assets/2ddc55fc-4c8d-499c-a280-f3db3d99023c"
/> |
| <img width="1082" height="446" alt="CleanShot 2026-09-18 at 15 52
35@2x"
src="https://github.com/user-attachments/assets/3dd5452d-325a-4e4a-a79d-26c6c6950a31"
/> | <img width="1078" height="446" alt="CleanShot 2026-09-18 at 15 51
13@2x"
src="https://github.com/user-attachments/assets/93666385-3e6e-42e0-9891-9cd6bb935b67"
/> |

## To test

### Design system

- [Button
page](https://design-system-git-chore-button-styles-supabase.vercel.app/design-system/docs/components/button):
default / primary in light and dark; hover should darken on light,
lighten on dark
- Same page: [Floating over
content](https://design-system-git-chore-button-styles-supabase.vercel.app/design-system/docs/components/button#floating-over-content)
/ [Floating
plate](https://design-system-git-chore-button-styles-supabase.vercel.app/design-system/docs/components/button#floating-plate)
example; Copy over SQL should stay opaque
- Spot-check hover on a code preview Copy control

### Docs

[Docs deploy
preview](https://docs-git-chore-button-styles-supabase.vercel.app/docs):

- [Docs
homepage](https://docs-git-chore-button-styles-supabase.vercel.app/docs):
top-right **Sign up** / **Dashboard** primary; menu icon beside it
(default icon button)
- Shrink below `lg` and open the hamburger drawer: bottom **Sign in**
(default) + **Start your project** (primary) medium block buttons
- Tab once for **Skip to content** (FloatingPlate)
- [MCP
guide](https://docs-git-chore-button-styles-supabase.vercel.app/docs/guides/ai-tools/mcp):
project picker
- [Apple
login](https://docs-git-chore-button-styles-supabase.vercel.app/docs/guides/auth/social-login/auth-apple):
**Generate Secret Key** button in the Apple Secret Generator
- Optional opacity check: any guide code block Copy control (e.g. at the
bottom of [Import data into
Supabase](https://docs-git-chore-button-styles-supabase.vercel.app/docs/guides/database/import-data))

### Studio

[Studio deploy
preview](https://studio-staging-git-chore-button-styles-supabase.vercel.app/):

- **Observability → Query Performance**: open a query detail →
Expand/Collapse pill + SQL Copy chip (dark: no bleed-through)
- **Observability → Query Insights**: select a query → Clear query pill
- **Table Editor → any table → Definition** → floating **Open in SQL
Editor**
- **Connect → Framework → Add files**: Copy on the code tabs
(FloatingPlate; light hover follow-up is DEPR-694)
- Tab once for **Skip to content**

### WWW

- [www deploy
preview](https://zone-www-dot-com-git-chore-button-styles-supabase.vercel.app/):
nav Sign in / Start your project vertical centering; hero medium CTAs
radius

---------

Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com>
2026-09-23 03:43:00 +00:00

194 lines
6.9 KiB
Plaintext

---
title: Button
description: Displays a button or a link that looks like a button.
featured: true
component: true
---
<ComponentPreview name="button-default" peekCode wide />
## Usage
```tsx
import { Button } from '@/components/ui/button'
```
```tsx
<Button variant="outline">Button</Button>
```
## Link
You can use the `buttonVariants` helper to create a link that looks like a button.
```tsx
import { buttonVariants } from '@/components/ui/button'
```
```tsx
<Link className={buttonVariants({ variant: 'outline' })}>Click here</Link>
```
Alternatively, you can set the `asChild` parameter and nest the link component.
```tsx
<Button asChild>
<Link href="/login">Login</Link>
</Button>
```
## Examples
### Sizes
Use the `size` prop to determine the size of the button.
<ComponentPreview name="button-sizes" />
### Variants
#### Default
Used when no `variant` is specified. Prefer this unless another variant fits better, as below.
<ComponentPreview name="button-default" />
#### Primary
Use sparingly for data insertion, confirming purchases, and other strong positive actions. Because it is so prominent, aim for at most one primary button in a viewport.
<ComponentPreview name="button-demo" />
#### Secondary
Can be used for signaling a data or config change, but not as serious as a primary button.
For destructive or side effect actions, use the `destructive` or `warning` variant.
<ComponentPreview name="button-secondary" />
#### Warning
Used for actions that might have a side effect, but not as serious as a destructive action.
<ComponentPreview name="button-warning" />
#### Destructive (currently `danger`)
Used for actions that will have a serious destructive side effect, like deleting data.
prop `variant` will probably be changed to `destructive` in the future.
<ComponentPreview name="button-destructive" />
#### Outline
Used for secondary actions, or actions that are not as important as the primary action.
<ComponentPreview name="button-outline" />
#### Ghost (currently `text`)
Used for actions that are not as important as the primary action, or for actions that are not as important as the primary action.
prop `variant` will probably be changed to `ghost` in the future.
<ComponentPreview name="button-ghost" />
#### Link
Used for actions that are not as important as the primary action, or for actions that are not as important as the primary action.
<ComponentPreview name="button-link" />
### Only an icon
Displaying only an Icon in a button.
<Admonition type="note" title="This feature requires more support" className="mt-3">
We should update the button component to support this use case better.
</Admonition>
<ComponentPreview name="button-icon" />
### As child
Supports slot behavior with `asChild` prop.
<ComponentPreview name="button-as-child" />
### Split with dropdown
Pair a button with a chevron `DropdownMenu` trigger when there are variations of the same action, or alternative ways to accomplish the same goal. The default or most likely option should be used on the exposed button.
When secondary actions are related but distinct (not alternatives to the primary action) display the primary action as a button and place the rest in an overflow menu instead. See [Table multiple actions](./table#multiple-actions).
<ComponentPreview name="button-split-dropdown" peekCode />
Ensure the middle border is shared rather than doubled-up. Do not use `border-l-0` on the chevron button as that drops the divider on hover/focus. Instead:
- Primary action: `rounded-r-none` and `hover:z-10` so its border stacks above the chevron on hover.
- Chevron trigger: `rounded-l-none`, `shrink-0`, `px-[4px] py-[5px]`, and `-ml-px` to overlap the adjacent border by one pixel.
- Both: `focus-visible:z-10` so the focus ring stacks above the neighbour, and `focus-visible:rounded-r-sm` / `focus-visible:rounded-l-sm` so the squared-off edge is slightly rounded while the ring is shown.
- Chevron trigger only: `aria-label` describing the menu (the icon is decorative).
Inside [Admonition](../fragments/admonition#split-button-with-dropdown) actions when `layout="responsive"`: also use `flex w-full @lg:w-auto` with `flex-1 @lg:flex-none` on the primary action.
## Default fill and floating buttons
The default variant is meant to read as raised chrome on whatever surface it sits on.
- **Light:** opaque `bg-card` (a solid elevated plate). Opaque on purpose so the button can cover busy content underneath.
- **Dark:** translucent `bg-muted` (a foreground wash). That adapts to the local surface, but content can show through the fill.
Hover: light uses `hover:bg-muted`; dark uses `dark:hover:bg-accent`.
### Floating over content
If a default button is absolutely or sticky-positioned over code, tables, maps, or other busy UI, wrap it in [`FloatingPlate`](#floating-plate) so nothing bleeds through:
<ComponentPreview name="button-floating-plate" peekCode />
Keep positioning, z-index, and hover/focus reveal on the plate's `className`. Use `rounded="full"` when the child is a pill. For clusters (split toggles, parallel actions), wrap the group once.
Do not override the button fill with `bg-popover` at the callsite. The plate owns occlusion; the button stays a normal default control.
## Floating plate
`FloatingPlate` is an opaque `bg-popover` shell for floating default buttons (and small clusters).
```tsx
import { Button, FloatingPlate } from 'ui'
```
| Prop | Default | Notes |
| ----------- | ------- | --------------------------------------------------------------- |
| `rounded` | `lg` | `md`, `lg`, or `full`. Match or exceed the child button radius. |
| `className` | — | Positioning, opacity, gaps for clusters. |
See [Floating over content](#floating-over-content) for when to use it.
## Accessibility
[Keyboard focus](../accessibility#focus-management) is automatically handled:
- Enabled buttons default to `tabIndex={0}` (keyboard accessible)
- Buttons with native `disabled` 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` for buttons using native `disabled`.
When a disabled action has a non-obvious reason and needs a tooltip, add `focusableWhenDisabled` so keyboard users can still focus the control. See [Disabled controls](../accessibility#disabled-controls).
### Focusable when disabled
Use `focusableWhenDisabled` with `disabled` when the action is blocked for a non-obvious reason and you need a tooltip or other explanation. The control stays in the tab order and uses `aria-disabled` instead of native `disabled`.
```tsx
<Button disabled focusableWhenDisabled>
Pause project
</Button>
```
In Studio, `ButtonTooltip` adds `focusableWhenDisabled` to disabled buttons with tooltip text automatically.