Skip to content
64 linesCodeBlameRaw
1import { type VariantProps, cva } from "class-variance-authority";
2import { Slot } from "radix-ui";
3import type { ComponentProps } from "react";
4
5import { cn } from "../../lib/cn";
6
7// shadcn/ui's button, styled with g1t's tokens: the one button. `asChild`
8// puts its look on a <Link> or a menu's trigger; a form's submit button
9// that says it is working is `SubmitButton` in ./index, and a link that
10// looks like one is `ButtonLink` there, both drawn with `buttonVariants`.
11//
12// Variants: `default` is the plain dark-on-light (light-on-dark) button for
13// the main action; `accent` the lavender one, for the one action a page
14// is for; `outline` a bordered quiet one; `secondary` a raised one;
15// `ghost` bare until hovered, for icons and toolbars; `soft` the accent as
16// a wash, the one tinted control in a row, such as Ask g1t; `destructive`
17// for what cannot be undone; `link` words alone.
18//
19// Sizes: `default` 36px, `sm` 32px, `xs` 28px, `lg` 40px; the icon sizes
20// are squares of the same. `bar` is the top bar's: 32px on a computer, 44px
21// to a finger on a phone, and `icon-bar` its square.
22
23export const buttonVariants = cva(
24 "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap outline-none transition-[color,background-color,border-color,box-shadow] focus-visible:ring-2 focus-visible:ring-accent disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
25 {
26 variants: {
27 variant: {
28 default: "bg-fg text-bg hover:bg-fg-hover",
29 accent: "bg-accent text-bg hover:bg-accent-hover",
30 outline: "border border-line text-fg/80 hover:border-line-strong hover:bg-surface hover:text-fg",
31 secondary: "bg-raised text-fg hover:bg-line",
32 ghost: "text-muted hover:bg-raised hover:text-fg data-[state=open]:bg-raised data-[state=open]:text-fg aria-pressed:bg-raised aria-pressed:text-fg",
33 soft: "bg-accent/10 text-fg ring-1 ring-accent/20 ring-inset hover:bg-accent/15 hover:ring-accent/35",
34 destructive: "border border-danger/40 text-danger hover:border-danger hover:bg-danger/10",
35 link: "font-normal text-accent underline-offset-4 hover:underline",
36 },
37 size: {
38 default: "h-9 px-3.5",
39 sm: "h-8 gap-1.5 px-2.5 text-[0.8125rem]",
40 xs: "h-7 gap-1 px-2 text-xs",
41 lg: "h-10 px-5",
42 bar: "h-8 gap-1.5 px-2 text-[0.8125rem] max-md:h-11 max-md:min-w-11",
43 icon: "size-9",
44 "icon-sm": "size-8",
45 "icon-xs": "size-7",
46 "icon-lg": "size-10",
47 "icon-bar": "size-8 max-md:size-11",
48 // Words in a sentence, for the `link` variant: no box of its own.
49 inline: "h-auto gap-1.5 p-0 text-[length:inherit]",
50 },
51 },
52 defaultVariants: { variant: "default", size: "default" },
53 },
54);
55
56export type ButtonVariant = NonNullable<VariantProps<typeof buttonVariants>["variant"]>;
57export type ButtonSize = NonNullable<VariantProps<typeof buttonVariants>["size"]>;
58
59export type ButtonProps = ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean };
60
61export function Button({ className, variant, size, asChild = false, ...props }: ButtonProps) {
62 const Comp = asChild ? Slot.Root : "button";
63 return <Comp data-slot="button" className={cn(buttonVariants({ variant, size }), className)} {...props} />;
64}