Actions

Button

Clickable button built on react-aria-components, styled with the quebi design system. Six intents and a full size scale.

actionforminteractivecta

Intents

Six intents. Teal primary is the single hero CTA; reach for the others to deprioritize.

Sizes

With icon

Disabled

Source

Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.

"use client"

import {
  Button as ButtonPrimitive,
  type ButtonProps as ButtonPrimitiveProps,
} from "react-aria-components"
import { tv, type VariantProps } from "tailwind-variants"
import { cn } from "@/lib/utils"

/**
 * Button — quebi design system
 *
 * Intents: primary (teal CTA), secondary (solid white-on-dark), outline,
 * ghost, accent (purple), danger (red).
 * Sizes: xs / sm / md (default) / lg / xl, plus square icon-only (sq-*).
 *
 * Depth comes from quebi glows, never drop shadows. Hover lifts with a
 * subtle scale; the brand teal is reserved for the primary CTA.
 */
export const buttonStyles = tv({
  base: [
    "inline-flex items-center justify-center gap-2",
    "font-sans font-semibold whitespace-nowrap select-none cursor-pointer",
    "rounded-quebi-sm border border-solid",
    "transition-all duration-200 ease-out",
    "hover:scale-[1.02] active:scale-100",
    "outline-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-quebi-brand/50 focus-visible:ring-offset-2 focus-visible:ring-offset-quebi-bg",
    "disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100",
    "pending:opacity-70 pending:cursor-wait",
    // react-aria slot conventions — icons & loaders inherit current color
    "*:data-[slot=icon]:shrink-0 *:data-[slot=icon]:self-center",
    "*:data-[slot=loader]:shrink-0 *:data-[slot=loader]:self-center",
  ],
  variants: {
    intent: {
      primary:
        "bg-quebi-brand border-quebi-brand text-quebi-bg hover:bg-quebi-brand-hover hover:border-quebi-brand-hover hover:shadow-quebi-glow-strong",
      secondary:
        "bg-white border-white text-quebi-bg hover:bg-quebi-fg-muted hover:border-quebi-fg-muted",
      outline:
        "bg-transparent border-cyan-500/20 text-white hover:border-quebi-brand hover:text-quebi-brand",
      ghost:
        "bg-transparent border-transparent text-quebi-fg-muted hover:bg-white/[0.04] hover:text-white",
      accent:
        "bg-purple-500 border-purple-500 text-white hover:bg-purple-400 hover:border-purple-400",
      danger:
        "bg-red-500 border-red-500 text-white hover:bg-red-400 hover:border-red-400",
    },
    size: {
      xs: ["text-xs px-2.5 py-1.5", "*:data-[slot=icon]:size-3 *:data-[slot=loader]:size-3"],
      sm: ["text-sm px-3 py-2", "*:data-[slot=icon]:size-3.5 *:data-[slot=loader]:size-3.5"],
      md: ["text-base px-5 py-2.5", "*:data-[slot=icon]:size-4 *:data-[slot=loader]:size-4"],
      lg: ["text-lg px-6 py-3", "*:data-[slot=icon]:size-5 *:data-[slot=loader]:size-5"],
      xl: ["text-xl px-8 py-4", "*:data-[slot=icon]:size-6 *:data-[slot=loader]:size-6"],
      // Square / icon-only
      "sq-xs": "size-7 p-0 *:data-[slot=icon]:size-3.5",
      "sq-sm": "size-9 p-0 *:data-[slot=icon]:size-4",
      "sq-md": "size-11 p-0 *:data-[slot=icon]:size-5",
      "sq-lg": "size-12 p-0 *:data-[slot=icon]:size-6",
    },
    isCircle: {
      true: "rounded-full",
      false: "",
    },
  },
  defaultVariants: {
    intent: "primary",
    size: "md",
    isCircle: false,
  },
})

export interface ButtonProps extends ButtonPrimitiveProps, VariantProps<typeof buttonStyles> {
  ref?: React.Ref<HTMLButtonElement>
}

export function Button({ className, intent, size, isCircle, ref, ...props }: ButtonProps) {
  return (
    <ButtonPrimitive
      ref={ref}
      {...props}
      className={cn(buttonStyles({ intent, size, isCircle }), className)}
    />
  )
}