Button
An interactive element used to trigger actions.
solid
subtle
outline
ghost
link
import { ArrowRightIcon } from 'lucide-react'
import { Button, type ButtonProps } from '~/components/ui/button'
export const Demo = (props: ButtonProps) => {
return (
<Button {...props}>
Button
<ArrowRightIcon />
</Button>
)
}
import { ArrowRightIcon } from 'lucide-solid'
import { Button, type ButtonProps } from '~/components/ui/button'
export const Demo = (props: ButtonProps) => {
return (
<Button {...props}>
Button
<ArrowRightIcon />
</Button>
)
}
<script setup lang="ts">
import { Button } from '~/components/ui/button'
</script>
<template>
<Button>Button</Button>
</template>
Usage
import { Button } from '~/components/ui/button'
Examples
With a different color
Use the colorPalette
prop to change the color of the button.
<HStack>
<Button colorPalette="red" variant="solid">
Button
</Button>
<Button colorPalette="red" variant="subtle">
Button
</Button>
<Button colorPalette="red" variant="outline">
Button
</Button>
<Button colorPalette="red" variant="ghost">
Button
</Button>
</HStack>
With loading indicator
Use the loading
prop to show a loading spinner.
<Button loading>Park UI</Button>
Alternatively, you can set a custom loading text using the loadingText
prop.
<Button loading loadingText="Loading...">
Park UI
</Button>
Renders as a link
Use the asChild
prop to render the button as a link.
<Button asChild>
<a href="https://park-ui.com" target="_blank">
Park UI
</a>
</Button>
Installation
npx @park-ui/cli components add button
1
Add Styled Primitive
Copy the code snippet below into ~/components/ui/styled/button.tsx
import { ark } from '@ark-ui/react/factory'
import { styled } from 'styled-system/jsx'
import { button } from 'styled-system/recipes'
import type { ComponentProps } from 'styled-system/types'
export type ButtonProps = ComponentProps<typeof Button>
export const Button = styled(ark.button, button)
import { ark } from '@ark-ui/solid'
import type { ComponentProps } from 'solid-js'
import { styled } from 'styled-system/jsx'
import { button } from 'styled-system/recipes'
export type ButtonProps = ComponentProps<typeof Button>
export const Button = styled(ark.button, button)
import { styled } from 'styled-system/jsx'
import { button } from 'styled-system/recipes'
import type { ComponentProps } from 'styled-system/types'
export type ButtonProps = ComponentProps<typeof Button>
export const Button = styled('button', button)
2
Add Composition
Copy the composition snippet below into ~/components/ui/button.tsx
import { forwardRef } from 'react'
import { Center, styled } from 'styled-system/jsx'
import { Spinner } from './spinner'
import { Button as StyledButton, type ButtonProps as StyledButtonProps } from './styled/button'
interface ButtonLoadingProps {
loading?: boolean
loadingText?: React.ReactNode
}
export interface ButtonProps extends StyledButtonProps, ButtonLoadingProps {}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
const { loading, disabled, loadingText, children, ...rest } = props
const trulyDisabled = loading || disabled
return (
<StyledButton disabled={trulyDisabled} ref={ref} {...rest}>
{loading && !loadingText ? (
<>
<ButtonSpinner />
<styled.span opacity={0}>{children}</styled.span>
</>
) : loadingText ? (
loadingText
) : (
children
)}
</StyledButton>
)
})
Button.displayName = 'Button'
const ButtonSpinner = () => (
<Center inline position="absolute" transform="translate(-50%, -50%)" top="50%" insetStart="50%">
<Spinner
width="1.1em"
height="1.1em"
borderWidth="1.5px"
borderTopColor="fg.disabled"
borderRightColor="fg.disabled"
/>
</Center>
)
import type { JSX } from 'solid-js'
import { Show, splitProps } from 'solid-js'
import { Center, styled } from 'styled-system/jsx'
import { Spinner } from './spinner'
import { Button as StyledButton, type ButtonProps as StyledButtonProps } from './styled/button'
interface ButtonLoadingProps {
loading?: boolean
loadingText?: JSX.Element
}
export interface ButtonProps extends StyledButtonProps, ButtonLoadingProps {}
export const Button = (props: ButtonProps) => {
const [localProps, rest] = splitProps(props, ['loading', 'disabled', 'loadingText', 'children'])
const trulyDisabled = () => localProps.loading || localProps.disabled
return (
<StyledButton disabled={trulyDisabled()} {...rest}>
<Show
when={localProps.loading && !localProps.loadingText}
fallback={localProps.loadingText || localProps.children}
>
<>
<ButtonSpinner />
<styled.span opacity={0}>{localProps.children}</styled.span>
</>
</Show>
</StyledButton>
)
}
const ButtonSpinner = () => (
<Center inline position="absolute" transform="translate(-50%, -50%)" top="50%" insetStart="50%">
<Spinner
width="1.1em"
height="1.1em"
borderWidth="1.5px"
borderTopColor="fg.disabled"
borderRightColor="fg.disabled"
/>
</Center>
)
export { Button, type ButtonProps } from './styled/button'
3
Integrate Recipe
If you're not using @park-ui/preset
, add the following recipe to yourpanda.config.ts
:
import { defineRecipe } from '@pandacss/dev'
export const button = defineRecipe({
className: 'button',
jsx: ['Button', 'IconButton', 'SubmitButton'],
base: {
alignItems: 'center',
appearance: 'none',
borderRadius: 'l2',
cursor: 'pointer',
display: 'inline-flex',
flexShrink: '0',
fontWeight: 'semibold',
isolation: 'isolate',
minWidth: '0',
justifyContent: 'center',
outline: 'none',
position: 'relative',
transitionDuration: 'normal',
transitionProperty: 'background, border-color, color, box-shadow',
transitionTimingFunction: 'default',
userSelect: 'none',
verticalAlign: 'middle',
whiteSpace: 'nowrap',
_hidden: {
display: 'none',
},
'& :where(svg)': {
fontSize: '1.1em',
width: '1.1em',
height: '1.1em',
},
},
defaultVariants: {
variant: 'solid',
size: 'md',
},
variants: {
variant: {
solid: {
background: 'colorPalette.default',
color: 'colorPalette.fg',
_hover: {
background: 'colorPalette.emphasized',
},
_focusVisible: {
outline: '2px solid',
outlineColor: 'colorPalette.default',
outlineOffset: '2px',
},
_disabled: {
color: 'fg.disabled',
background: 'bg.disabled',
cursor: 'not-allowed',
_hover: {
color: 'fg.disabled',
background: 'bg.disabled',
},
},
},
outline: {
borderWidth: '1px',
borderColor: 'colorPalette.a7',
color: 'colorPalette.text',
colorPalette: 'gray',
_hover: {
background: 'colorPalette.a2',
},
_disabled: {
borderColor: 'border.disabled',
color: 'fg.disabled',
cursor: 'not-allowed',
_hover: {
background: 'transparent',
borderColor: 'border.disabled',
color: 'fg.disabled',
},
},
_focusVisible: {
outline: '2px solid',
outlineColor: 'colorPalette.default',
outlineOffset: '2px',
},
_selected: {
background: 'accent.default',
borderColor: 'accent.default',
color: 'accent.fg',
_hover: {
background: 'accent.emphasized',
borderColor: 'accent.emphasized',
},
},
},
ghost: {
color: 'colorPalette.text',
colorPalette: 'gray',
_hover: {
background: 'colorPalette.a3',
},
_selected: {
background: 'colorPalette.a3',
},
_disabled: {
color: 'fg.disabled',
cursor: 'not-allowed',
_hover: {
background: 'transparent',
color: 'fg.disabled',
},
},
_focusVisible: {
outline: '2px solid',
outlineColor: 'colorPalette.default',
outlineOffset: '2px',
},
},
link: {
verticalAlign: 'baseline',
_disabled: {
color: 'border.disabled',
cursor: 'not-allowed',
_hover: {
color: 'border.disabled',
},
},
height: 'auto!',
px: '0!',
minW: '0!',
},
subtle: {
background: 'colorPalette.a3',
color: 'colorPalette.text',
colorPalette: 'gray',
_hover: {
background: 'colorPalette.a4',
},
_focusVisible: {
outline: '2px solid',
outlineColor: 'colorPalette.default',
outlineOffset: '2px',
},
_disabled: {
background: 'bg.disabled',
color: 'fg.disabled',
cursor: 'not-allowed',
_hover: {
background: 'bg.disabled',
color: 'fg.disabled',
},
},
},
},
size: {
xs: {
h: '8',
minW: '8',
textStyle: 'xs',
px: '3',
gap: '2',
},
sm: {
h: '9',
minW: '9',
textStyle: 'sm',
px: '3.5',
gap: '2',
},
md: {
h: '10',
minW: '10',
textStyle: 'sm',
px: '4',
gap: '2',
},
lg: {
h: '11',
minW: '11',
textStyle: 'md',
px: '4.5',
gap: '2',
},
xl: {
h: '12',
minW: '12',
textStyle: 'md',
px: '5',
gap: '2.5',
},
'2xl': {
h: '16',
minW: '16',
textStyle: 'lg',
px: '7',
gap: '3',
},
},
},
})