> ## Documentation Index
> Fetch the complete documentation index at: https://docs.twenty.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Button

> Trigger actions or navigate with styled buttons.

export const StoryEmbed = ({storyId, title, height = 120}) => <>
    <Tabs>
      <Tab title="Light">
        <iframe title={`${title} (light)`} src={`https://storybook.twenty.com/iframe.html?id=${storyId}&viewMode=story&globals=colorScheme:light`} width="100%" height={height} loading="lazy" allow="clipboard-write" style={{
  border: 0
}} />
      </Tab>
      <Tab title="Dark">
        <iframe title={`${title} (dark)`} src={`https://storybook.twenty.com/iframe.html?id=${storyId}&viewMode=story&globals=colorScheme:dark`} width="100%" height={height} loading="lazy" allow="clipboard-write" style={{
  border: 0
}} />
      </Tab>
    </Tabs>
    <a href={`https://storybook.twenty.com/?path=/story/${storyId}`}>
      Open in Storybook
    </a>
  </>;

Use `Button` for an action and supply its visible label as children. Use [IconButton](/ui/components/input/icon-button) for icon-only controls, with an accessible name from `aria-label` or `aria-labelledby`.

For recurring design defaults, use [MainButton](/ui/components/input/main-button)
or [LightButton](/ui/components/input/light-button) from `twenty-ui/components`.
Both share Button's native props, refs, state, and rendering behavior.

<StoryEmbed title="Button display" storyId="ui-input-button-button--display" height={100} />

## Usage

```tsx theme={null}
import { Button } from 'twenty-ui/primitives/input';

export const SaveChanges = () => <Button>Save changes</Button>;
```

## Appearance

Compare variants, semantic colors, sizes, and common states separately. Each group changes one choice while keeping the others fixed.

<StoryEmbed storyId="ui-input-button-button--catalog" title="Appearance for button" height={630} />

```tsx theme={null}
import { Button } from 'twenty-ui/primitives/input';
import { IconPlus } from 'twenty-ui/icon';

export const CreateRecord = () => (
  <Button variant="solid" color="accent" size="sm" startIcon={<IconPlus />}>
    Create record
  </Button>
);
```

Choose `solid`, `outline`, `soft`, `ghost`, or `link` independently of the `neutral`, `accent`, `danger`, or `success` color. Sizes are `sm` and `md`; the defaults are `outline`, `neutral`, and `md`. `fullWidth` fills the available width. `startIcon` and `endIcon` accept elements and are decorative. `elevated` adds a shadow and backdrop blur. Neutral outline buttons also gain a bordered surface with matching text and interaction colors. Other variants and semantic colors keep their existing palette. It defaults to `false` and does not change size or position. `shortcut` displays keyboard hints on larger screens; bind the shortcuts in your application.

`link` uses compact text with no border or filled background. It follows its content height at either size, with a smaller font and narrower padding. Its neutral color uses stronger text on hover. The appearance does not determine semantics: use `onClick` for actions and `href` for navigation.

`disabled` and `loading` prevent activation. Loading keeps the label accessible and communicates a busy state. By default, `loadingPosition="center"` hides the content while preserving its layout space and centers the spinner. Set `loadingPosition` to `start` or `end` to keep children and the opposite icon visible while replacing the corresponding icon with the spinner.

Your application owns progress formatting and the translated label. Pass the complete status phrase as children, for example `Installing (42%)`.

<StoryEmbed title="Button loading at end" storyId="ui-input-button-button--loading-at-end" height={100} />

## State ownership

`Button` triggers an action and does not manage a selected value, so it has no controlled or uncontrolled selection mode. Its native `value` prop supplies form data, not selection state.

Your application owns `loading` and `disabled`, which default to `false`. Clicking a button does not change these props or automatically show loading while an asynchronous handler runs. Pass the current application state explicitly.

```tsx theme={null}
import { useState } from 'react';
import { Button } from 'twenty-ui/primitives/input';

type SaveRecordProps = { onSave: () => Promise<void> };

export const SaveRecord = ({ onSave }: SaveRecordProps) => {
  const [loading, setLoading] = useState(false);
  const [hasError, setHasError] = useState(false);

  const handleSave = async () => {
    setLoading(true);
    setHasError(false);

    try {
      await onSave();
    } catch {
      setHasError(true);
    } finally {
      setLoading(false);
    }
  };

  return (
    <>
      <Button loading={loading} onClick={handleSave}>
        Save record
      </Button>
      {hasError && <p role="alert">Unable to save the record. Try again.</p>}
    </>
  );
};
```

The application starts and clears loading around the save operation. The button prevents activation while loading and becomes available again when the operation finishes, including after an error. `MainButton` and `LightButton` use the same state model.

## Popup triggers

Compose a popup trigger with `Button`, `LightButton`, or another Button wrapper through `render`. The trigger supplies `data-popup-open`, so the button keeps its hover background while the popup is open and uses its active background on hover or press. Each variant and color keeps its own palette. Link backgrounds remain transparent, and disabled or loading buttons keep their disabled appearance. Buttons with `aria-pressed="true"` retain their existing toggle styling. Popup state does not set `aria-pressed` or represent a selected value.

```tsx theme={null}
import { LightButton } from 'twenty-ui/components/input';
import { Popover } from 'twenty-ui/primitives/surfaces';

export const DetailsPopup = () => (
  <Popover.Root>
    <Popover.Trigger render={<LightButton>Show details</LightButton>} />
    <Popover.Portal>
      <Popover.Positioner>
        <Popover.Popup>
          <Popover.Title>Details</Popover.Title>
          <Popover.Description>
            More information about this record.
          </Popover.Description>
        </Popover.Popup>
      </Popover.Positioner>
    </Popover.Portal>
  </Popover.Root>
);
```

## Upcoming features

Your application owns feature availability and its label. Compose a disabled button with `Badge` as children so the label remains part of the button's accessible name. Supply localized text from your application.

```tsx theme={null}
import { Badge } from 'twenty-ui/primitives/data-display';
import { Button } from 'twenty-ui/primitives/input';
import { Text } from 'twenty-ui/primitives/typography';

export const UpcomingAction = () => (
  <Button disabled>
    <Text
      render={<span />}
      style={{
        display: 'inline-flex',
        alignItems: 'center',
        gap: 'var(--t-spacing-1)',
      }}
    >
      Upcoming action
      <Badge>Coming soon</Badge>
    </Text>
  </Button>
);
```

This composition also works with `MainButton` and `LightButton`. It replaces the removed `soon` and `soonLabel` props.

## Animated icons

Compose `Button` with `AnimatedIconCrossfade` to change its icon when application state changes. Pass the animated element to `startIcon` and the label as children.

```tsx theme={null}
import { useState } from 'react';
import { IconPencil, IconX } from 'twenty-ui/icon';
import { Button } from 'twenty-ui/primitives/input';
import { AnimatedIconCrossfade } from 'twenty-ui/components/layout';

export const EditActions = () => {
  const [isEditing, setIsEditing] = useState(false);

  return (
    <Button
      size="sm"
      aria-expanded={isEditing}
      onClick={() => setIsEditing(!isEditing)}
      startIcon={
        <AnimatedIconCrossfade
          isActive={isEditing}
          activeIcon={<IconX size={14} />}
          inactiveIcon={<IconPencil size={14} />}
        />
      }
    >
      Edit actions
    </Button>
  );
};
```

When an icon is itself an action, use `IconButton` or `LightIconButton` and animate its icon content. When a disclosure header owns expansion, keep its chevron decorative and expose `aria-expanded` on the header control. Keep animation styles with the feature and respect `prefers-reduced-motion`.

<StoryEmbed title="Button with an animated icon" storyId="ui-components-layout-animatediconcrossfade--display" height={100} />

## Links and forms

```tsx theme={null}
import { Button } from 'twenty-ui/primitives/input';

export const DocumentationLink = () => (
  <Button
    variant="link"
    href="https://twenty.com"
    target="_blank"
    rel="noreferrer"
    hrefLang="en"
    referrerPolicy="no-referrer"
  >
    Read documentation
  </Button>
);
```

Without a custom `render`, `href` renders an anchor with link semantics, unless you explicitly set `nativeButton` to `true`. Native anchor attributes such as `download`, `hrefLang`, `ping`, and `referrerPolicy` reach the anchor. Props shared with native buttons retain their Base UI types. To supply an anchor MIME type, put it on the renderer, for example `render={<a href="/records.csv" type="text/csv">Download records</a>}` with `nativeButton={false}` and `role="link"`. Disabled and loading links cannot activate.

Use `render` to compose another element or a router link. `nativeButton` describes the rendered element and defaults to `true` for custom renderers. Set `nativeButton={false}` and `role="link"` for a navigation anchor. The anchor or router component can own its destination without a `Button` `href`.

```tsx theme={null}
import { Button } from 'twenty-ui/primitives/input';

export const RecordsLink = () => (
  <Button nativeButton={false} role="link" render={<a href="/records" />}>
    Open records
  </Button>
);
```

For a custom native `<button>`, keep `nativeButton` true. For a non-native action element such as a `<div>`, use `nativeButton={false}`; Base UI supplies button keyboard behavior. An explicit `nativeButton` or `role` is preserved even when `href` is also supplied.

Buttons default to `type="button"`. Set `type="submit"` or `type="reset"` explicitly for forms. Native attributes, event handlers, event details, and refs reach the rendered root. A ref receives the actual button, anchor, or custom root element. Keyboard handlers retain Base UI event methods, including `preventBaseUIHandler()` to cancel its built-in keyboard action. Base UI's render callback receives both root props and the current disabled state; spread the props onto your root to retain composition behavior.

## Group defaults

[ButtonGroup](/ui/primitives/input/button-group) supplies a shared variant, color, and size. An explicit prop on a child takes precedence over the group default. When neither sets a prop, the component default applies.

## Migration from ClickToActionLink

Import `Button` from `twenty-ui/primitives/input` and replace `ClickToActionLink` with `Button variant="link"`. Keep `href`, `target`, and `rel` for navigation. Actions without `href` now render a native button with keyboard activation. Native attributes, events, and refs pass through to the root.

```tsx theme={null}
import { Button } from 'twenty-ui/primitives/input';

type LogOutProps = { onLogOut: () => void };

export const LogOut = ({ onLogOut }: LogOutProps) => (
  <Button variant="link" onClick={onLogOut}>
    Log out
  </Button>
);
```

## Props

<ParamField body="className" type="string | ((state: ButtonState) => string | undefined)">
  CSS class applied to the element, or a function that
  returns a class based on the component's state.
</ParamField>

<ParamField body="color" type="&#x22;accent&#x22; | &#x22;danger&#x22; | &#x22;neutral&#x22; | &#x22;success&#x22;" default="neutral">
  Semantic color of the button.
</ParamField>

<ParamField body="elevated" type="boolean" default="false">
  Adds a shadow and backdrop blur. Neutral outline buttons also use elevated surface colors.
</ParamField>

<ParamField body="endIcon" type="ReactNode">
  Decorative content displayed after the label.
</ParamField>

<ParamField body="focusableWhenDisabled" type="boolean" default="false">
  Whether the button should be focusable when disabled.
</ParamField>

<ParamField body="fullWidth" type="boolean" default="false">
  Expands the button to fill its container width.
</ParamField>

<ParamField body="href" type="string">
  Destination URL. Renders a native anchor when `render` is omitted unless `nativeButton` is explicitly true. Custom renderers retain their own element contract.
</ParamField>

<ParamField body="loading" type="boolean" default="false">
  Shows a loading indicator and disables activation.
</ParamField>

<ParamField body="loadingPosition" type="&#x22;center&#x22; | &#x22;end&#x22; | &#x22;start&#x22;" default="center">
  Where the loading indicator appears. `center` hides the content while preserving its layout space; `start` and `end` keep children and the opposite icon visible and replace the corresponding icon with the spinner.
</ParamField>

<ParamField body="nativeButton" type="boolean" default="true (false for the built-in href anchor)">
  Whether the rendered root is a native button. Defaults to true for custom renderers. The built-in `href` anchor defaults to false; an explicit value takes precedence.
</ParamField>

<ParamField body="render" type="ReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ButtonState>">
  Caller-supplied root element or renderer. Native attributes, handlers, event details, and refs reach the rendered root. Use `nativeButton={false}` and `role="link"` for a custom navigation anchor. Router integration belongs to the caller.
</ParamField>

<ParamField body="role" type="AriaRole">
  Role of the root element. Use `link` for custom navigation renderers.
</ParamField>

<ParamField body="shortcut" type="ShortcutDefinition">
  Flat key array for simultaneous keys, or nested key arrays for an ordered sequence. Displayed on non-mobile screens.
</ParamField>

<ParamField body="shortcutJoinLabel" type="string">
  Localized text between sequence steps. Defaults to then.
</ParamField>

<ParamField body="size" type="&#x22;md&#x22; | &#x22;sm&#x22;" default="md">
  Button height: `sm` (24px) or `md` (32px). The `link` variant uses its content height at either size.
</ParamField>

<ParamField body="startIcon" type="ReactNode">
  Decorative content displayed before the label.
</ParamField>

<ParamField body="style" type="CSSProperties | ((state: ButtonState) => CSSProperties | undefined)">
  Style applied to the element, or a function that
  returns a style object based on the component's state.
</ParamField>

<ParamField body="variant" type="&#x22;ghost&#x22; | &#x22;link&#x22; | &#x22;outline&#x22; | &#x22;soft&#x22; | &#x22;solid&#x22;" default="outline">
  Visual treatment of the button surface. `link` uses compact text styling without a filled surface or border; render composition determines native semantics.
</ParamField>

Pass `shortcut={['Mod', 'Enter']}` for simultaneous keys or `shortcut={[['G'], ['P']]}` for successive presses. See [Shortcut](/ui/primitives/typography/shortcut) for nested combinations and the shared string formatter.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.