Button

A pressable button with sizing, icons, and themes

Button combines accessible press behavior with shared sizing, icons, themes, and full HTML form semantics.

Features

  • One size prop for all styles

  • Icons before or after content

  • Explicit icon sizing with iconSize

  • Full HTML button form semantics

Installation

Button is already installed in tamagui:

// copy-paste: these skins import from files you own. copy each registry item
// below into your app, then adjust the relative import paths to fit.
// components/tamagui/Button.tsx (registry item "button")
// npm dependencies: @tamagui/button @tamagui/core @tamagui/helpers-tamagui
import { Button } from "../components/tamagui/Button";

@tamagui/button holds the unstyled half: useButton and the parts, with no styled Button of its own. Import from it when you are building your own button rather than using ours; see Build your own.

Usage

// copy-paste: these skins import from files you own. copy each registry item
// below into your app, then adjust the relative import paths to fit.
// components/tamagui/Button.tsx (registry item "button")
// npm dependencies: @tamagui/button @tamagui/core @tamagui/helpers-tamagui
import { Button } from "../components/tamagui/Button";
export default () => <Button>Lorem ipsum</Button>;

Sizing

Sizing a button means adjusting many properties at once, not just on the outer frame but on the text inside it. Tamagui adjusts padding, border radius, font size, and icon size together with the size prop.

// copy-paste: these skins import from files you own. copy each registry item
// below into your app, then adjust the relative import paths to fit.
// components/tamagui/Button.tsx (registry item "button")
// npm dependencies: @tamagui/button @tamagui/core @tamagui/helpers-tamagui
import { Button } from "../components/tamagui/Button";
export default () => <Button size="lg">Lorem ipsum</Button>;

Each name is a rung in the shared Sizing ladder (xs through xl, default md), so the frame, its text, and its icon agree by construction.

Variants

The variant prop selects a visual style. Tamagui ships outlined (transparent background with a visible border) and quiet (no background, no border).

// copy-paste: these skins import from files you own. copy each registry item
// below into your app, then adjust the relative import paths to fit.
// components/tamagui/Button.tsx (registry item "button")
// npm dependencies: @tamagui/button @tamagui/core @tamagui/helpers-tamagui
import { XStack } from 'tamagui';
import { Button } from "../components/tamagui/Button";
export default () => <XStack gap="2">
<Button>Default</Button>
<Button variant="outlined">Outlined</Button>
<Button variant="quiet">Quiet</Button>
</XStack>;

Icon theming

You can pass icons as either elements or components. If passing components, Tamagui themes them, passing size and color. The icon size comes from the Button’s size prop by default.

Set the icon size explicitly with iconSize (pixels, e.g. {18}). scaleIcon adjusts it further, relative to whichever size was resolved.

Spacing between an icon and the text comes from gap on the button frame, derived from the size table so it stays consistent with the button’s dimensions.

// copy-paste: these skins import from files you own. copy each registry item
// below into your app, then adjust the relative import paths to fit.
// components/tamagui/Button.tsx (registry item "button")
// npm dependencies: @tamagui/button @tamagui/core @tamagui/helpers-tamagui
import { Button } from "../components/tamagui/Button";
import { Star } from './components/icons'; // your generated icons, see Lucide Icons
export default () => <>
<Button icon={Star} size="lg">
Icon sized by Button
</Button>
<Button icon={Star} iconSize={18} size="lg">
Icon sized explicitly
</Button>
</>;

Web form props

Button supports the standard HTML <button> attributes for form integration. These are web-only and ignored on native:

// copy-paste: these skins import from files you own. copy each registry item
// below into your app, then adjust the relative import paths to fit.
// components/tamagui/Button.tsx (registry item "button")
// npm dependencies: @tamagui/button @tamagui/core @tamagui/helpers-tamagui
import { Form } from 'tamagui';
import { Button } from "../components/tamagui/Button";
export default () => <Form action="/submit">
<Button type="submit">Submit Form</Button>
<Button type="reset">Reset</Button>
<Button type="button">Regular Button (default)</Button>
{/* Override form attributes */}
<Button type="submit" formAction="/alternative-endpoint" formMethod="post">
Submit to Different Endpoint
</Button>
</Form>;

Button defaults to type="button" to prevent unintended form submissions. Use type="submit" explicitly when you want form submission behavior.

Text styling

Button’s frame is a View, so text style props passed to it are ignored. The one exception is fontFamily, which forwards to the wrapped text, since a font family is a base value and needs no conditional resolution.

Everything else goes on Button.Text:

// copy-paste: these skins import from files you own. copy each registry item
// below into your app, then adjust the relative import paths to fit.
// components/tamagui/Button.tsx (registry item "button")
// npm dependencies: @tamagui/button @tamagui/core @tamagui/helpers-tamagui
import { Button } from "../components/tamagui/Button";
export default () => <Button>
<Button.Text color="blue" fontSize="5" fontWeight="bold">
Styled text
</Button.Text>
</Button>;

Interactive text styles

Combine Button.Text with the group prop on Button to coordinate text styles across hover, press, and focus states:

// copy-paste: these skins import from files you own. copy each registry item
// below into your app, then adjust the relative import paths to fit.
// components/tamagui/Button.tsx (registry item "button")
// npm dependencies: @tamagui/button @tamagui/core @tamagui/helpers-tamagui
import { Button } from "../components/tamagui/Button";
export default () => <Button group="btn" backgroundColor="pink">
<Button.Text color="blue group-hover/btn:red">Hello world</Button.Text>
</Button>;

The group prop lets any child reference all of Tamagui’s interactive states like group-hover/btn, group-press/btn, and group-focus/btn for precise styling per interaction state.

Build your own

No two buttons are alike, so @tamagui/button ships the behavior and the parts rather than a finished component. useButton themes the icon props, wraps bare children in a text, and handles the html nesting rules; the skin is yours. This is how tamagui’s own Button is built:

import {
ButtonFrame as ButtonBehaviorFrame,
ButtonText as ButtonBehaviorText,
createStyledContext,
createStyledHOC,
styled,
useButton,
withStaticProperties,
} from '@tamagui/ui'
const ButtonContext = createStyledContext<{ size?: 'sm' | 'md' | 'lg' }>({
size: 'md',
})
const ButtonFrame = styled(ButtonBehaviorFrame, {
context: ButtonContext,
backgroundColor: 'background hover:background-hover press:background-press',
borderColor: 'border-color hover:border-color-hover',
borderWidth: 1,
cursor: 'web:pointer',
})
const ButtonText = styled(ButtonBehaviorText, {
context: ButtonContext,
color: 'color',
fontWeight: '600',
})
const ButtonComponent = createStyledHOC(ButtonFrame, function Button(props, ref) {
const { props: buttonProps } = useButton(props, { Text: ButtonText })
return <ButtonFrame ref={ref} {...buttonProps} />
})
export const Button = withStaticProperties(ButtonComponent, {
Frame: ButtonFrame,
Text: ButtonText,
})

Because the frame declares ButtonContext, passing size to it is what publishes it to the text and icons below. Each component owns its context; there is no shared size context.

useButton also reads ButtonNestingContext, which is how a Button inside another Button (a Dialog.Trigger inside a toolbar button, say) renders as a span instead of nesting an invalid <button> inside a <button>.

For a longer walkthrough see How to Build a Button.

API reference

Button extends View, inheriting all the Tamagui standard props. Sizes come from the shared Sizing ladder.

Props

  • size

    'xs' | 'sm' | 'md' | 'lg' | 'xl'

    A named size ("xs" to "xl", default "md") is a rung in the sizing ladder: padding, border radius, font size, and icon size, never a height. Published to Button.Text and Button.Icon through the Button context.

  • variant

    "outlined" | "quiet"

    "outlined" is transparent with a border, "quiet" is transparent with no border.

  • circular

    boolean

    Forces a square frame with a fully rounded border radius.

  • disabled

    boolean

    Dims the button and sets `pointerEvents: "none"`. On web it also renders a disabled `<button>` and sets `aria-disabled`.

  • theme

    string

    Apply a theme just to the button and its children.

  • icon

    React.ReactNode | React.ComponentType<{ color?: string, size?: number }>

    Any React element or a component. Appears before the text. A component receives 'color' and 'size'.

  • iconAfter

    React.ReactNode | React.ComponentType<{ color?: string, size?: number }>

    Any React element or a component. Appears after the text. A component receives 'color' and 'size'.

  • iconSize

    number

    Explicitly set the icon size in pixels, overriding the one derived from 'size'.

  • scaleIcon

    number

    Scale the icon by this factor. Default is 1, applied after 'iconSize' or the default size calculation.

  • fontFamily

    string

    The one text style prop the frame forwards, since a font family does not vary per breakpoint. Every other text style belongs on `Button.Text`.

  • noTextWrap

    boolean

    Render children as given instead of wrapping bare text in a `Button.Text`.

  • // Web Form Props

    ---

    The following props are web-only and provide full HTML button form semantics. They are ignored on native.

  • type

    "button" | "submit" | "reset"

    Web-only. The button type. Defaults to "button" to prevent unintended form submissions.

  • form

    string

    Web-only. The ID of the form element the button is associated with.

  • formAction

    string

    Web-only. URL for form submission, overrides the form's action.

  • formMethod

    "get" | "post"

    Web-only. HTTP method for form submission.

  • formEncType

    string

    Web-only. Encoding type for form data.

  • formNoValidate

    boolean

    Web-only. Bypass form validation when submitting.

  • formTarget

    string

    Web-only. Where to display form response (_self, _blank, etc).

  • name

    string

    Web-only. Name submitted with form data.

  • value

    string

    Web-only. Value submitted with form data.

  • // Text Styling

    ---

    Apart from `fontFamily`, Button does not forward text style props. Use `Button.Text`, with the `group` prop for interactive states. See the Text Styling section above.

  • Button.Frame

    The button’s View. Renders as <button type="button"> on web with role="button".

    Button.Text

    The text the button wraps bare children in, and what you use to style the label. Extends SizableText.

    Button.Icon

    Sizes and colors an icon from the surrounding size context. Used for the icon and iconAfter props, and available for your own compositions.

    useButton

    Themes the icon and iconAfter props, wraps bare children in a text, and applies the html nesting rules. Returns the props to spread onto a frame, with the ones it consumed removed and everything else untouched.

    const { isNested, props } = useButton(props, { Text: MyButtonText })

    Source

    v2-look Button: named sizes from the inline size table, circular and variant (outlined/quiet) skins, icon + text composition on the unstyled @tamagui/ui Button behavior. This is the exact source the registry ships — copy it into your app and the examples above import from your copy.

    components/tamagui/Button.tsx
    // Styled Button = @tamagui/ui's button behavior and parts + the default v2-look // skin, assembled here in `tamagui`. @tamagui/ui deliberately ships no Button of // its own: no two buttons are alike, so it exposes `useButton` and the frame, // text, and icon parts and lets a skin decide the rest. This is the single skin // definition — `tamagui` exports it as the default `Button`, and the shadcn // registry item is generated from this exact file. import { ButtonFrame as ButtonBehaviorFrame, ButtonIcon as ButtonBehaviorIcon, ButtonText as ButtonBehaviorText, type ButtonBehaviorProps, type ButtonIconProps as ButtonBehaviorIconProps, useButton, } from '@tamagui/button' import { type ComponentSize, createStyledContext, createStyledHOC, type GetProps, resolveSizing, type ResolvedSizing, type SizingEnv, styled, Theme, type ThemeProps, withStaticProperties, } from '@tamagui/core' import { getThemedIconSize } from '@tamagui/helpers-tamagui' import React from 'react' export type ButtonSize = ComponentSize | boolean const ButtonContext = createStyledContext<{ size?: ButtonSize }>({ size: 'md' }) // icon/px lookups outside the size variants: false opts out of the size // styles, so those paths read the default rung instead. undefined when // neither the size nor the default rung resolves. const buttonSizing = ( size: ButtonSize | undefined, env?: SizingEnv ): ResolvedSizing | undefined => resolveSizing(size, env) ?? resolveSizing(undefined, env) const getButtonFrameSize = styled.dynamic<ButtonSize>((val, env) => { const sizing = resolveSizing(val, env) if (!sizing) return return { paddingInline: sizing.paddingInline, paddingBlock: sizing.paddingBlock, borderRadius: sizing.radius, gap: sizing.gap, // keep text buttons at the same outer height as circular buttons: the // text's line box plus vertical padding, plus the frame's 1px border minHeight: sizing.height + 2, // `size` is a control preset, not square geometry. keep the frame's width // content-driven even if an outer styled layer also recognizes `size` as // the generic width/height shorthand. width: 'auto', } }) const getButtonTextSize = styled.dynamic<ButtonSize>((val, env) => { const sizing = resolveSizing(val, env) if (!sizing) return return { fontSize: sizing.fontSize, lineHeight: sizing.lineHeight, } }) const ButtonFrameBase = styled(ButtonBehaviorFrame, { context: ButtonContext, displayName: 'ButtonFrame', className: 'tm-button', backgroundColor: 'background hover:background-hover press:background-press', // flat by default, the way current libraries draw a button (shadcn's // default variant has no border): the fill carries it, and the outlined // variant adds the border back. the transparent border keeps the 1px every // height below accounts for, so the variants line up borderColor: 'transparent', borderStyle: 'solid', borderWidth: 1, cursor: 'web:pointer', opacity: 'press:0.7', outlineColor: 'focus-visible:outline-color', outlineStyle: 'focus-visible:solid', outlineWidth: 'focus-visible:2px', variants: { size: getButtonFrameSize, circular: styled.dynamic<boolean>(), disabled: { true: { opacity: 0.35, }, }, variant: { outlined: { backgroundColor: 'transparent', borderColor: 'border-color', }, quiet: { backgroundColor: 'transparent', borderColor: 'transparent', }, }, } as const, defaultVariants: { size: 'md', }, }) export const ButtonFrame = ButtonFrameBase.resolve((props, env) => { if (!props.circular) return const sizing = buttonSizing(props.size as ButtonSize, env) if (!sizing) return // the control height plus the frame's 1px border on each side const side = sizing.height + 2 return { borderRadius: 1000, paddingHorizontal: 0, height: side, maxHeight: side, maxWidth: side, minWidth: side, width: side, } }) export const ButtonText = styled(ButtonBehaviorText, { context: ButtonContext, displayName: 'ButtonText', color: 'color', fontWeight: '600', userSelect: 'none', variants: { size: getButtonTextSize, } as const, defaultVariants: { size: 'md', }, }) export const ButtonIcon = ({ size, ...props }: ButtonBehaviorIconProps) => { const context = ButtonContext.useStyledContext() return ( <ButtonBehaviorIcon {...props} size={size ?? getThemedIconSize(buttonSizing(context?.size)?.icon)} /> ) } const ButtonComponent = createStyledHOC( ButtonFrame, function Button( props: ButtonBehaviorProps & { size?: ButtonSize; theme?: ThemeProps['name'] }, ref ) { const { theme, ...buttonBehaviorProps } = props const content = ( <Theme name="level2"> <ButtonInner ref={ref} buttonBehaviorProps={buttonBehaviorProps} /> </Theme> ) return theme ? <Theme name={theme}>{content}</Theme> : content }, { disableTheme: true } ) const ButtonInner = React.forwardRef< any, { buttonBehaviorProps: ButtonBehaviorProps & { size?: ButtonSize } } >(function ButtonInner({ buttonBehaviorProps }, ref) { const size = ((buttonBehaviorProps.size as ButtonSize | undefined) ?? ButtonContext.useStyledContext()?.size ?? 'md') as ButtonSize const { props: buttonProps } = useButton(buttonBehaviorProps, { Text: ButtonText, iconSize: getThemedIconSize(buttonSizing(size)?.icon), }) return <ButtonFrame ref={ref} {...buttonProps} /> }) export const Button = withStaticProperties(ButtonComponent, { Frame: ButtonFrame, Icon: ButtonIcon, Text: ButtonText, }) export type ButtonProps = GetProps<typeof ButtonComponent>

    Dependencies

    yarn add @tamagui/button @tamagui/core @tamagui/helpers-tamagui

    Expects theme tokens: background, background-hover, background-press, border-color, border-color-hover, outline-color, color.

    Need raw behavior without any skin? tamagui/unstyled re-exports the @tamagui/ui primitives (advanced).