ephemera

Checkbox

A box ticked in pen, with an animated stroke.

Install

$ npx shadcn@latest add https://ephemera.kerrshift.com/r/checkbox.json

Or copy the source below into components/ui/checkbox.tsx. Core components need the Paper Theme and Radix where noted.

Usage

example.tsx
import { Checkbox } from "@/components/ui/checkbox";

<div className="flex items-center gap-3">
  <Checkbox id="terms" defaultChecked />
  <Label htmlFor="terms">I read the small print</Label>
</div>

Built on the Radix Checkbox, so keyboard and screen reader support come for free.

Props

PropTypeDefaultDescription
checked / defaultCheckedboolean | 'indeterminate'—Controlled or uncontrolled state.
onCheckedChange(checked) => void—Called when ticked or unticked.
disabledboolean—Greyed out and not clickable.

Any other props are passed to the root element.

Fonts

Components read optional CSS variables and fall back to system fonts: --font-display, --font-receipt, --font-type, --font-hand and --font-stamp. This site uses Fraunces, IBM Plex Mono, Courier Prime, Caveat and Anton.

Source

components/ui/checkbox.tsx
"use client";

import * as React from "react";
import { Checkbox as CheckboxPrimitive } from "radix-ui";

/** A box ticked in pen. */
export function Checkbox({ className = "", ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
  return (
    <CheckboxPrimitive.Root
      className={`peer relative inline-flex size-[20px] shrink-0 cursor-pointer items-center justify-center border-[1.5px] border-(--ink-muted) bg-(--paper-raised) outline-none transition-colors hover:border-(--ink) focus-visible:ring-2 focus-visible:ring-(--pen) focus-visible:ring-offset-2 focus-visible:ring-offset-(--paper) disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
      style={{ borderRadius: "3px 4px 2px 5px / 4px 2px 5px 3px" }}
      {...props}
    >
      <CheckboxPrimitive.Indicator forceMount className="pointer-events-none absolute -inset-[3px] data-[state=unchecked]:hidden">
        <svg viewBox="0 0 26 26" className="h-full w-full overflow-visible text-(--pen)" aria-hidden>
          <path
            d="M5 14 C 7 15.5, 9 18, 10.5 20.5 C 13 14, 17 8, 23 2.5"
            fill="none"
            stroke="currentColor"
            strokeWidth="2.6"
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeDasharray="30"
            data-paper-motion
            style={{ animation: "paper-tick .22s cubic-bezier(.3,.6,.4,1) both", strokeDashoffset: 0 }}
          />
        </svg>
      </CheckboxPrimitive.Indicator>
    </CheckboxPrimitive.Root>
  );
}