Bevel

Due-date slip

The library card in the back of a book: each loan or renewal stamps the next ruled line, and the last date is when it's due. Overdue dates are stamped red. For expiring keys, trials, seats and access grants.

An expiring API key

The key lapsed four days ago. Renew it, and again, until the renewals run out.

Production API key · sk-…4f2a

  1. 15 JUL 2026
  2. 14 AUG 2026
  3. 13 OCT 2026

Overdue by 4 days. Was due 13 Oct 2026.

Built from

Due-date slip 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 Due-date slip. 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/due-date-slip
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 Due-date slip is built from: Stamp and seal.

  4. Copy due-date-slip.tsx into components/ui.

    components/due-date-slip.tsx
    "use client"
    
    import * as React from "react"
    import { Button as ButtonPrimitive } from "@base-ui/react/button"
    import { cn } from "cn"
    import { Stamp } from "@/components/ui/stamp"
    
    const DAY = 86_400_000
    
    /** `now` is undefined until the slip knows today: before mount, when no `now` prop was given. */
    type DueDateSlipContextValue = { latest: Date | undefined; now: Date | undefined; statusId: string }
    
    const DueDateSlipContext = React.createContext<DueDateSlipContextValue | null>(null)
    
    function useDueDateSlip() {
      const context = React.useContext(DueDateSlipContext)
      if (!context) throw new Error("DueDateSlip parts must be inside <DueDateSlip>")
      return context
    }
    
    function toDate(d: Date | string) {
      return typeof d === "string" ? new Date(d) : d
    }
    
    /** Fixed locale and time zone, so server and client stamp the same date. */
    function stampText(d: Date) {
      return d
        .toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric", timeZone: "UTC" })
        .toUpperCase()
    }
    
    type DueDateSlipProps = React.ComponentProps<"div"> & {
      /** What's on loan: a key, a seat, a trial. Printed at the top. */
      title: string
      /** Every due date stamped so far, oldest first. The last one counts. */
      dates: (Date | string)[]
      /** Today. Pass it to keep server and client in step. */
      now?: Date | string
    }
    
    /**
     * A library date-due slip. Each loan or renewal stamps the next line;
     * the last date is when it's due. Overdue dates are stamped in red.
     */
    function DueDateSlip({ className, title, dates, now: nowProp, children, ...props }: DueDateSlipProps) {
      const statusId = React.useId()
      // Read the clock after mount, not during render, so a prerendered page and
      // the browser agree. Pass `now` to know it from the first render.
      const [clientNow, setClientNow] = React.useState<Date>()
      React.useEffect(() => {
        if (nowProp === undefined) setClientNow(new Date())
      }, [nowProp])
      const now = React.useMemo(() => (nowProp === undefined ? clientNow : toDate(nowProp)), [nowProp, clientNow])
      const parsed = dates.map(toDate)
      const latest = parsed.at(-1)
      // Stamp only dates added after the slip appeared, not the history.
      const initial = React.useRef(dates.length)
      return (
        <DueDateSlipContext value={{ latest, now, statusId }}>
          <div
            role="group"
            aria-label={`${title}: date due`}
            data-slot="due-date-slip"
            data-overdue={latest && now && latest < now ? "" : undefined}
            className={cn("bevel-paper flex w-64 flex-col rounded-(--bevel-radius-xs) px-4 pt-3 pb-4", className)}
            {...props}
          >
            <p className="truncate text-xs font-semibold">{title}</p>
            <p
              aria-hidden
              className="mt-1 border-b-2 border-(--due-date-slip-header) pb-1 text-center text-sm font-black tracking-(--bevel-tracking-label) text-(--due-date-slip-header)"
            >
              DATE DUE
            </p>
            <ol
              aria-label="Due dates"
              className="mb-3 flex flex-col bg-[linear-gradient(to_bottom,transparent_calc(var(--due-date-slip-line)-1px),var(--due-date-slip-rule)_calc(var(--due-date-slip-line)-1px))] bg-size-[100%_var(--due-date-slip-line)]"
            >
              {parsed.map((d, i) => {
                const last = i === parsed.length - 1
                return (
                  <li
                    key={`${d.getTime()}-${i}`}
                    className={cn("flex h-(--due-date-slip-line) items-center justify-center", !last && "opacity-60")}
                  >
                    <Stamp tone={now && d < now ? "red" : "blue"} animate={i >= initial.current} className="py-0.5">
                      {stampText(d)}
                    </Stamp>
                  </li>
                )
              })}
              {/* Empty lines below, waiting for renewals. */}
              {Array.from({ length: Math.max(0, 4 - parsed.length) }, (_, i) => (
                <li key={`empty-${i}`} aria-hidden className="h-(--due-date-slip-line)" />
              ))}
            </ol>
            {children}
          </div>
        </DueDateSlipContext>
      )
    }
    
    /** When it's due, in words: “Due in 12 days” or “Overdue by 3 days”. Announced when it changes. */
    function DueDateSlipStatus({ className, ...props }: React.ComponentProps<"p">) {
      const { latest, now, statusId } = useDueDateSlip()
      let text = "Not on loan."
      if (latest) {
        const date = stampText(latest).toLowerCase().replace(/(^|\s)\S/g, (c) => c.toUpperCase())
        const days = now ? Math.round((latest.getTime() - now.getTime()) / DAY) : undefined
        text =
          days === undefined
            ? `Due ${date}.`
            : days > 1
            ? `Due ${date}, in ${days} days.`
            : days === 1
              ? `Due tomorrow, ${date}.`
              : days === 0
                ? `Due today.`
                : `Overdue by ${-days} day${days === -1 ? "" : "s"}. Was due ${date}.`
      }
      return (
        <p
          id={statusId}
          role="status"
          data-slot="due-date-slip-status"
          className={cn("text-xs in-data-overdue:font-semibold in-data-overdue:text-(--bevel-ink-red)", className)}
          {...props}
        >
          {text}
        </p>
      )
    }
    
    /** Renews the loan. Your handler adds the next date. */
    function DueDateSlipRenew({ className, ...props }: ButtonPrimitive.Props) {
      const { statusId } = useDueDateSlip()
      return (
        <ButtonPrimitive
          data-slot="due-date-slip-renew"
          aria-describedby={statusId}
          className={cn(
            "bevel-raised mt-2 h-9 rounded-(--bevel-radius-sm) px-4 text-sm font-semibold text-(--bevel-paper-foreground) outline-none [--bevel-material:var(--bevel-paper)]",
            "transition-[translate,box-shadow] duration-(--bevel-duration-press) ease-(--bevel-ease-out) active:translate-y-0.5 active:bevel-pressed motion-reduce:transition-none",
            "focus-visible:ring-3 focus-visible:ring-ring/60 data-disabled:cursor-not-allowed data-disabled:opacity-50",
            className
          )}
          {...props}
        />
      )
    }
    
    export { DueDateSlip, DueDateSlipStatus, DueDateSlipRenew }
    export type { DueDateSlipProps }
  5. Copy due-date-slip.css into your styles.

    styles/due-date-slip.css
    /*
     * Due-date slip: the library card in the back of a book. Each loan or
     * renewal is stamped on the next ruled line; the last stamp is the one
     * that counts. Builds on bevel.css (paper, ruled, ink) and stamp.css.
     */
    
    :root {
      --due-date-slip-paper: oklch(0.93 0.05 95); /* buff card */
      --due-date-slip-rule: oklch(0.7 0.08 240 / 0.45);
      --due-date-slip-header: oklch(0.45 0.12 27);
      --due-date-slip-line: 2rem; /* one ruled line, one stamp */
    }
    
    [data-slot="due-date-slip"] {
      --bevel-paper: var(--due-date-slip-paper);
    }
  6. Import the stylesheets after Tailwind in your global CSS, foundation first.

    globals.css
    @import "./bevel.css";
    @import "./due-date-slip.css";

Anatomy

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

Anatomy
<DueDateSlip title="Production API key" dates={dates} now={today}>
  <DueDateSlipStatus />
  <DueDateSlipRenew onClick={renew}>Renew 30 days</DueDateSlipRenew>
</DueDateSlip>

Parts

PartRendersWhat it does
DueDateSlipdiv (group) + oltitle, dates (oldest first; the last counts), now. New dates land with a stamp; the history doesn't. Dates print in UTC so server and client agree.
DueDateSlipStatusp (status)“Due 16 Nov 2026, in 30 days.” or “Overdue by 3 days.” Describes the renew button.
DueDateSlipRenewButtonYour onClick adds the next date.