Bevel

Approval

An action an agent wants to take, waiting for a person. Say what it does and what can't be undone; press and hold the brass seal to approve.

Before an agent sends email

Hold the seal until it presses. Let go early and nothing happens.

Send the redline to opposing counsel

The drafting agent will email it to j.lee@haleward.com. Sent email can't be recalled.

  • Priya Shah: signed
  • You: not signed yet
Press and hold to approve

Built from

Approval is made of components that also work on their own.

Installation

Add with the CLI

  1. Once per project, add the Bevel registry to components.json.

    components.json
    {
      "registries": {
        "@bevel": "https://bevel-ui.vercel.app/r/{name}.json"
      }
    }
  2. Add Approval. This brings in Stamp and seal and the Bevel foundation, installs @base-ui/react and cn, and merges the styles into your global CSS.

    Terminal
    npx shadcn@latest add @bevel/approval
Install manually
  1. Install the dependencies.

    Terminal
    pnpm add @base-ui/react cn
  2. Add the Bevel foundation, if you haven't already. Light, depth, materials, motion and shape tokens, plus the utilities components use to apply them.

    styles/bevel.css
    /*
     * Bevel foundation. Every component builds on these tokens and utilities;
     * component stylesheets only add their own palettes and shapes.
     *
     * Layers:
     *   1. Light     one source, above and slightly in front of the screen.
     *   2. Depth     named elevations built from the light: recessed, flush,
     *                raised, pressed, object.
     *   3. Material  a surface recipe (leather, plastic, metal) that reads
     *                --bevel-material and --bevel-material-edge.
     *   4. Motion    two curves, a spring, and durations named by purpose.
     *   5. Shape     radii, stitching, and the type scale for amounts.
     */
    
    :root {
      /* 1. Light */
      --bevel-highlight: oklch(1 0 0 / 0.22);
      --bevel-highlight-strong: oklch(1 0 0 / 0.35);
      --bevel-shade: oklch(0 0 0 / 0.32);
      --bevel-shade-strong: oklch(0 0 0 / 0.5);
      --bevel-cast: oklch(0.25 0.03 50 / 0.22);
      /* Stamped into a surface: shaded on the upper inner edge, lit below. */
      --bevel-deboss: 0 -1px 0 oklch(0 0 0 / 0.45), 0 1px 0 oklch(1 0 0 / 0.12);
    
      /* 3. Material defaults; components and variants override these. */
      --bevel-material: oklch(0.47 0.085 52);
      --bevel-material-edge: oklch(0.3 0.06 44);
      --bevel-material-recess: oklch(0.2 0.008 260); /* neutral dark; materials set their own */
      --bevel-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.7' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .7 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
      --bevel-grain-size: 180px;
    
      /* 2. Depth */
      --bevel-depth-recessed:
        inset 0 1px 2px var(--bevel-shade-strong),
        0 1px 0 var(--bevel-highlight);
      --bevel-depth-flush:
        inset 0 1px 0 var(--bevel-highlight),
        inset 0 -1px 0 var(--bevel-shade);
      --bevel-depth-raised:
        inset 0 1px 0 var(--bevel-highlight),
        0 1px 0 var(--bevel-material-edge),
        0 2px 0 var(--bevel-material-edge),
        0 3px 5px var(--bevel-shade);
      --bevel-depth-pressed:
        inset 0 1px 2px var(--bevel-shade),
        0 1px 0 var(--bevel-highlight);
      /* A whole object resting on the page. */
      --bevel-depth-object:
        inset 0 1px 0 var(--bevel-highlight),
        inset 0 -1px 0 var(--bevel-shade),
        0 0 0 1px var(--bevel-material-edge),
        0 1px 1px var(--bevel-cast),
        0 6px 12px -2px var(--bevel-cast),
        0 18px 32px -8px var(--bevel-cast);
      /* The shadow under a loose object, for filter: drop-shadow(). */
      --bevel-depth-cast-sm: 0 4px 6px var(--bevel-cast);
      /* A thin object (a card) lying on another surface. */
      --bevel-depth-sheet:
        inset 0 1px 0 var(--bevel-highlight-strong),
        inset 0 0 0 1px oklch(0 0 0 / 0.12),
        0 -1px 2px oklch(0 0 0 / 0.18);
    
      /* 4. Motion */
      --bevel-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
      --bevel-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
      /* Underdamped spring, ~8% overshoot. Pure CSS; pair with --bevel-duration-move or longer. */
      --bevel-ease-spring: linear(0, 0.06, 0.202, 0.38, 0.561, 0.723, 0.856, 0.955, 1.023, 1.062, 1.081, 1.083, 1.075, 1.062, 1.047, 1.032, 1.019, 1.009, 1.002, 0.997, 0.994, 0.993, 0.993, 0.994, 1);
      --bevel-duration-press: 100ms; /* a control giving under a finger */
      --bevel-duration-small: 200ms; /* hover lifts, tabs, small state changes */
      --bevel-duration-move: 300ms; /* objects moving: cards, drawers */
      --bevel-duration-ambient: 520ms; /* whole-scene changes: lighting */
      --bevel-stagger: 30ms;
      --bevel-lift: 3px; /* how far an object rises on hover */
      --bevel-perspective: 30rem; /* viewing distance for parts that turn in 3D (covers, flaps) */
      /* Unchosen items in a group sit back in shadow. */
      --bevel-dim: 0.82;
    
      /* 5. Shape */
      --bevel-radius-xs: 0.25rem;
      --bevel-radius-sm: 0.5rem;
      --bevel-radius-md: 0.625rem;
      --bevel-radius-lg: 1rem;
      --bevel-radius-xl: 1.5rem;
      --bevel-stitch-width: 1.5px;
      --bevel-stitch-inset: 0.375rem;
      /* Amounts: the figure, its secondary parts (currency, cents), labels, fine print. */
      --bevel-amount: 2.25rem;
      --bevel-amount-minor: 1.25rem;
      --bevel-label: 0.75rem;
      --bevel-fine: 0.6875rem;
      --bevel-tracking-label: 0.08em; /* stamped labels */
      --bevel-tracking-print: 0.06em; /* printed marks on cards */
    
      /* Paper and ink */
      --bevel-paper: oklch(0.975 0.008 85);
      --bevel-paper-foreground: oklch(0.28 0.02 260);
      --bevel-paper-muted: oklch(0.48 0.015 260);
      --bevel-paper-rule: oklch(0.78 0.04 240 / 0.45);
      --bevel-paper-margin: oklch(0.68 0.14 25 / 0.45);
      --bevel-fibre: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='f'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9 .35' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .18 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23f)'/%3E%3C/svg%3E");
      /* Ink soaks unevenly: mostly solid with pale specks. Used as a mask. */
      --bevel-ink-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='i'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -2.4 0 0 0 2.1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23i)'/%3E%3C/svg%3E");
      --bevel-ink-red: oklch(0.52 0.19 27);
      --bevel-ink-blue: oklch(0.44 0.14 262);
      --bevel-ink-green: oklch(0.5 0.12 155);
      --bevel-ink-black: oklch(0.3 0.01 260);
      --bevel-ink-amber: oklch(0.55 0.13 65);
    
      /* Brass and ivory: instrument hardware and dial faces */
      --bevel-brass: oklch(0.76 0.11 82);
      --bevel-brass-edge: oklch(0.52 0.08 70);
      --bevel-ivory: oklch(0.95 0.025 90);
      --bevel-ivory-foreground: oklch(0.25 0.015 60);
      --bevel-needle: oklch(0.55 0.2 27);
    
      /* Desk stationery: legal pads, pens, pencils, exhibit stickers */
      --bevel-pad: oklch(0.95 0.075 98); /* yellow legal pad */
      --bevel-pad-foreground: oklch(0.27 0.03 255);
      --bevel-pad-rule: oklch(0.72 0.06 235 / 0.45);
      --bevel-pad-margin: oklch(0.62 0.16 25 / 0.55);
      --bevel-pen-red: oklch(0.52 0.2 27); /* correcting pen */
      --bevel-pen-blue: oklch(0.42 0.15 262); /* writing pen */
      --bevel-pencil: oklch(0.55 0.01 260); /* not written in ink yet */
      --bevel-sticker: oklch(0.9 0.12 95); /* exhibit sticker */
      --bevel-sticker-foreground: oklch(0.26 0.03 70);
      /* Handwriting. Bring a marker font (e.g. Permanent Marker) for the full effect; these are fallbacks. */
      --bevel-font-marker: "Permanent Marker", "Marker Felt", "Chalkboard SE", ui-rounded, "Arial Rounded MT Bold", system-ui, sans-serif;
    }
    
    .dark {
      --bevel-highlight: oklch(1 0 0 / 0.14);
      /* Paper under dim light, not inverted: it's still paper. */
      --bevel-paper: oklch(0.88 0.008 85);
      --bevel-ivory: oklch(0.86 0.022 90);
      --bevel-pad: oklch(0.86 0.07 98);
      --bevel-cast: oklch(0 0 0 / 0.55);
    }
    
    /* Depth */
    @utility bevel-recessed {
      background-color: var(--bevel-material-recess);
      box-shadow: var(--bevel-depth-recessed);
    }
    @utility bevel-flush {
      box-shadow: var(--bevel-depth-flush);
    }
    @utility bevel-raised {
      background-color: color-mix(in oklch, var(--bevel-material), white 7%);
      background-image: linear-gradient(to bottom, var(--bevel-highlight), transparent 60%);
      box-shadow: var(--bevel-depth-raised);
    }
    @utility bevel-pressed {
      background-color: color-mix(in oklch, var(--bevel-material), black 4%);
      background-image: none;
      box-shadow: var(--bevel-depth-pressed);
    }
    @utility bevel-object {
      box-shadow: var(--bevel-depth-object);
    }
    
    /* Materials */
    @utility bevel-leather {
      background-color: var(--bevel-material);
      background-image:
        var(--bevel-grain),
        linear-gradient(to bottom, var(--bevel-highlight), transparent 28%, transparent 72%, var(--bevel-shade));
      background-size: var(--bevel-grain-size) var(--bevel-grain-size), 100% 100%;
      background-blend-mode: soft-light, normal;
    }
    @utility bevel-plastic {
      background-color: var(--bevel-material);
      background-image:
        linear-gradient(115deg, var(--bevel-highlight-strong), transparent 40%),
        linear-gradient(to bottom, transparent 25%, oklch(0 0 0 / 0.45));
      box-shadow: var(--bevel-depth-sheet);
    }
    @utility bevel-metal {
      background-color: var(--bevel-material);
      background-image:
        linear-gradient(to bottom, var(--bevel-highlight-strong), transparent 45%, var(--bevel-shade) 100%),
        repeating-linear-gradient(90deg, oklch(1 0 0 / 0.06) 0 1px, transparent 1px 3px);
      box-shadow:
        inset 0 0 0 1px oklch(0 0 0 / 0.25),
        inset 0 1px 0 var(--bevel-highlight-strong);
    }
    
    /* Details */
    @utility bevel-deboss {
      text-shadow: var(--bevel-deboss);
    }
    @utility bevel-stitch {
      border-color: var(--bevel-stitch, oklch(0.88 0.05 82 / 0.8));
      border-style: dashed;
      border-width: var(--bevel-stitch-width);
      filter: drop-shadow(0 1px 0 var(--bevel-shade));
    }
    /* A lit fill sitting in a recess (meters, progress). Colour from --bevel-fill. */
    @utility bevel-fill {
      background-color: var(--bevel-fill);
      background-image: linear-gradient(to bottom, oklch(1 0 0 / 0.55), transparent 55%, oklch(0 0 0 / 0.18));
      box-shadow: 0 0 6px -1px var(--bevel-fill);
    }
    /* A small indicator light. Colour from --bevel-led. */
    @utility bevel-led {
      background-color: var(--bevel-led);
      background-image: radial-gradient(circle at 35% 30%, oklch(1 0 0 / 0.85), transparent 45%);
      box-shadow:
        0 0 6px var(--bevel-led),
        inset 0 -1px 1px oklch(0 0 0 / 0.35),
        0 1px 0 var(--bevel-highlight);
    }
    
    /* Paper: a sheet resting on the surface, with fibre in it. */
    @utility bevel-paper {
      background-color: var(--bevel-paper);
      background-image: var(--bevel-fibre);
      background-size: 240px 240px;
      color: var(--bevel-paper-foreground);
      box-shadow:
        0 1px 1px var(--bevel-cast),
        0 6px 14px -4px var(--bevel-cast);
    }
    /* Ink on paper: multiplies into the surface and soaks unevenly. Colour from --bevel-ink. */
    @utility bevel-ink {
      color: var(--bevel-ink, var(--bevel-ink-red));
      border-color: currentColor;
      mix-blend-mode: multiply;
      mask-image: var(--bevel-ink-texture);
      mask-size: 160px 160px;
    }
    /* Glass over an instrument window: one reflection from the light above. Put on an overlay element. */
    @utility bevel-glass {
      pointer-events: none;
      background-image: linear-gradient(165deg, var(--bevel-highlight-strong) 0%, transparent 38%, transparent 70%, var(--bevel-shade) 100%);
    }
    
    /* Material families: one per screen. Set on the screen's container. */
    [data-bevel-family="leather"] {
      --bevel-cast: oklch(0.25 0.03 50 / 0.22);
    }
    [data-bevel-family="paper"] {
      --bevel-cast: oklch(0.3 0.01 250 / 0.16);
    }
    .dark :is([data-bevel-family="leather"], [data-bevel-family="paper"]) {
      --bevel-cast: oklch(0 0 0 / 0.55);
    }
    
    /*
     * Ruled paper: rules every --bevel-rule-height and a margin line at
     * --bevel-rule-margin. Set --bevel-sheet for the paper colour. The ruling
     * scrolls with the content.
     */
    @utility bevel-ruled {
      background-color: var(--bevel-sheet, var(--bevel-paper));
      background-image:
        linear-gradient(
          to right,
          transparent calc(var(--bevel-rule-margin) - 1px),
          var(--bevel-sheet-margin, var(--bevel-paper-margin)) calc(var(--bevel-rule-margin) - 1px) calc(var(--bevel-rule-margin) + 1px),
          transparent calc(var(--bevel-rule-margin) + 1px)
        ),
        repeating-linear-gradient(
          to bottom,
          transparent 0 calc(var(--bevel-rule-height) - 1px),
          var(--bevel-sheet-rule, var(--bevel-paper-rule)) calc(var(--bevel-rule-height) - 1px) var(--bevel-rule-height)
        ),
        var(--bevel-fibre);
      background-size: 100% 100%, 100% 100%, 240px 240px;
      background-attachment: local;
      color: var(--bevel-paper-foreground);
      box-shadow:
        0 1px 1px var(--bevel-cast),
        0 6px 14px -4px var(--bevel-cast);
    }
  3. Install the components Approval is built from: Stamp and seal.

  4. Copy approval.tsx into components/ui.

    components/approval.tsx
    "use client"
    
    import * as React from "react"
    import { Button as ButtonPrimitive } from "@base-ui/react/button"
    import { cn } from "cn"
    import { Seal, Stamp } from "@/components/ui/stamp"
    
    type ApprovalState = "pending" | "approved" | "rejected"
    
    const ApprovalContext = React.createContext<{
      state: ApprovalState
      approve: () => void
      reject: () => void
      titleId: string
    }>({ state: "pending", approve: () => {}, reject: () => {}, titleId: "" })
    
    type ApprovalProps = Omit<React.ComponentProps<"div">, "onReset"> & {
      state?: ApprovalState
      defaultState?: ApprovalState
      onApprove?: () => void
      onReject?: () => void
    }
    
    /**
     * An action waiting for a person: what it does, what it can't undo, who has
     * signed, and the seal to approve it.
     */
    function Approval({
      className,
      state: stateProp,
      defaultState = "pending",
      onApprove,
      onReject,
      children,
      ...props
    }: ApprovalProps) {
      const [uncontrolled, setUncontrolled] = React.useState(defaultState)
      const state = stateProp ?? uncontrolled
      const titleId = React.useId()
      const context = React.useMemo(
        () => ({
          state,
          titleId,
          approve: () => {
            if (stateProp === undefined) setUncontrolled("approved")
            onApprove?.()
          },
          reject: () => {
            if (stateProp === undefined) setUncontrolled("rejected")
            onReject?.()
          },
        }),
        [state, stateProp, titleId, onApprove, onReject]
      )
      return (
        <ApprovalContext value={context}>
          <div
            role="group"
            aria-labelledby={titleId}
            data-slot="approval"
            data-state={state}
            className={cn(
              "bevel-paper relative flex w-full max-w-md flex-col gap-4 rounded-(--bevel-radius-xs) p-5 text-sm",
              className
            )}
            {...props}
          >
            {children}
            <ApprovalOutcome />
          </div>
        </ApprovalContext>
      )
    }
    
    /** The outcome pressed into the card, and announced. */
    function ApprovalOutcome() {
      const { state } = React.useContext(ApprovalContext)
      return (
        <div role="status" className="pointer-events-none absolute top-4 right-4">
          {state === "approved" && (
            <>
              {/* Announce one clear word; the seal's ring text is decoration. */}
              <span className="sr-only">Approved</span>
              <Seal aria-hidden text="Approved · Approved ·" animate className="w-(--approval-seal-size)">
                OK
              </Seal>
            </>
          )}
          {state === "rejected" && (
            <Stamp tone="red" animate>
              Sent back
            </Stamp>
          )}
        </div>
      )
    }
    
    function ApprovalTitle({ className, ...props }: React.ComponentProps<"h3">) {
      const { titleId } = React.useContext(ApprovalContext)
      return (
        <h3
          id={titleId}
          data-slot="approval-title"
          className={cn("pr-24 text-base font-semibold text-(--bevel-paper-foreground)", className)}
          {...props}
        />
      )
    }
    
    /** What happens, and what can't be undone. Say it plainly. */
    function ApprovalDescription({ className, ...props }: React.ComponentProps<"p">) {
      return (
        <p
          data-slot="approval-description"
          className={cn("pr-20 leading-relaxed text-(--bevel-paper-muted)", className)}
          {...props}
        />
      )
    }
    
    function ApprovalSignatures({ className, ...props }: React.ComponentProps<"ul">) {
      return (
        <ul
          aria-label="Signatures"
          data-slot="approval-signatures"
          className={cn("grid grid-cols-2 gap-4", className)}
          {...props}
        />
      )
    }
    
    /** A signature line. Signed names are written over it in marker. */
    function ApprovalSignature({
      name,
      signed = false,
      className,
      ...props
    }: { name: string; signed?: boolean } & Omit<React.ComponentProps<"li">, "children">) {
      return (
        <li data-slot="approval-signature" data-signed={signed ? "" : undefined} className={cn("flex flex-col", className)} {...props}>
          <span
            aria-hidden
            className="flex h-8 items-end border-b border-(--bevel-paper-foreground)/50 pb-0.5 font-(family-name:--bevel-font-marker) text-lg leading-none text-(--bevel-pen-blue)"
          >
            {signed ? name : ""}
          </span>
          <span className="mt-1 text-(length:--bevel-fine) text-(--bevel-paper-muted)">
            {name}
            <span className="sr-only">: {signed ? "signed" : "not signed yet"}</span>
            <span aria-hidden>{signed ? " · signed" : " · awaiting"}</span>
          </span>
        </li>
      )
    }
    
    function ApprovalActions({ className, ...props }: React.ComponentProps<"div">) {
      const { state } = React.useContext(ApprovalContext)
      if (state !== "pending") return null
      return (
        <div
          data-slot="approval-actions"
          className={cn("flex items-center justify-between gap-4 pt-1", className)}
          {...props}
        />
      )
    }
    
    /** Send it back without approving. A plain, single press. */
    function ApprovalReject({ className, onClick, ...props }: ButtonPrimitive.Props) {
      const { reject } = React.useContext(ApprovalContext)
      return (
        <ButtonPrimitive
          data-slot="approval-reject"
          onClick={(event) => {
            onClick?.(event)
            if (!event.defaultPrevented) reject()
          }}
          className={cn(
            "h-9 rounded-(--bevel-radius-sm) border border-(--bevel-paper-foreground)/30 px-3 text-sm font-medium text-(--bevel-paper-foreground) outline-none",
            "hover:bg-(--bevel-paper-foreground)/5 focus-visible:ring-2 focus-visible:ring-(--bevel-pen-blue)/50",
            className
          )}
          {...props}
        />
      )
    }
    
    /** Browsers may report a custom-property duration as "1200ms" or "1.2s". */
    function toMilliseconds(value: string) {
      const n = parseFloat(value)
      if (Number.isNaN(n)) return 1200
      return /ms\s*$/.test(value) ? n : /s\s*$/.test(value) ? n * 1000 : n
    }
    
    type ApprovalSealProps = Omit<ButtonPrimitive.Props, "onClick"> & {
      /** Press and hold to approve (the default), or a single press. */
      requireHold?: boolean
    }
    
    /**
     * The brass embosser. Press and hold (pointer, Enter or Space) until it
     * seals; letting go early springs it back. The hold is deliberate on
     * purpose: approving is hard to undo.
     */
    function ApprovalSeal({ className, requireHold = true, children, ...props }: ApprovalSealProps) {
      const { approve } = React.useContext(ApprovalContext)
      const [holding, setHolding] = React.useState(false)
      const timer = React.useRef<number | undefined>(undefined)
      const ref = React.useRef<HTMLButtonElement>(null)
      const hintId = React.useId()
    
      const start = () => {
        if (!requireHold) return approve()
        if (timer.current !== undefined) return
        const ms = toMilliseconds(getComputedStyle(ref.current!).getPropertyValue("--approval-hold"))
        setHolding(true)
        timer.current = window.setTimeout(() => {
          timer.current = undefined
          setHolding(false)
          approve()
        }, ms)
      }
      const cancel = () => {
        if (timer.current !== undefined) window.clearTimeout(timer.current)
        timer.current = undefined
        setHolding(false)
      }
      React.useEffect(() => cancel, [])
    
      return (
        <div className="flex items-center gap-3">
          <span id={hintId} className="text-right text-(length:--bevel-fine) text-(--bevel-paper-muted)">
            {requireHold ? "Press and hold to approve" : "Press to approve"}
          </span>
          <ButtonPrimitive
            ref={ref}
            data-slot="approval-seal"
            data-holding={holding ? "" : undefined}
            aria-describedby={hintId}
            onPointerDown={(event) => {
              if (event.button !== 0) return
              event.currentTarget.setPointerCapture(event.pointerId)
              start()
            }}
            onPointerUp={cancel}
            onPointerCancel={cancel}
            onLostPointerCapture={cancel}
            onKeyDown={(event) => {
              if ((event.key === "Enter" || event.key === " ") && !event.repeat) {
                event.preventDefault()
                start()
              }
            }}
            onKeyUp={(event) => {
              if (event.key === "Enter" || event.key === " ") cancel()
            }}
            className={cn(
              "bevel-metal bevel-object relative grid size-16 shrink-0 cursor-pointer place-items-center overflow-hidden rounded-full text-(length:--bevel-fine) font-bold tracking-(--bevel-tracking-print) text-(--bevel-ivory-foreground) uppercase outline-none select-none",
              "transition-[translate] duration-(--bevel-duration-press) ease-(--bevel-ease-out) data-holding:translate-y-0.5 motion-reduce:transition-none",
              "focus-visible:ring-3 focus-visible:ring-(--bevel-pen-blue)/50 focus-visible:ring-offset-2",
              className
            )}
            {...props}
          >
            {/* Fills slowly while held; drains fast on release. */}
            <span
              aria-hidden
              className={cn(
                "absolute inset-0 bg-(--approval-press-fill)",
                holding
                  ? "[clip-path:inset(0_0_0_0)] transition-[clip-path] duration-(--approval-hold) ease-linear"
                  : "[clip-path:inset(100%_0_0_0)] transition-[clip-path] duration-(--approval-release) ease-(--bevel-ease-out)",
                "motion-reduce:transition-none"
              )}
            />
            <span className="relative">{children ?? "Seal"}</span>
          </ButtonPrimitive>
        </div>
      )
    }
    
    export {
      Approval,
      ApprovalTitle,
      ApprovalDescription,
      ApprovalSignatures,
      ApprovalSignature,
      ApprovalActions,
      ApprovalReject,
      ApprovalSeal,
    }
    export type { ApprovalProps, ApprovalState, ApprovalSealProps }
  5. Copy approval.css into your styles.

    styles/approval.css
    /*
     * Approval: an action an agent wants to take, waiting for a person. Press
     * and hold the brass embosser to approve; an embossed seal stays on the
     * card. Builds on bevel.css and stamp.css (Seal, Stamp).
     */
    
    :root {
      --approval-hold: 1200ms; /* how long to hold the embosser */
      --approval-release: 200ms; /* letting go early: quick, not deliberate */
      --approval-press-fill: oklch(0.42 0.08 70 / 0.55); /* the embosser darkening as it presses */
      --approval-seal-size: 5rem;
    }
    
    [data-slot="approval-seal"] {
      --bevel-material: var(--bevel-brass);
      --bevel-material-edge: var(--bevel-brass-edge);
    }
  6. Import the stylesheets after Tailwind in your global CSS, foundation first.

    globals.css
    @import "./bevel.css";
    @import "./approval.css";

Anatomy

Each part is its own component. Leave out the ones you don't need.

Anatomy
<Approval onApprove={send} onReject={sendBack}>
  <ApprovalTitle />
  <ApprovalDescription />
  <ApprovalSignatures>
    <ApprovalSignature name="Priya Shah" signed />
  </ApprovalSignatures>
  <ApprovalActions>
    <ApprovalReject />
    <ApprovalSeal />
  </ApprovalActions>
</Approval>

Parts

PartRendersWhat it does
Approvaldiv (group)state (pending, approved, rejected), defaultState, onApprove, onReject. Labelled by its title; the outcome is pressed into the card and announced.
ApprovalDescriptionpWhat happens and what can't be undone.
ApprovalSignatureliA signature line; signed names are written over it. For dual approval.
ApprovalRejectButtonSend it back. A single press.
ApprovalSealButtonPress and hold (pointer, Enter or Space) for --approval-hold to approve; letting go early springs back. requireHold={false} for a single press.