Button
A comprehensive button component with multiple variants and sizes
registry/preskok/ui/preskok-ui/button.tsx
"use client"
import React from "react"
import {
Button as ButtonPrimitive,
type ButtonProps as ButtonPrimitiveProps,
} from "react-aria-components/Button"
import { composeRenderProps } from "react-aria-components/composeRenderProps"
import { tv, type VariantProps } from "tailwind-variants"
import { Loader } from "@/components/ui/preskok-ui/loader"
const buttonStyles = tv({
base: [
"[--btn-border:var(--color-foreground)]/15 [--btn-icon:var(--btn-foreground)] [--btn-outline:var(--btn-background)] [--btn-ring:var(--btn-background)]/20",
"bg-(--btn-background) text-(--btn-foreground) ring-(--btn-ring) outline-(--btn-outline) hover:bg-(--btn-overlay) focus-visible:bg-(--btn-overlay) pressed:bg-(--btn-overlay)",
"relative isolate inline-flex items-center justify-center border border-(--btn-border) font-medium",
"transition-transform duration-150 focus:outline-0 focus-visible:ring-2 focus-visible:ring-offset-3 focus-visible:ring-offset-background focus-visible:outline focus-visible:outline-offset-2",
"*:data-[slot=icon]:-mx-0.5 *:data-[slot=icon]:my-0.5 *:data-[slot=icon]:shrink-0 *:data-[slot=icon]:self-center *:data-[slot=icon]:text-(--btn-icon) sm:*:data-[slot=icon]:my-1 forced-colors:[--btn-icon:ButtonText]",
"[&_svg.lucide]:-mx-0.5 [&_svg.lucide]:my-0.5 [&_svg.lucide]:shrink-0 [&_svg.lucide]:self-center [&_svg.lucide]:text-(--btn-icon) sm:[&_svg.lucide]:my-1",
"*:data-[slot=loader]:-mx-0.5 *:data-[slot=loader]:my-0.5 *:data-[slot=loader]:shrink-0 *:data-[slot=loader]:self-center *:data-[slot=loader]:text-(--btn-icon) sm:*:data-[slot=loader]:my-1",
"[&_svg:not([class*='size-'])]:size-4",
"[&_svg]:shrink-0",
],
variants: {
intent: {
primary:
"[--btn-background:var(--color-primary)] [--btn-foreground:var(--color-primary-foreground)] [--btn-overlay:var(--color-primary)]/85",
secondary:
"[--btn-background:var(--color-secondary)] [--btn-foreground:var(--color-secondary-foreground)] [--btn-outline:var(--color-secondary-foreground)] [--btn-overlay:var(--color-secondary)]/85 [--btn-ring:var(--color-muted-foreground)]/20",
warning:
"[--btn-background:var(--color-warning)] [--btn-foreground:var(--color-warning-foreground)] [--btn-overlay:var(--color-warning)]/85",
danger:
"[--btn-background:var(--color-destructive)] [--btn-foreground:var(--color-destructive-foreground)] [--btn-overlay:var(--color-destructive)]/85",
outline:
"border-border [--btn-background:transparent] [--btn-foreground:var(--color-foreground)] [--btn-outline:var(--color-ring)] [--btn-overlay:var(--color-accent)] [--btn-ring:var(--color-ring)]/20",
plain:
"border-transparent [--btn-background:transparent] [--btn-foreground:var(--color-foreground)] [--btn-outline:var(--color-ring)] [--btn-overlay:var(--color-accent)] [--btn-ring:var(--color-ring)]/20",
},
size: {
xs: [
"gap-x-1 px-2.5 py-1.5 text-sm sm:px-2 sm:py-[--spacing(1.4)] sm:text-xs/4",
"*:data-[slot=icon]:size-3.5 sm:*:data-[slot=icon]:size-3",
"[&_svg.lucide]:size-3.5 sm:[&_svg.lucide]:size-3",
"*:data-[slot=loader]:size-3.5 sm:*:data-[slot=loader]:size-3",
],
sm: [
"gap-x-1.5 px-3 py-2 sm:px-2.5 sm:py-1.5 sm:text-sm/5",
"*:data-[slot=icon]:size-4.5 sm:*:data-[slot=icon]:size-4",
"[&_svg.lucide]:size-4.5 sm:[&_svg.lucide]:size-4",
"*:data-[slot=loader]:size-4.5 sm:*:data-[slot=loader]:size-4",
],
md: [
"gap-x-2 px-3.5 py-2 sm:px-3 sm:py-1.5 sm:text-sm/6",
"*:data-[slot=icon]:size-5 sm:*:data-[slot=icon]:size-4",
"[&_svg.lucide]:size-5 sm:[&_svg.lucide]:size-4",
"*:data-[slot=loader]:size-5 sm:*:data-[slot=loader]:size-4",
],
lg: [
"gap-x-2 px-4 py-2.5 sm:px-3.5 sm:py-2 sm:text-sm/6",
"*:data-[slot=icon]:size-5 sm:*:data-[slot=icon]:size-4.5",
"[&_svg.lucide]:size-5 sm:[&_svg.lucide]:size-4.5",
"*:data-[slot=loader]:size-5 sm:*:data-[slot=loader]:size-4.5",
],
"sq-xs":
"size-8 *:data-[slot=icon]:size-3.5 *:data-[slot=loader]:size-3.5 sm:size-7 sm:*:data-[slot=icon]:size-3 sm:*:data-[slot=loader]:size-3 [&_svg.lucide]:size-3.5 sm:[&_svg.lucide]:size-3",
"sq-sm":
"size-9 *:data-[slot=icon]:size-4.5 *:data-[slot=loader]:size-4.5 sm:size-8 sm:*:data-[slot=icon]:size-4 sm:*:data-[slot=loader]:size-4 [&_svg.lucide]:size-4.5 sm:[&_svg.lucide]:size-4",
"sq-md":
"size-10 *:data-[slot=icon]:size-5 *:data-[slot=loader]:size-5 sm:size-9 sm:*:data-[slot=icon]:size-4 sm:*:data-[slot=loader]:size-4 [&_svg.lucide]:size-5 sm:[&_svg.lucide]:size-4",
"sq-lg":
"size-11 *:data-[slot=icon]:size-5 *:data-[slot=loader]:size-5 sm:size-10 sm:*:data-[slot=icon]:size-4.5 sm:*:data-[slot=loader]:size-4.5 [&_svg.lucide]:size-5 sm:[&_svg.lucide]:size-4.5",
},
isCircle: {
true: "rounded-full",
false: "rounded-lg",
},
isDisabled: {
true: "opacity-50 forced-colors:text-[GrayText]",
},
isPending: {
true: "opacity-50",
},
isPressed: {
true: "not-aria-[haspopup]:translate-y-px",
},
},
defaultVariants: {
intent: "primary",
size: "md",
isCircle: false,
},
compoundVariants: [
{
size: ["xs", "sq-xs"],
className:
"rounded-md *:data-[slot=icon]:size-3.5 [&_svg.lucide]:size-3.5",
},
],
})
interface ButtonProps
extends ButtonPrimitiveProps, VariantProps<typeof buttonStyles> {
ref?: React.Ref<HTMLButtonElement>
}
const Button = ({
className,
intent,
size,
isCircle,
ref,
...props
}: ButtonProps) => {
return (
<ButtonPrimitive
ref={ref}
{...props}
className={composeRenderProps(className, (className, renderProps) =>
buttonStyles({
...renderProps,
intent,
size,
isCircle,
className,
})
)}
>
{(values) => (
<>
{values.isPending && typeof props.children !== "function" && (
<Loader variant="spin" />
)}
{typeof props.children === "function"
? props.children(values)
: props.children}
</>
)}
</ButtonPrimitive>
)
}
export { Button, buttonStyles }
export type { ButtonProps }
No selection tags yet — empty means no evidence, not lack of support.
DependenciesBundle weight · light
react-aria-componentstailwind-variants