All components
Button
Trigger actions with solid, surface, outline, ghost, and glass variants across six sizes — polymorphic via asChild (Ark) or render (Base UI).
123456789101112131415161718192021222324import { Button } from "#components/ui/base-ui/button.tsx";import { indigoPalette } from "@stylexblocks/ui/themes/palettes.ts";
export function Example() { return ( <> {/* variant + size props resolve through the StyleX recipe and are exposed as data-variant / data-size attributes */} <Button variant="solid" size="md" palette={indigoPalette} onClick={save}> Get started </Button>
{/* composition: the render prop replaces the underlying element */} <Button variant="surface" size="md" render={<a href="/docs" />}> Read the docs </Button>
{/* native props merge through mergeProps */} <Button variant="outline" disabled aria-label="Retry sync"> Retry </Button> </> );}Variants
solid
subtle
surface
outline
ghost
plain
Sizes
xs
sm
md
lg
xl
Install
1npx stylexblocks add buttonSource
components/ui/base-ui/button.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242import * as stylex from "@stylexjs/stylex";import { createComponent } from "#lib/base-ui/create-component.tsx";import { defineRecipe, fromMap } from "#lib/recipe-props.ts";import { buttonIconVars } from '@stylexblocks/ui/recipes/button.stylex.ts'
import { colorPalette } from '@stylexblocks/ui/color-palette.stylex.ts'import { durations } from '@stylexblocks/ui/tokens/durations.stylex.ts'import { fontWeights } from '@stylexblocks/ui/tokens/font-weights.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { semanticRadii } from '@stylexblocks/ui/semantic-tokens/radii.stylex.ts'import { semanticShadows } from '@stylexblocks/ui/semantic-tokens/shadows.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'import { textStyles } from '@stylexblocks/ui/text-styles.ts'
const buttonStyles = stylex.create({ base: { display: 'inline-flex', appearance: 'none', alignItems: 'center', justifyContent: 'center', userSelect: 'none', position: 'relative', whiteSpace: 'nowrap', verticalAlign: 'middle', cursor: { default: 'pointer', ':is(:disabled, [disabled], [data-disabled])': 'not-allowed', }, flexShrink: 0, outline: 'none', lineHeight: 1.2, isolation: 'isolate', fontWeight: fontWeights.medium, transitionProperty: 'background-color, border-color, color, fill, stroke, opacity, box-shadow, transform', transitionDuration: durations.moderate, outlineWidth: { default: null, ':focus-visible': '1px', }, outlineOffset: { default: null, ':focus-visible': '2px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, [buttonIconVars.fontSize]: '1em', [buttonIconVars.flexShrink]: 0, },})
const buttonSizes = stylex.create({ '2xs': { gap: spacing._1, height: sizes.controlXs, minWidth: sizes.controlXs, borderRadius: semanticRadii.control, paddingInline: spacing._2, }, xs: { gap: spacing._1, height: sizes.controlXs, minWidth: sizes.controlXs, borderRadius: semanticRadii.control, paddingInline: spacing._2, }, sm: { gap: spacing._2, height: sizes.controlSm, minWidth: sizes.controlSm, borderRadius: semanticRadii.control, paddingInline: spacing._2_5, }, md: { gap: spacing._2, height: sizes.controlMd, minWidth: sizes.controlMd, borderRadius: semanticRadii.control, paddingInline: spacing._3, }, lg: { gap: spacing._3, height: sizes.controlLg, minWidth: sizes.controlLg, borderRadius: semanticRadii.control, paddingInline: spacing._4_5, }, xl: { gap: spacing._3, height: sizes.controlXl, minWidth: sizes.controlXl, borderRadius: semanticRadii.control, paddingInline: spacing._6, },})
const buttonVariants = stylex.create({ solid: { backgroundColor: { default: colorPalette.solid, ':hover': `color-mix(in oklch, ${colorPalette.solid} 90%, transparent)`, }, boxShadow: semanticShadows.sm, color: colorPalette.contrast, }, subtle: { backgroundColor: { default: `color-mix(in oklch, ${colorPalette.solid} 10%, transparent)`, ':hover': `color-mix(in oklch, ${colorPalette.solid} 20%, transparent)`, }, color: colorPalette.fg, }, glass: { backgroundColor: { default: colorPalette.solid, ':hover': colorPalette.solid, }, color: colorPalette.contrast, '--btn-shadow': semanticShadows.sm, boxShadow: 'light-dark(0 0 0 1px rgba(0,0,0,0.25) inset, 0px 2px 0px 0px rgba(255,255,255,0.2) inset, var(--btn-shadow), 0px 1px 0px 0px rgba(255,255,255,0.2) inset, var(--btn-shadow))', textShadow: '0 1px 2px rgba(0,0,0,0.1)', overflow: 'clip', '::after': { content: '""', position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundImage: { default: 'linear-gradient(180deg, white 40%, rgba(0,0,0,0.2))', ':hover': 'linear-gradient(180deg, rgba(255,255,255,0.8) 40%, rgba(0,0,0,0.6))', }, opacity: 0.2, transitionProperty: 'opacity', transitionDuration: durations.moderate, pointerEvents: 'none', }, }, surface: { backgroundColor: { default: semanticColors.bg, ':hover': colorPalette.muted, }, borderWidth: '1px', borderColor: { default: `color-mix(in oklch, ${colorPalette.emphasized} 90%, transparent)`, ':hover': colorPalette.emphasized, }, color: colorPalette.fg, }, outline: { borderWidth: '0.5px', borderColor: colorPalette.emphasized, color: colorPalette.fg, backgroundColor: { default: null, ':hover': colorPalette.muted, }, }, ghost: { color: colorPalette.fg, backgroundColor: { default: null, ':hover': colorPalette.subtle, }, }, plain: { color: colorPalette.fg, paddingInline: 0, },})
const buttonTextStyles = { '2xs': textStyles.xs, xs: textStyles.xs, sm: textStyles.sm, md: textStyles.sm, lg: textStyles.md, xl: textStyles.lg,} as const
const buttonWhenVariantSurface = stylex.create({ sm: { boxShadow: semanticShadows.xs, }, md: { boxShadow: semanticShadows.sm, }, lg: { boxShadow: semanticShadows.sm, }, xl: { boxShadow: semanticShadows.md, },})
const buttonIcon = stylex.create({ base: { fontSize: buttonIconVars.fontSize, flexShrink: buttonIconVars.flexShrink, },})
export type ButtonSize = keyof typeof buttonSizes
export type ButtonVariant = keyof typeof buttonVariants
/** * Button — recipe and component colocated in one file. Variant/size props * resolve mechanically from the axis maps; `extend` carries what a single * map per axis can't express (the per-size text styles and the * surface-variant compound). Composition via `render`. */const buttonRecipe = defineRecipe({ styles: buttonStyles, axes: { variant: buttonVariants, size: buttonSizes }, defaultVariants: { variant: "surface", size: "md" }, extend: (v) => [ buttonTextStyles[v.size], v.variant === "surface" && fromMap(buttonWhenVariantSurface, v.size), ],});
export const Button = createComponent("button", buttonRecipe, "Button");
export type ButtonProps = React.ComponentProps<typeof Button>;