Progress
A punch card that fills hole by hole.
35% punched
Install
$ npx shadcn@latest add https://ephemera.kerrshift.com/r/progress.jsonOr copy the source below into components/ui/progress.tsx. Core components need the Paper Theme and Radix where noted.
Usage
example.tsx
import { Progress } from "@/components/ui/progress";
<Progress value={60} />
<Progress value={3} max={10} holes={10} />Built on the Radix Progress, so keyboard navigation and screen reader support come for free.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value / max | number | 0 / 100 | How much is done. |
| holes | number | 20 | Number of holes on the punch card. |
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/progress.tsx
"use client";
import * as React from "react";
import { Progress as ProgressPrimitive } from "radix-ui";
export interface ProgressProps extends React.ComponentProps<typeof ProgressPrimitive.Root> {
/** Number of holes on the card. */
holes?: number;
}
/** A punch card: each hole punched is a step closer to done. */
export function Progress({ value = 0, max = 100, holes = 20, className = "", ...props }: ProgressProps) {
const punched = Math.round(((value ?? 0) / max) * holes);
return (
<ProgressPrimitive.Root
value={value}
max={max}
className={`paper relative flex w-full items-center gap-[3px] rounded-[3px] border border-(--edge) bg-(--paper-raised) px-2.5 py-2 ${className}`}
style={{ clipPath: "polygon(8px 0, 100% 0, 100% 100%, 0 100%, 0 8px)" }}
{...props}
>
{Array.from({ length: holes }, (_, i) => (
<span
key={i}
className="aspect-square min-w-0 flex-1 rounded-full transition-[background-color,box-shadow] duration-200"
style={
i < punched
? { background: "var(--ink)", boxShadow: "inset 0 1px 2px rgb(0 0 0 / .6)", transitionDelay: `${i * 25}ms` }
: { boxShadow: "inset 0 0 0 1px var(--edge)" }
}
/>
))}
<ProgressPrimitive.Indicator className="sr-only" />
</ProgressPrimitive.Root>
);
}