Open UI

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.json
pnpm dlx shadcn@latest add https://oui.open.gov.sg/r/button.json
npx shadcn@latest add https://oui.open.gov.sg/r/button.json
bunx --bun shadcn@latest add https://oui.open.gov.sg/r/button.json

Button 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 over onClick for 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), pass aria-label to 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-disabled and excluded from tab order.
  • The pending state (when isPending is true) is communicated to assistive technology via aria-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

PropTypeDefaultDescription
childrenReactNode | ((renderProps: ButtonRenderProps) => ReactNode)-The button label or content, or a render function
isDisabledbooleanfalseWhether the button is disabled
isPendingbooleanfalseWhether the button is in a pending/loading state
pendingElementReactNode | ((renderProps: ButtonRenderProps) => ReactNode)-Content to display while isPending is true; replaces children during the pending state. Prefer this over the deprecated loadingText
loadingTextstring-Deprecated. Text to show when pending. Use pendingElement instead
spinnerReactNode | ((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
preserveWidthbooleantrueKeep the button width constant while pending to prevent layout shift. Only applies when no loadingText/pendingElement is provided
isIconOnlybooleanfalseWhether the button contains only an icon. When true, pass aria-label for accessibility
disableRipplebooleanfalseWhether to disable the ripple animation on press
startContentReactNode | ((renderProps: ButtonRenderProps) => ReactNode)-Content placed before the button label
endContentReactNode | ((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
classNamestring-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.

On this page