@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.
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.
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.
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.
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:
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>.
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.
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.
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>