The example below loads live from Storybook; open it in Storybook to use the controls.
Default
Section titled “Default”The default Button uses the primary variant, the neutral tone and the medium size (36 px). Use it for the action people are most likely to take in that area.
import { Button } from '@bigbang/ui';
export default function Default() { return <Button>Button</Button>;}Variants
Section titled “Variants”The variant tells people how important an action is. An area has only one primary Button; other actions follow in the secondary or tertiary variant.
- Primary (
primary): The main action of the area, such as saving or submitting. Filled. - Secondary (
secondary): Supporting actions next to the main one, such as exporting. Outlined. - Tertiary (
tertiary): Low-emphasis actions: cancelling, closing, actions in a table row. No fill and no border.
import { Button } from '@bigbang/ui';
export default function Variants() { return ( <> <Button variant="primary">Primary button</Button> <Button variant="secondary">Secondary button</Button> <Button variant="tertiary">Tertiary button</Button> </> );}The tone tells people what the action does. The default tone is neutral (neutral). Actions that cannot be undone, such as deleting, take the danger tone (danger) and always come with a confirmation step. The danger tone combines with all three variants: use it with variant="tertiary" for low-emphasis actions such as deleting a table row.
import { Button } from '@bigbang/ui';
export default function Tones() { return ( <> <Button tone="danger">Primary danger button</Button> <Button variant="secondary" tone="danger"> Secondary danger button </Button> <Button variant="tertiary" tone="danger"> Tertiary danger button </Button> </> );}There are three sizes. Pick a size for density and device, not for emphasis.
- Small (
sm, 32 px): Dense tables, filter bars and toolbars. - Medium (
md, 36 px): Most places, including forms and page headers. - Large (
lg, 40 px): The main action on mobile screens.
import { Button } from '@bigbang/ui';
export default function Sizes() { return ( <> <Button size="sm">Small</Button> <Button size="md">Medium</Button> <Button size="lg">Large</Button> </> );}Disabled
Section titled “Disabled”A disabled Button cannot be clicked and leaves the tab order, so screen reader users may never find it. First consider keeping the action enabled and saying what is missing when it is used. If you do need to disable it and nearby text explains why, add focusableWhenDisabled: the Button stays focusable and screen readers announce it as disabled.
import { Button } from '@bigbang/ui';
export default function Disabled() { return ( <> <Button disabled>Disabled button</Button> <Button variant="secondary" disabled focusableWhenDisabled> Disabled button </Button> </> );}Full width
Section titled “Full width”fullWidth stretches the Button to the width of its container. On mobile forms it puts the main action within easy reach of the thumb. Do not use it on desktop pages, where a very wide Button is harder to read.
import { Button } from '@bigbang/ui';
export default function FullWidth() { return ( <div className="flex w-80 max-w-full flex-col gap-sm"> <Button size="lg" fullWidth> Full width button </Button> <Button size="lg" variant="tertiary" fullWidth> Cancel </Button> </div> );}Submitting forms
Section titled “Submitting forms”Button renders with type="button" by default, so it never submits a form by accident. Give the Button that submits the form type="submit". Pressing Enter in the form then submits it through that Button.
import { Button } from '@bigbang/ui';
export default function Submit() { return ( <form onSubmit={(event) => event.preventDefault()} className="flex gap-sm"> <Button type="submit">Submit</Button> <Button variant="tertiary">Cancel</Button> </form> );}Loading
Section titled “Loading”loading draws a spinner over the label while the action runs. The Button keeps its width, keeps focus and ignores clicks, so a request is never sent twice. The spinner uses the Button’s own label colour.
Visually, only the spinner shows. The Button keeps its label as its name; when loading starts, screen readers read “Loading” once from a polite status region next to the Button. Give loadingLabel for a more specific text (“Saving”, “Submitting”). The default text comes from the library’s string catalogue (ui.button.loading) and Provider sets its language: Turkish without a Provider, English with locale="en-GB". With reduced motion turned on, the spinner stops rotating but stays visible.
Use it only for actions that take longer than about a second (Usage tab). Click the first Button below and it stays in the loading state for two seconds.
import { Button, Provider } from '@bigbang/ui';import { useState } from 'react';
export default function Loading() { const [loading, setLoading] = useState(false); const start = () => { setLoading(true); // Stands in for a slow request. setTimeout(() => setLoading(false), 2000); }; // The Provider reads the library's own strings, such as the loading announcement, in English. return ( <Provider locale="en-GB"> <Button loading={loading} onClick={start}> Button </Button> <Button variant="secondary" loading> Button </Button> </Provider> );}Success
Section titled “Success”success shows a check mark over the label once the action has finished. The mark grows from 60 % in 240 ms and stays; it never loops. Screen readers read “Done” once from the same status region; give successLabel for a more specific text (“Saved”). Turn success on when the action finishes and off after motionValues.motion['feedback-hold'] (2000 ms). When loading is set as well, the spinner wins. With reduced motion turned on, the mark does not grow; it only appears.
Click the Button below and it shows the loading state, then the success state.
import { Button, Provider, motionValues } from '@bigbang/ui';import { useState } from 'react';
export default function Success() { const [state, setState] = useState<'idle' | 'loading' | 'success'>('idle'); const save = () => { setState('loading'); // Stands in for a request; the check mark stays for the feedback hold, then the label returns. setTimeout(() => { setState('success'); setTimeout(() => setState('idle'), motionValues.motion['feedback-hold']); }, 1200); }; return ( <Provider locale="en-GB"> <Button loading={state === 'loading'} success={state === 'success'} successLabel="Saved" onClick={save}> Save </Button> </Provider> );}startIcon shows an icon before the label, endIcon after it. The size follows the Button size: 16 px for small and medium, 20 px for large. The gap between icon and label is fixed. Use currentColor in the SVG and the icon takes the label colour.
Icons are decorative: screen readers skip them and the label carries the meaning. In this version an icon never replaces the label; every Button keeps a visible label. The package ships no icon set; pass your app’s own SVG icons.
import { Button, Icon } from '@bigbang/ui';
export default function Icons() { return ( <> <Button variant="secondary" size="sm" startIcon={<Icon name="download" />}> Icon before </Button> <Button startIcon={<Icon name="plus" />}>Icon before</Button> <Button size="lg" endIcon={<Icon name="arrow-right" />}> Icon after </Button> </> );}Light and dark mode
Section titled “Light and dark mode”Button takes every colour from semantic tokens, so light and dark mode need no extra setup. When you switch brand with the selector at the top, both examples below change to the new brand’s colours.
import { Button } from '@bigbang/ui';
export default function Variants() { return ( <> <Button variant="primary">Primary button</Button> <Button variant="secondary">Secondary button</Button> <Button variant="tertiary">Tertiary button</Button> </> );}