fix(studio): slightly round split-button corners on focus (#49129)

## What kind of change does this PR introduce?

UI polish for split buttons (primary action + dropdown chevron).
Follow-up to #49055.

## What is the current behavior?

The focus ring sits above the neighbouring half, but the inner edge
stays square, so the ring has two sharp corners at the join.

## What is the new behavior?

On keyboard focus, the squared-off edge uses a slight radius so the ring
matches the outer corners more closely. Resting state is unchanged.
Split-button callsites now share the same join classes as the
design-system example.

| Before | After |
| --- | --- |
| <img width="1030" height="296" alt="43471"
src="https://github.com/user-attachments/assets/9df3bd72-c7ac-4419-ae18-a7e649dc2d66"
/> | <img width="1056" height="276" alt="CleanShot 2026-08-17 at 10 45
09@2x"
src="https://github.com/user-attachments/assets/52e8a4dc-9c52-45ce-b4d0-f0e7b1b75935"
/> |

## To test

Tab to each half (labelled button, then chevron). Inner corners of the
focus ring should be slightly rounded, not square.

1. [Split with
dropdown](https://design-system-git-fix-split-button-focus-radius-supabase.vercel.app/design-system/docs/components/button#split-with-dropdown)
(no login)
2. [Access
Tokens](https://studio-staging-git-fix-split-button-focus-radius-supabase.vercel.app/dashboard/account/tokens)
→ Generate new token
3. Any project on [studio
staging](https://studio-staging-git-fix-split-button-focus-radius-supabase.vercel.app/dashboard/_/settings/general)
→ Settings → General → Restart project

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Accessibility**
  - Added accessible labels to dropdown and export controls.
- Improved keyboard-focus visibility, layering, and rounded edge
treatment across joined buttons and menus.
  - Removed misleading or redundant screen-reader text and titles.

- **Bug Fixes**
- Prevented split-button controls from shrinking or displaying awkward
borders and corners.
- Refined hover and focus behavior for action buttons throughout
settings, database, storage, account, and documentation interfaces.

- **Documentation**
- Clarified guidance for using overflow menus and responsive
split-button actions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Danny White authored and GitHub committed 2026-08-17 17:28:22 +10:00
1 parent 65a5e593ca
commit 14fe0c0cc8
17 files changed
+53 -38

No files matched your search

@@ -125,17 +125,18 @@ Supports slot behavior with `asChild` prop.
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).
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 />
The shared middle border is the tricky part. Do **not** use `border-l-0` on the chevron button — that drops the divider on hover/focus. Instead:
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: `rounded-r-none` and `hover:z-10` so its border stacks above the chevron on hover.
- 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, also use `flex w-full @lg:w-auto` with `flex-1 @lg:flex-none` on the primary when `layout="responsive"`.
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.
## Accessibility
@@ -20,7 +20,7 @@ export default function AdmonitionButtonSplitDemo() {
<Button
type="button"
variant="default"
className="flex-1 rounded-r-none px-3 @lg:flex-none hover:z-10"
className="flex-1 rounded-r-none px-3 @lg:flex-none hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm"
>
Set up SMTP
</Button>
@@ -30,7 +30,7 @@ export default function AdmonitionButtonSplitDemo() {
type="button"
variant="default"
aria-label="More email template editing options"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10 focus-visible:rounded-l-sm"
icon={<ChevronDown />}
/>
</DropdownMenuTrigger>
@@ -11,7 +11,11 @@ import {
export default function ButtonSplitDropdownDemo() {
return (
<div className="flex w-fit">
<Button type="button" variant="default" className="rounded-r-none hover:z-10">
<Button
type="button"
variant="default"
className="rounded-r-none hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm"
>
Primary action
</Button>
<DropdownMenu>
@@ -20,7 +24,7 @@ export default function ButtonSplitDropdownDemo() {
type="button"
variant="default"
aria-label="More actions"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10 focus-visible:rounded-l-sm"
icon={<ChevronDown />}
/>
</DropdownMenuTrigger>
@@ -35,7 +35,7 @@ export const ExperimentalTokenDropdown = ({ onCreateToken }: ExperimentalTokenDr
<Button
variant="primary"
aria-label="Choose token scope"
className="-ml-px rounded-l-none px-[4px] py-[5px] focus-visible:z-10"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10 focus-visible:rounded-l-sm"
icon={<ChevronDown />}
/>
</DropdownMenuTrigger>
@@ -16,7 +16,7 @@ export const NewTokenButton = ({ onCreateToken }: NewAccessTokenButtonProps) =>
<>
<div className="flex items-center">
<Button
className="rounded-r-none px-3 hover:z-10 focus-visible:z-10"
className="rounded-r-none px-3 hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm"
onClick={() => setVisible(true)}
>
Generate new token
@@ -109,7 +109,10 @@ export const NewScopedTokenSheet = ({ onCreateExperimentalToken }: NewScopedToke
<Sheet open={isOpen} onOpenChange={handleOpenChange}>
<div className="flex items-center">
<SheetTrigger asChild>
<Button variant="primary" className="rounded-r-none px-3 hover:z-10 focus-visible:z-10">
<Button
variant="primary"
className="rounded-r-none px-3 hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm"
>
Generate new token
</Button>
</SheetTrigger>
@@ -28,7 +28,7 @@ export const CustomEmailTemplateRestrictionAdmonition = () => {
<Button
asChild
variant="default"
className="flex-1 rounded-r-none px-3 @lg:flex-none hover:z-10 focus-visible:z-10"
className="flex-1 rounded-r-none px-3 @lg:flex-none hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm"
>
<Link href={`/project/${projectRef}/auth/smtp`}>Set up SMTP</Link>
</Button>
@@ -37,7 +37,7 @@ export const CustomEmailTemplateRestrictionAdmonition = () => {
<Button
variant="default"
aria-label="More email template editing options"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10 focus-visible:rounded-l-sm"
icon={<ChevronDown />}
/>
</DropdownMenuTrigger>
@@ -419,7 +419,7 @@ export const ReplicationPipelineStatus = () => {
<Button
size="tiny"
variant="default"
className="rounded-r-none hover:z-10 focus-visible:z-10"
className="rounded-r-none hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm"
icon={<RotateCcw />}
disabled={isAnyRestartInProgress || showDisabledState || isPipelineError}
loading={isAnyRestartInProgress}
@@ -434,8 +434,9 @@ export const ReplicationPipelineStatus = () => {
<DropdownMenuTrigger asChild>
<Button
variant="default"
aria-label="More restart options"
icon={<ChevronDown />}
className="w-7 rounded-l-none -ml-px focus-visible:z-10"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10 focus-visible:rounded-l-sm"
disabled={showDisabledState || isPipelineError}
/>
</DropdownMenuTrigger>
@@ -433,7 +433,7 @@ export const SchemaGraph = () => {
<div className="flex items-center gap-0">
<ButtonTooltip
variant="default"
className="rounded-r-none border-r-0"
className="rounded-r-none hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm"
icon={copied ? <Check data-testid="copy-sql-ready" /> : <Copy />}
onClick={copyAsSQL}
tooltip={{
@@ -458,11 +458,10 @@ export const SchemaGraph = () => {
<Button
variant="default"
size="tiny"
className="rounded-l-none pl-1 pr-0"
aria-label="Export options"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10 focus-visible:rounded-l-sm"
icon={<ChevronDown size={12} />}
>
<span className="sr-only">Export options</span>
</Button>
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-44">
<DropdownMenuItem
@@ -157,7 +157,7 @@ export function OrgAuditLogDrains() {
disabled={!canManageLogDrains}
onClick={handleAddDestinationClick}
variant="primary"
className="rounded-r-none px-3 hover:z-10 focus-visible:z-10"
className="rounded-r-none px-3 hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm"
>
Add destination
</Button>
@@ -166,8 +166,8 @@ export function OrgAuditLogDrains() {
<DropdownMenuTrigger asChild>
<Button
variant="primary"
title="Choose destination type"
className="-ml-px rounded-l-none px-[4px] py-[5px] focus-visible:z-10"
aria-label="Choose destination type"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10 focus-visible:rounded-l-sm"
icon={<ChevronDown />}
/>
</DropdownMenuTrigger>
@@ -511,7 +511,7 @@ export function useQueryInsightsTableColumns({
<Button
variant="primary"
size="tiny"
className="rounded-r-none border-r-0 focus-visible:z-10"
className="rounded-r-none hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm"
onClick={() => {
setSelectedTriageRow(props.rowIdx)
setSheetView('indexes')
@@ -524,7 +524,8 @@ export function useQueryInsightsTableColumns({
<Button
variant="primary"
size="tiny"
className="rounded-l-none px-1 focus-visible:z-10"
aria-label="More index actions"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10 focus-visible:rounded-l-sm"
icon={<ChevronDown size={12} />}
/>
</DropdownMenuTrigger>
@@ -110,7 +110,7 @@ export const RestartServerButton = () => {
variant="default"
className={cn(
'flex-1 px-3 hover:z-10 focus-visible:z-10 @lg:flex-none',
canRestartProject && canRestart ? 'rounded-r-none' : ''
canRestartProject && canRestart ? 'rounded-r-none focus-visible:rounded-r-sm' : ''
)}
disabled={
project === undefined ||
@@ -143,7 +143,7 @@ export const RestartServerButton = () => {
<Button
variant="default"
aria-label={`Restart ${entityLabel}`}
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10 focus-visible:rounded-l-sm"
icon={<ChevronDown />}
disabled={!canRestartProject}
/>
@@ -31,7 +31,8 @@ export const CreateTableInstructionsDialog = () => {
variant="primary"
icon={<Plus />}
className={cn(
enableCreationOfTablesFromDashboard && 'rounded-r-none hover:z-10 focus-visible:z-10'
enableCreationOfTablesFromDashboard &&
'rounded-r-none hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm'
)}
onClick={() => {
if (enableCreationOfTablesFromDashboard) setShowSheet(true)
@@ -45,7 +46,8 @@ export const CreateTableInstructionsDialog = () => {
<DropdownMenuTrigger asChild>
<Button
variant="primary"
className="w-7 rounded-l-none -ml-px focus-visible:z-10"
aria-label="More table creation options"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10 focus-visible:rounded-l-sm"
icon={<ChevronDown />}
/>
</DropdownMenuTrigger>
@@ -134,7 +134,7 @@ export function AiAssistantDropdown({
onClick={handleOpenAssistant}
icon={<AiIconAnimation size={iconOnly ? 16 : 14} loading={loading} />}
className={cn(
'rounded-r-none border-r-0 focus-visible:z-10',
'rounded-r-none hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm',
iconOnly && 'px-1.5',
className
)}
@@ -150,7 +150,7 @@ export function AiAssistantDropdown({
variant={variant}
size={size}
disabled={disabled}
className={cn('rounded-l-none px-1 focus-visible:z-10', iconOnly && 'px-1')}
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10 focus-visible:rounded-l-sm"
icon={<ChevronDown size={12} />}
/>
</DropdownMenuTrigger>
@@ -215,7 +215,7 @@ const LogDrainsSettings: NextPageWithLayout = () => {
disabled={!hasAccessToLogDrains || !canManageLogDrains}
onClick={handleAddDestinationClick}
variant="primary"
className="rounded-r-none px-3 hover:z-10 focus-visible:z-10"
className="rounded-r-none px-3 hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm"
>
Add destination
</Button>
@@ -224,8 +224,8 @@ const LogDrainsSettings: NextPageWithLayout = () => {
<DropdownMenuTrigger asChild>
<Button
variant="primary"
title="Choose token scope"
className="-ml-px rounded-l-none px-[4px] py-[5px] focus-visible:z-10"
aria-label="Choose destination type"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10 focus-visible:rounded-l-sm"
icon={<ChevronDown />}
/>
</DropdownMenuTrigger>
@@ -28,7 +28,7 @@ export function ChangelogLlmMarkdownButton({ className, markdownPath = '/changel
<div className={cn('flex items-center', className)}>
<Button
variant="default"
className="rounded-r-none border-r-0"
className="rounded-r-none hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm"
icon={
copied ? (
<Check className="h-4 w-4" strokeWidth={2} aria-hidden />
@@ -45,7 +45,7 @@ export function ChangelogLlmMarkdownButton({ className, markdownPath = '/changel
<DropdownMenuTrigger asChild>
<Button
variant="default"
className="rounded-l-none px-1"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10 focus-visible:rounded-l-sm"
icon={<ChevronDown className="h-4 w-4" strokeWidth={2} aria-hidden />}
aria-label="Open LLM options for this changelog page"
/>
@@ -186,7 +186,11 @@ export const KeyValueFieldArray = <
icon={<Plus />}
disabled={disabled}
onClick={() => append(createEmptyRow())}
className={cn(hasAddActions && 'rounded-r-none border-r-0 px-3', addButtonClassName)}
className={cn(
hasAddActions &&
'rounded-r-none px-3 hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm',
addButtonClassName
)}
>
{addLabel}
</Button>
@@ -201,7 +205,7 @@ export const KeyValueFieldArray = <
icon={<ChevronDown size={14} />}
aria-label={addActionsLabel}
disabled={disabled}
className="rounded-l-none px-[4px] py-[5px]"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px focus-visible:z-10 focus-visible:rounded-l-sm"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom">