Styled button with variants and sizes, built on the @foldkit/ui Button helper.

Preview
Style
Variants & Sizes
Icon Right
Icon Left
Icon Only
Examples
Invalid States
src/demo/views/button.ts720 lines
import type { Html, HtmlBuilder } from 'foldkit/html'

import { button } from '../../generated/registry/ui/button'
import { icon } from '../../generated/registry/lib/icons'
import { ArrowRight, ArrowLeftCircle } from 'lucide'

import { Schema as S } from 'effect'

import { defineSlice } from '../slice'
import type { Model, Message } from '../assemble'

export const buttonView = (_model: Model, h: HtmlBuilder<Message>): Html =>
  h.div(
    [h.Class('flex w-full flex-col gap-8')],
    [
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Variants & Sizes']),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>({ size: 'xs' }, 'Default', h),
              button<Message>({ size: 'xs', variant: 'secondary' }, 'Secondary', h),
              button<Message>({ size: 'xs', variant: 'outline' }, 'Outline', h),
              button<Message>({ size: 'xs', variant: 'ghost' }, 'Ghost', h),
              button<Message>({ size: 'xs', variant: 'destructive' }, 'Destructive', h),
              button<Message>({ size: 'xs', variant: 'link' }, 'Link', h),
            ],
          ),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>({ size: 'sm' }, 'Default', h),
              button<Message>({ size: 'sm', variant: 'secondary' }, 'Secondary', h),
              button<Message>({ size: 'sm', variant: 'outline' }, 'Outline', h),
              button<Message>({ size: 'sm', variant: 'ghost' }, 'Ghost', h),
              button<Message>({ size: 'sm', variant: 'destructive' }, 'Destructive', h),
              button<Message>({ size: 'sm', variant: 'link' }, 'Link', h),
            ],
          ),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>({}, 'Default', h),
              button<Message>({ variant: 'secondary' }, 'Secondary', h),
              button<Message>({ variant: 'outline' }, 'Outline', h),
              button<Message>({ variant: 'ghost' }, 'Ghost', h),
              button<Message>({ variant: 'destructive' }, 'Destructive', h),
              button<Message>({ variant: 'link' }, 'Link', h),
            ],
          ),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>({ size: 'lg' }, 'Default', h),
              button<Message>({ size: 'lg', variant: 'secondary' }, 'Secondary', h),
              button<Message>({ size: 'lg', variant: 'outline' }, 'Outline', h),
              button<Message>({ size: 'lg', variant: 'ghost' }, 'Ghost', h),
              button<Message>({ size: 'lg', variant: 'destructive' }, 'Destructive', h),
              button<Message>({ size: 'lg', variant: 'link' }, 'Link', h),
            ],
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Icon Right']),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>(
                { size: 'xs' },
                ['Default ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { size: 'xs', variant: 'secondary' },
                ['Secondary ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { size: 'xs', variant: 'outline' },
                ['Outline ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { size: 'xs', variant: 'ghost' },
                ['Ghost ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { size: 'xs', variant: 'destructive' },
                ['Destructive ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { size: 'xs', variant: 'link' },
                ['Link ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
            ],
          ),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>(
                { size: 'sm' },
                ['Default ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { size: 'sm', variant: 'secondary' },
                ['Secondary ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { size: 'sm', variant: 'outline' },
                ['Outline ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { size: 'sm', variant: 'ghost' },
                ['Ghost ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { size: 'sm', variant: 'destructive' },
                ['Destructive ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { size: 'sm', variant: 'link' },
                ['Link ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
            ],
          ),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>({}, ['Default ', icon(h, ArrowRight, 'size-3', 'inline-end')], h),
              button<Message>(
                { variant: 'secondary' },
                ['Secondary ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { variant: 'outline' },
                ['Outline ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { variant: 'ghost' },
                ['Ghost ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { variant: 'destructive' },
                ['Destructive ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { variant: 'link' },
                ['Link ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
            ],
          ),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>(
                { size: 'lg' },
                ['Default ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { size: 'lg', variant: 'secondary' },
                ['Secondary ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { size: 'lg', variant: 'outline' },
                ['Outline ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { size: 'lg', variant: 'ghost' },
                ['Ghost ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { size: 'lg', variant: 'destructive' },
                ['Destructive ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
              button<Message>(
                { size: 'lg', variant: 'link' },
                ['Link ', icon(h, ArrowRight, 'size-3', 'inline-end')],
                h,
              ),
            ],
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Icon Left']),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>(
                { size: 'xs' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Default'],
                h,
              ),
              button<Message>(
                { size: 'xs', variant: 'secondary' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Secondary'],
                h,
              ),
              button<Message>(
                { size: 'xs', variant: 'outline' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Outline'],
                h,
              ),
              button<Message>(
                { size: 'xs', variant: 'ghost' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Ghost'],
                h,
              ),
              button<Message>(
                { size: 'xs', variant: 'destructive' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Destructive'],
                h,
              ),
              button<Message>(
                { size: 'xs', variant: 'link' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Link'],
                h,
              ),
            ],
          ),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>(
                { size: 'sm' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Default'],
                h,
              ),
              button<Message>(
                { size: 'sm', variant: 'secondary' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Secondary'],
                h,
              ),
              button<Message>(
                { size: 'sm', variant: 'outline' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Outline'],
                h,
              ),
              button<Message>(
                { size: 'sm', variant: 'ghost' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Ghost'],
                h,
              ),
              button<Message>(
                { size: 'sm', variant: 'destructive' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Destructive'],
                h,
              ),
              button<Message>(
                { size: 'sm', variant: 'link' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Link'],
                h,
              ),
            ],
          ),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>(
                {},
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Default'],
                h,
              ),
              button<Message>(
                { variant: 'secondary' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Secondary'],
                h,
              ),
              button<Message>(
                { variant: 'outline' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Outline'],
                h,
              ),
              button<Message>(
                { variant: 'ghost' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Ghost'],
                h,
              ),
              button<Message>(
                { variant: 'destructive' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Destructive'],
                h,
              ),
              button<Message>(
                { variant: 'link' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Link'],
                h,
              ),
            ],
          ),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>(
                { size: 'lg' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Default'],
                h,
              ),
              button<Message>(
                { size: 'lg', variant: 'secondary' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Secondary'],
                h,
              ),
              button<Message>(
                { size: 'lg', variant: 'outline' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Outline'],
                h,
              ),
              button<Message>(
                { size: 'lg', variant: 'ghost' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Ghost'],
                h,
              ),
              button<Message>(
                { size: 'lg', variant: 'destructive' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Destructive'],
                h,
              ),
              button<Message>(
                { size: 'lg', variant: 'link' },
                [icon(h, ArrowLeftCircle, 'size-3', 'inline-start'), ' Link'],
                h,
              ),
            ],
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Icon Only']),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>(
                { size: 'icon-xs', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-3'),
                h,
              ),
              button<Message>(
                { size: 'icon-xs', variant: 'secondary', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-3'),
                h,
              ),
              button<Message>(
                { size: 'icon-xs', variant: 'outline', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-3'),
                h,
              ),
              button<Message>(
                { size: 'icon-xs', variant: 'ghost', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-3'),
                h,
              ),
              button<Message>(
                { size: 'icon-xs', variant: 'destructive', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-3'),
                h,
              ),
              button<Message>(
                { size: 'icon-xs', variant: 'link', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-3'),
                h,
              ),
            ],
          ),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>(
                { size: 'icon-sm', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-3.5'),
                h,
              ),
              button<Message>(
                { size: 'icon-sm', variant: 'secondary', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-3.5'),
                h,
              ),
              button<Message>(
                { size: 'icon-sm', variant: 'outline', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-3.5'),
                h,
              ),
              button<Message>(
                { size: 'icon-sm', variant: 'ghost', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-3.5'),
                h,
              ),
              button<Message>(
                { size: 'icon-sm', variant: 'destructive', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-3.5'),
                h,
              ),
              button<Message>(
                { size: 'icon-sm', variant: 'link', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-3.5'),
                h,
              ),
            ],
          ),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>(
                { size: 'icon', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-4'),
                h,
              ),
              button<Message>(
                { size: 'icon', variant: 'secondary', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-4'),
                h,
              ),
              button<Message>(
                { size: 'icon', variant: 'outline', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-4'),
                h,
              ),
              button<Message>(
                { size: 'icon', variant: 'ghost', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-4'),
                h,
              ),
              button<Message>(
                { size: 'icon', variant: 'destructive', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-4'),
                h,
              ),
              button<Message>(
                { size: 'icon', variant: 'link', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-4'),
                h,
              ),
            ],
          ),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>(
                { size: 'icon-lg', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-4'),
                h,
              ),
              button<Message>(
                { size: 'icon-lg', variant: 'secondary', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-4'),
                h,
              ),
              button<Message>(
                { size: 'icon-lg', variant: 'outline', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-4'),
                h,
              ),
              button<Message>(
                { size: 'icon-lg', variant: 'ghost', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-4'),
                h,
              ),
              button<Message>(
                { size: 'icon-lg', variant: 'destructive', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-4'),
                h,
              ),
              button<Message>(
                { size: 'icon-lg', variant: 'link', attributes: [h.AriaLabel('Arrow')] },
                icon(h, ArrowRight, 'size-4'),
                h,
              ),
            ],
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Examples']),
          h.div(
            [h.Class('flex flex-wrap items-center gap-4')],
            [
              h.div(
                [h.Class('flex items-center gap-2')],
                [
                  button<Message>({ variant: 'outline' }, 'Cancel', h),
                  button<Message>({}, ['Submit ', icon(h, ArrowRight, 'size-3', 'inline-end')], h),
                ],
              ),
              h.div(
                [h.Class('flex items-center gap-2')],
                [
                  button<Message>({ variant: 'destructive' }, 'Delete', h),
                  button<Message>(
                    { size: 'icon', attributes: [h.AriaLabel('Action')] },
                    icon(h, ArrowRight, 'size-4'),
                    h,
                  ),
                ],
              ),
            ],
          ),
        ],
      ),
      h.div(
        [h.Class('flex w-full flex-col gap-2')],
        [
          h.div([h.Class('px-1 text-xs font-medium text-muted-foreground')], ['Invalid States']),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>(
                { size: 'xs', attributes: [h.Attribute('aria-invalid', 'true')] },
                'Default',
                h,
              ),
              button<Message>(
                {
                  size: 'xs',
                  variant: 'secondary',
                  attributes: [h.Attribute('aria-invalid', 'true')],
                },
                'Secondary',
                h,
              ),
              button<Message>(
                {
                  size: 'xs',
                  variant: 'outline',
                  attributes: [h.Attribute('aria-invalid', 'true')],
                },
                'Outline',
                h,
              ),
              button<Message>(
                { size: 'xs', variant: 'ghost', attributes: [h.Attribute('aria-invalid', 'true')] },
                'Ghost',
                h,
              ),
              button<Message>(
                {
                  size: 'xs',
                  variant: 'destructive',
                  attributes: [h.Attribute('aria-invalid', 'true')],
                },
                'Destructive',
                h,
              ),
              button<Message>(
                { size: 'xs', variant: 'link', attributes: [h.Attribute('aria-invalid', 'true')] },
                'Link',
                h,
              ),
            ],
          ),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>(
                { size: 'sm', attributes: [h.Attribute('aria-invalid', 'true')] },
                'Default',
                h,
              ),
              button<Message>(
                {
                  size: 'sm',
                  variant: 'secondary',
                  attributes: [h.Attribute('aria-invalid', 'true')],
                },
                'Secondary',
                h,
              ),
              button<Message>(
                {
                  size: 'sm',
                  variant: 'outline',
                  attributes: [h.Attribute('aria-invalid', 'true')],
                },
                'Outline',
                h,
              ),
              button<Message>(
                { size: 'sm', variant: 'ghost', attributes: [h.Attribute('aria-invalid', 'true')] },
                'Ghost',
                h,
              ),
              button<Message>(
                {
                  size: 'sm',
                  variant: 'destructive',
                  attributes: [h.Attribute('aria-invalid', 'true')],
                },
                'Destructive',
                h,
              ),
              button<Message>(
                { size: 'sm', variant: 'link', attributes: [h.Attribute('aria-invalid', 'true')] },
                'Link',
                h,
              ),
            ],
          ),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>({ attributes: [h.Attribute('aria-invalid', 'true')] }, 'Default', h),
              button<Message>(
                { variant: 'secondary', attributes: [h.Attribute('aria-invalid', 'true')] },
                'Secondary',
                h,
              ),
              button<Message>(
                { variant: 'outline', attributes: [h.Attribute('aria-invalid', 'true')] },
                'Outline',
                h,
              ),
              button<Message>(
                { variant: 'ghost', attributes: [h.Attribute('aria-invalid', 'true')] },
                'Ghost',
                h,
              ),
              button<Message>(
                { variant: 'destructive', attributes: [h.Attribute('aria-invalid', 'true')] },
                'Destructive',
                h,
              ),
              button<Message>(
                { variant: 'link', attributes: [h.Attribute('aria-invalid', 'true')] },
                'Link',
                h,
              ),
            ],
          ),
          h.div(
            [h.Class('flex flex-wrap items-center gap-2')],
            [
              button<Message>(
                { size: 'lg', attributes: [h.Attribute('aria-invalid', 'true')] },
                'Default',
                h,
              ),
              button<Message>(
                {
                  size: 'lg',
                  variant: 'secondary',
                  attributes: [h.Attribute('aria-invalid', 'true')],
                },
                'Secondary',
                h,
              ),
              button<Message>(
                {
                  size: 'lg',
                  variant: 'outline',
                  attributes: [h.Attribute('aria-invalid', 'true')],
                },
                'Outline',
                h,
              ),
              button<Message>(
                { size: 'lg', variant: 'ghost', attributes: [h.Attribute('aria-invalid', 'true')] },
                'Ghost',
                h,
              ),
              button<Message>(
                {
                  size: 'lg',
                  variant: 'destructive',
                  attributes: [h.Attribute('aria-invalid', 'true')],
                },
                'Destructive',
                h,
              ),
              button<Message>(
                { size: 'lg', variant: 'link', attributes: [h.Attribute('aria-invalid', 'true')] },
                'Link',
                h,
              ),
            ],
          ),
        ],
      ),
    ],
  )

const fields = {}
const stateSchema = S.Struct(fields)
type State = typeof stateSchema.Type

export const slice = defineSlice({
  fields,
  init: {},
  messages: [],
  handlers: (_model: State) => ({}),
})

This code is shown verbatim — it’s the exact file used for this preview. Much of the surrounding wiring (slice, fields, messages, handlers) belongs to this site’s demo harness; you only need the view and the state that matters for your own app. Adapt what you need.

Installation

Add this component to your project:

pnpm dlx shadcn@latest add @foldcn/button

Source

The component ships as plain source — no build step, no wrapper. Copy it and make it yours.

registry/default/ui/button.ts99 lines
import { Button as FoldkitButton } from '@foldkit/ui'
import type { Attribute, Html, HtmlBuilder } from 'foldkit/html'

import { cn } from '@/lib/utils'

/** Button variant keys. Sync with `buttonVariants` is compiler-enforced:
 *  `buttonVariants` is `Record<ButtonVariant, string>` (missing key = error)
 *  and annotated object literals reject unknown keys. */
export const buttonVariantKeys = [
  'default',
  'destructive',
  'outline',
  'secondary',
  'ghost',
  'link',
] as const

export const buttonVariants: Record<ButtonVariant, string> = {
  default: 'bg-primary text-primary-foreground hover:bg-primary/80',
  destructive: 'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30',
  outline: 'border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground',
  secondary: 'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
  ghost: 'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground',
  link: 'text-primary underline-offset-4 hover:underline',
}

export type ButtonVariant = (typeof buttonVariantKeys)[number]

/** Button size keys. Sync with `buttonSizes` is compiler-enforced (see above). */
export const buttonSizeKeys = [
  'default',
  'xs',
  'sm',
  'lg',
  'icon',
  'icon-xs',
  'icon-sm',
  'icon-lg',
] as const

export const buttonSizes: Record<ButtonSize, string> = {
  default: 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
  xs: 'h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*=\'size-\'])]:size-3',
  sm: 'h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*=\'size-\'])]:size-3.5',
  lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
  icon: 'size-8',
  'icon-xs': 'size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*=\'size-\'])]:size-3',
  'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
  'icon-lg': 'size-9',
}

export type ButtonSize = (typeof buttonSizeKeys)[number]

const buttonBase = 'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 active:not-aria-[haspopup]:translate-y-px [&_svg:not([class*=\'size-\'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none [&_svg]:pointer-events-none [&_svg]:shrink-0 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50'

export type ButtonConfig<M> = Readonly<{
  onClick?: M
  isDisabled?: boolean
  type?: 'button' | 'submit' | 'reset'
  isAutofocus?: boolean
  variant?: ButtonVariant
  size?: ButtonSize
  className?: string
  /** Extra attributes merged onto the button element (ids, handlers,
   *  popoover anchors, …). */
  attributes?: ReadonlyArray<Attribute<M>>
}>

export type ButtonLabel = Html | string | ReadonlyArray<Html | string>

/** Styled button built on the @foldkit/ui Button helper. */
export const button = <M>(config: ButtonConfig<M>, label: ButtonLabel, h: HtmlBuilder<M>): Html =>
  FoldkitButton.view<M>(
    {
      onClick: config.onClick,
      isDisabled: config.isDisabled,
      type: config.type,
      isAutofocus: config.isAutofocus,
      toView: (attributes) =>
        h.button(
          [
            ...attributes.button,
            h.Class(
              cn(
                buttonBase,
                buttonVariants[config.variant ?? 'default'],
                buttonSizes[config.size ?? 'default'],
                config.className,
              ),
            ),
            h.DataAttribute('slot', 'button'),
            ...(config.attributes ?? []),
          ],
          Array.isArray(label) ? label : [label],
        ),
    },
    h,
  )