Skip to content
Ballpoint
Contents

Checkbox

A drawn box that ticks with a pull up and out past the corner, and dashes when indeterminate.

Built on Base UI Checkbox

Once a month, written by hand. Well, typed.

Installation

pnpm dlx shadcn@latest add @ballpoint/checkbox
Or copy the source
ui/checkbox.tsx
"use client";

import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
import { cn } from "@/lib/utils";
import { usePen, type Pen } from "@/hooks/use-ink-box";
import { Stroke } from "@/lib/ink";
import { InkOutline } from "@/lib/ink-outline";
import { dashStroke, tickStroke } from "@/lib/ink-sketch";

/**
 * A drawn box. Checking it ticks it, the pen running up and out past the
 * corner; unchecking pulls the tick back out. Indeterminate is a dash.
 */
function Checkbox({
  className,
  seed,
  roughness,
  passes,
  radius,
  corners,
  draw,
  weight,
  speed,
  ...props
}: CheckboxPrimitive.Root.Props & Omit<Pen, "fill" | "shadow"> & { seed?: string | number }) {
  const pen = usePen({ roughness, passes, radius, corners, draw, weight, speed });
  return (
    <CheckboxPrimitive.Root
      data-slot="checkbox"
      className={cn(
        "peer group/checkbox relative inline-flex size-5 shrink-0 cursor-pointer items-center justify-center outline-none",
        // A finger-sized hit area around the small box.
        "after:absolute after:-inset-x-3 after:-inset-y-2",
        "text-ink-3 transition-colors duration-(--dur-hover) not-data-checked:not-data-indeterminate:hover:text-ink data-checked:text-ink data-indeterminate:text-ink",
        "aria-invalid:text-destructive data-invalid:text-destructive",
        "focus-visible:outline-solid focus-visible:outline-[1.5px] focus-visible:outline-offset-4 focus-visible:outline-ring focus-visible:[border-radius:var(--hand-radius)]",
        "data-disabled:cursor-not-allowed data-disabled:opacity-50",
        className,
      )}
      {...props}
    >
      <InkOutline pen={pen} seed={seed} estimate={[20, 20]} pad={6} maxRadius={6}>
        {({ w, h, s }) => (
          <>
            <Stroke d={tickStroke(s + 40, w)} draw="checked" duration={280} width={1.9} />
            <Stroke d={dashStroke(s + 41, w, h / 2)} draw="indeterminate" duration={200} width={1.9} />
          </>
        )}
      </InkOutline>
    </CheckboxPrimitive.Root>
  );
}

export { Checkbox };

Usage

tsx
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"

<Label>
  <Checkbox defaultChecked />
  Send me the newsletter
</Label>

API reference

indeterminateboolean
Draws a dash instead of a tick.
seedstring | number
Pins the drawing.

Every other prop goes to Base UI's Checkbox.

Pen settings

The drawing also takes draw, roughness, passes, radius, corners, weight and speed, as props or from the nearest InkProvider. What each one does.