Button
Used to trigger an action or event
import { Button } from "@opengovsg/oui"export const Example = () => { return <Button>Button</Button>}Usage
import { Button } from "@opengovsg/oui"<Button>Click me</Button>Alternatively, install the component as local source via the shadcn CLI:
npx shadcn@latest add https://oui.open.gov.sg/r/button.jsonpnpm dlx shadcn@latest add https://oui.open.gov.sg/r/button.jsonnpx shadcn@latest add https://oui.open.gov.sg/r/button.jsonbunx --bun shadcn@latest add https://oui.open.gov.sg/r/button.jsonButton triggers an action or event when pressed, built on React Aria's Button. It handles keyboard activation, focus management, pending state, and ARIA semantics automatically.
Examples
Sizes
Use the size prop to change the size of the button.
import { Button } from "@opengovsg/oui"export const Example = () => { return ( <div className="flex flex-row flex-wrap items-center gap-4"> <Button className="shrink-0" size="xs"> Button (xs) </Button> <Button className="shrink-0" size="sm"> Button (sm) </Button> <Button className="shrink-0" size="md"> Button (md) </Button> <Button className="shrink-0" size="lg"> Button (lg) </Button> </div> )}Variants
Use the variant prop to change the visual style of the Button.
import { Button } from "@opengovsg/oui"export const Example = () => { return ( <div className="flex flex-row items-center gap-4"> <Button variant="solid">Solid</Button> <Button variant="outline">Outline</Button> </div> )}Start and end content
Use icons within a button
import { ArrowRightIcon, MailIcon } from "lucide-react"import { Button } from "@opengovsg/oui"export const Example = () => { return ( <div className="flex flex-row items-center gap-4"> <Button color="neutral" startContent={<MailIcon />}> Email </Button> <Button color="neutral" variant="outline" endContent={<ArrowRightIcon />}> Proceed </Button> </div> )}Color
Use the color prop to change the color of the button.
import { Button } from "@opengovsg/oui"const colorPalettes = [ "sub", "main", "neutral", "critical", "warning", "success", "inverse",] as constexport const Example = () => { return ( <div className="flex flex-col items-center gap-4"> {colorPalettes.map((colorPalette) => ( <div className="flex flex-row items-center gap-4" key={colorPalette}> <div className="min-w-[8ch]">{colorPalette}</div> <Button color={colorPalette}>Button</Button> <Button color={colorPalette} variant="outline"> Button </Button> <Button color={colorPalette} variant="reverse"> Button </Button> <Button color={colorPalette} variant="clear"> Button </Button> </div> ))} </div> )}Disabled
Use the isDisabled prop to disable the button.
import { Button } from "@opengovsg/oui"export const Example = () => { return <Button isDisabled>Button</Button>}Loading
Pass the isPending and loadingText props to the Button component to show a
loading spinner and add a loading text.
import { Button } from "@opengovsg/oui"export const Example = () => { return ( <div className="flex flex-row gap-4"> <Button isPending>Button</Button> <Button isPending loadingText="Loading..."> Button </Button> </div> )}Preserve width while pending
By default, the button keeps its width constant while pending to prevent layout shift. The children are kept in the layout (visually hidden and hidden from assistive technology) and the spinner is overlaid on top of them.
This only applies when no loadingText or pendingElement is provided, since
those intentionally replace the children with content of a different width.
Pass preserveWidth={false} to opt out and let the button collapse to the
spinner's width instead.
import { useState } from "react"import { User2Icon } from "lucide-react"import { Button } from "@opengovsg/oui"export const Example = () => { const [preserveWidth, setPreserveWidth] = useState(true) return ( <div className="flex flex-col items-start gap-4"> <Button size="xs" variant="outline" onPress={() => setPreserveWidth((prev) => !prev)} > Toggle preserveWidth (currently {preserveWidth ? "true" : "false"}) </Button> <div className="flex flex-row items-center gap-4"> <Button isPending preserveWidth={preserveWidth}> Submit form </Button> <Button isPending preserveWidth={preserveWidth} startContent={<User2Icon />} > Submit form </Button> </div> <div className="flex flex-row items-center gap-4"> <Button preserveWidth={preserveWidth}>Submit form</Button> <Button preserveWidth={preserveWidth} startContent={<User2Icon />}> Submit form </Button> </div> </div> )}Spinner Placement
Use the spinnerPlacement prop to change the placement of the spinner.
import { Button } from "@opengovsg/oui"export const Example = () => { return ( <div className="flex flex-row gap-4"> <Button spinnerPlacement="start" isPending loadingText="Loading"> Button </Button> <Button spinnerPlacement="end" isPending loadingText="Loading"> Button </Button> </div> )}Custom Spinner
Use the spinner prop to change the spinner.
import { Loader } from "lucide-react"import { Button } from "@opengovsg/oui"export const Example = () => { return ( <Button isPending spinner={<Loader />}> Button </Button> )}Radius
Use the radius prop to change the radius of the button.
import { Button } from "@opengovsg/oui"export const Example = () => { return ( <div className="flex flex-wrap gap-4"> <Button className="shrink-0" radius="default"> Default </Button> <Button className="shrink-0" radius="none"> None </Button> <Button className="shrink-0" radius="sm"> Rounded sm </Button> <Button className="shrink-0" radius="md"> Rounded md </Button> <Button className="shrink-0" radius="lg"> Rounded lg </Button> <Button className="shrink-0" radius="full"> Rounded full </Button> </div> )}Events
onPress— Called when the button is pressed (mouse, touch, or keyboard). Preferred overonClickfor cross-platform consistency.onPressStart— Called when a press interaction starts.onPressEnd— Called when a press interaction ends, regardless of whether it was completed.onPressChange— Called when the pressed state changes, receiving a boolean.onPressUp— Called when a press is released over the button, regardless of where it started.onFocus/onBlur— Called when the button receives or loses focus.onFocusChange— Called when focus state changes, receiving a boolean.onKeyDown/onKeyUp— Called on keyboard events while the button is focused.onHoverStart/onHoverEnd/onHoverChange— Called on pointer hover events.
Accessibility
- Button renders as a native
<button>element with full keyboard and screen reader support managed by React Aria. - When used as an icon-only button (set
isIconOnly), passaria-labelto identify the action to assistive technology. - Focus is visible via a focus ring on keyboard navigation, and suppressed on pointer press.
- Disabled buttons are marked
aria-disabledand excluded from tab order. - The pending state (when
isPendingistrue) is communicated to assistive technology viaaria-busy.
Slots
Slots are named regions of the component you can target with custom Tailwind classes via the classNames prop. Each slot below corresponds to a key on the classNames object.
Button is a single-element component; use className directly to style the root <button> element.
Props
Button
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | ((renderProps: ButtonRenderProps) => ReactNode) | - | The button label or content, or a render function |
isDisabled | boolean | false | Whether the button is disabled |
isPending | boolean | false | Whether the button is in a pending/loading state |
pendingElement | ReactNode | ((renderProps: ButtonRenderProps) => ReactNode) | - | Content to display while isPending is true; replaces children during the pending state. Prefer this over the deprecated loadingText |
loadingText | string | - | Deprecated. Text to show when pending. Use pendingElement instead |
spinner | ReactNode | ((renderProps: ButtonRenderProps) => ReactNode) | - | Custom spinner element to display while pending; defaults to OUI's Spinner |
spinnerPlacement | "start" | "end" | "start" | Which side of the button content the spinner appears on |
preserveWidth | boolean | true | Keep the button width constant while pending to prevent layout shift. Only applies when no loadingText/pendingElement is provided |
isIconOnly | boolean | false | Whether the button contains only an icon. When true, pass aria-label for accessibility |
disableRipple | boolean | false | Whether to disable the ripple animation on press |
startContent | ReactNode | ((renderProps: ButtonRenderProps) => ReactNode) | - | Content placed before the button label |
endContent | ReactNode | ((renderProps: ButtonRenderProps) => ReactNode) | - | Content placed after the button label |
size | "xs" | "sm" | "md" | "lg" | "md" | The size of the button |
variant | "solid" | "outline" | "ghost" | "clear" | "solid" | The visual style variant |
color | "main" | "sub" | "critical" | "success" | "warning" | "none" | "main" | The color scheme |
radius | "none" | "sm" | "md" | "lg" | "full" | "md" | The border radius |
className | string | - | Additional Tailwind classes applied to the root button element |
onPress | (e: PressEvent) => void | - | Called when the button is pressed |
onPressStart | (e: PressEvent) => void | - | Called when a press interaction starts |
onPressEnd | (e: PressEvent) => void | - | Called when a press interaction ends |
Inherits all remaining props from React Aria's Button. Notable ones: autoFocus, excludeFromTabOrder, aria-label, aria-labelledby, aria-describedby.