Skip to main content

What we’ll build

We’ll build a flexible button component with multiple variants and states from scratch.
1

Create your primitive component

Start with a basic, unstyled button component:
button-primitive.tsx
import { forwardRef, ButtonHTMLAttributes } from 'react'

type ButtonPrimitiveProps = ButtonHTMLAttributes<HTMLButtonElement>

export const ButtonPrimitive = forwardRef<HTMLButtonElement, ButtonPrimitiveProps>(
  ({ children, ...props }, ref) => {
    return (
      <button ref={ref} {...props}>
        {children}
      </button>
    )
  }
)
2

Define your styles

Create a style definition using recast.styles():
button.styles.ts
import { recast } from '@rpxl/recast'

export const buttonStyles = recast.styles({
  base: "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
  variants: {
    variant: {
      default: "bg-blue-600 text-white hover:bg-blue-700",
      destructive: "bg-red-600 text-white hover:bg-red-700",
      outline: "border border-gray-300 bg-white hover:bg-gray-50 hover:text-gray-900",
      secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200",
      ghost: "hover:bg-gray-100 hover:text-gray-900",
      link: "text-blue-600 underline-offset-4 hover:underline"
    },
    size: {
      default: "h-10 px-4 py-2",
      sm: "h-9 rounded-md px-3",
      lg: "h-11 rounded-md px-8",
      icon: "h-10 w-10"
    }
  },
  modifiers: {
    disabled: "opacity-50 cursor-not-allowed",
    loading: "cursor-wait",
    fullWidth: "w-full"
  },
  defaults: {
    variants: { variant: "default", size: "default" }
  }
})
3

Create your styled component

Apply the styles to your primitive:
button.tsx
import { buttonStyles } from './button.styles'
import { ButtonPrimitive } from './button-primitive'

export const Button = buttonStyles(ButtonPrimitive)
4

Use your component

Now you can use your button with full TypeScript support:
<Button>Default Button</Button>
<Button variant="secondary">Secondary</Button>
<Button size="lg">Large Button</Button>

Advanced: Adding conditional styles

For more complex styling logic, use conditionals:
Advanced Button
const advancedButtonStyles = recast.styles({
  base: "...",
  variants: { /* ... */ },
  modifiers: { /* ... */ },
  
  conditionals: [
    {
      variants: { variant: "default" },
      modifiers: ["loading"],
      className: "bg-primary/70" // Lighter primary when default + loading
    }
  ]
})

Advanced: Style extraction

Extract computed styles for use with other libraries:
// Get computed class names
const defaultButtonClasses = buttonStyles.extract({ 
  variant: "default", 
  size: "lg" 
})
// Result: "inline-flex items-center ... bg-primary text-primary-foreground ... h-11 px-8"
You’re ready! You now have a fully functional, type-safe, performant button component. Apply these same patterns to build any component in your design system.