Button group
Two or three related buttons set 8px apart, with the one primary last. It holds dialog and form footers, page actions and the toolbar's actions.
Playground
Change the controls to try each prop. The code under the preview updates as you go and lists only the props you changed; copy it into your page.
import { ButtonGroup } from '@/ui/button-group';<ButtonGroup aria-label="Job actions" />Footer
Dialogs and forms: right-aligned with align="end", Cancel first, the primary last.
import { Button } from '@/ui/button';
import { ButtonGroup } from '@/ui/button-group';
// Dialogs and forms: right-aligned, Cancel first, the primary last.
export default function Footer() {
return (
<ButtonGroup align="end" className="w-80">
<Button>Cancel</Button>
<Button variant="primary">Move to Placed</Button>
</ButtonGroup>
);
}Page actions
The page header's right side: secondary actions, then the page's one primary. Name the group when the buttons need context.
import { PlusIcon, UploadSimpleIcon } from '@phosphor-icons/react';
import { Button } from '@/ui/button';
import { ButtonGroup } from '@/ui/button-group';
// Page header, right side: secondary actions, then the page's one primary.
export default function PageActions() {
return (
<ButtonGroup aria-label="Job actions">
<Button>
<UploadSimpleIcon />
Import
</Button>
<Button variant="primary">
<PlusIcon />
Add job
</Button>
</ButtonGroup>
);
}Compact
Ghost buttons in a bar sit 4px apart with spacing="compact"; hover fills one at a time.
import { Button } from '@/ui/button';
import { ButtonGroup } from '@/ui/button-group';
// Ghost buttons in a bar sit 4px apart; hover fills one at a time.
export default function Compact() {
return (
<ButtonGroup spacing="compact" aria-label="Actions for 3 candidates">
<Button variant="ghost">Move stage</Button>
<Button variant="ghost">Send CVs</Button>
<Button variant="ghost">Archive</Button>
</ButtonGroup>
);
}Saving and a failed save
The primary shows its spinner and ignores more presses until the save returns, so a double click saves once; Cancel waits too. A failed save says so beside the buttons and keeps every value. Press Save changes twice to see both.
import { useState } from 'react';
import { Button } from '@/ui/button';
import { ButtonGroup } from '@/ui/button-group';
import { InlineMessage } from '@/ui/inline-message';
// Save shows its spinner and ignores more presses until it returns, so a double click saves once. The first save fails
// here, to show the failure: an inline error beside the buttons that says what to do. Save again succeeds.
export default function Saving() {
const [saving, setSaving] = useState(false);
const [state, setState] = useState<'idle' | 'failed' | 'saved'>('idle');
return (
<ButtonGroup align="end">
{state === 'failed' && <InlineMessage>Couldn’t save. Check your connection and try again.</InlineMessage>}
{state === 'saved' && <InlineMessage appearance="confirmation">Saved</InlineMessage>}
<Button disabled={saving}>Cancel</Button>
<Button
variant="primary"
loading={saving}
loadingLabel="Saving"
onClick={() => {
setSaving(true);
setTimeout(() => {
setSaving(false);
setState(state === 'idle' ? 'failed' : 'saved');
}, 1000);
}}
>
Save changes
</Button>
</ButtonGroup>
);
}Disabled and long labels
When nothing has changed only the primary is disabled, and its tooltip says why. Long labels stay on one line and the group wraps.
import { Button } from '@/ui/button';
import { ButtonGroup } from '@/ui/button-group';
import { Hint } from '@/ui/tooltip';
// Nothing changed: only the primary is disabled, and its tooltip says why. A long label keeps one line and the group wraps.
export default function States() {
return (
<div className="flex flex-col items-end gap-200">
<ButtonGroup>
<Button>Cancel</Button>
<Hint label="Change something to save">
<Button variant="primary" disabled>
Save changes
</Button>
</Hint>
</ButtonGroup>
<ButtonGroup align="end" className="max-w-90">
<Button>Keep Nguyễn Thị Minh Khai on the short list</Button>
<Button variant="primary">Move Nguyễn Thị Minh Khai to Placed</Button>
</ButtonGroup>
</div>
);
}