Keysort filter
Edge-notched cards from the 1950s office: each hole along the top is an attribute, notched open if the card has it. Push needles down the columns and the cards notched for all of them fall free. AND filtering you can see. For documents, tickets and records with tags.
A drawer of contracts
Push needles through to find the contracts with every attribute.
- Acme mutual NDA
- Globex NDA, redlined
- Initech supply agreement
- Umbrella term sheet
- Hooli data processing addendum
- Stark consulting SOW
- Wayne one-way NDA
- Tyrell DPA, draft
0 cards. Push a needle in to sort.
Installation
Add with the CLI
Once per project, add the Bevel registry to
components.json.components.json { "registries": { "@bevel": "https://bevel-ui.vercel.app/r/{name}.json" } }Add Keysort filter. This brings in the Bevel foundation, installs @base-ui/react and cn, and merges the styles into your global CSS.
Terminal npx shadcn@latest add @bevel/keysort-filter
Install manually
Install the dependencies.
Terminal pnpm add @base-ui/react cnAdd 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); }Copy
keysort-filter.tsxintocomponents/ui.components/keysort-filter.tsx "use client" import * as React from "react" import { Toggle } from "@base-ui/react/toggle" import { cn } from "cn" type Needle = { value: string; label: string } type KeysortFilterContextValue = { needles: Needle[] registerNeedle: (needle: Needle) => () => void selected: string[] toggle: (value: string, on: boolean) => void cards: Record<string, string[]> registerCard: (id: string, notches: string[]) => () => void } const KeysortFilterContext = React.createContext<KeysortFilterContextValue | null>(null) function useKeysortFilter() { const context = React.useContext(KeysortFilterContext) if (!context) throw new Error("KeysortFilter parts must be inside <KeysortFilter>") return context } function matches(notches: string[], selected: string[]) { return selected.length > 0 && selected.every((s) => notches.includes(s)) } type KeysortFilterProps = Omit<React.ComponentProps<"div">, "defaultValue"> & { /** The needles pushed in, by value. */ value?: string[] defaultValue?: string[] onValueChange?: (value: string[]) => void } /** * Edge-notched cards. Each column of holes is an attribute; push needles * down the columns and the cards notched for all of them fall free. AND * filtering you can see. Give it an aria-label. */ function KeysortFilter({ className, value, defaultValue = [], onValueChange, style, children, ...props }: KeysortFilterProps) { const [needles, setNeedles] = React.useState<Needle[]>([]) const [cards, setCards] = React.useState<Record<string, string[]>>({}) const [uncontrolled, setUncontrolled] = React.useState(defaultValue) const selected = value ?? uncontrolled const registerNeedle = React.useCallback((needle: Needle) => { // Needles keep the order they mounted in, which is their column order. setNeedles((all) => all.some((n) => n.value === needle.value) ? all.map((n) => (n.value === needle.value ? needle : n)) : [...all, needle] ) return () => setNeedles((all) => all.filter((n) => n.value !== needle.value)) }, []) const registerCard = React.useCallback((id: string, notches: string[]) => { setCards((all) => ({ ...all, [id]: notches })) return () => setCards((all) => { const next = { ...all } delete next[id] return next }) }, []) const toggle = React.useCallback( (needle: string, on: boolean) => { const next = on ? [...selected.filter((s) => s !== needle), needle] : selected.filter((s) => s !== needle) setUncontrolled(next) onValueChange?.(next) }, [selected, onValueChange] ) const context = React.useMemo( () => ({ needles, registerNeedle, selected, toggle, cards, registerCard }), [needles, registerNeedle, selected, toggle, cards, registerCard] ) return ( <KeysortFilterContext value={context}> <div role="group" data-slot="keysort-filter" data-sorting={selected.length ? "" : undefined} className={cn( "grid w-full grid-cols-[minmax(6rem,1fr)_repeat(var(--keysort-filter-needles),var(--keysort-filter-column))] gap-y-3", className )} style={{ "--keysort-filter-needles": Math.max(needles.length, 1), ...style } as React.CSSProperties} {...props} > {children} </div> </KeysortFilterContext> ) } /** The needle handles, one above each column of holes. */ function KeysortFilterNeedles({ className, children, ...props }: React.ComponentProps<"div">) { return ( <div role="group" aria-label="Needles" data-slot="keysort-filter-needles" className={cn("col-span-full grid grid-cols-subgrid items-end", className)} {...props} > <span aria-hidden /> {children} </div> ) } type KeysortFilterNeedleProps = Omit<Toggle.Props, "value" | "children" | "pressed"> & { value: string /** The attribute's name. Keep it to a word or two. */ children: string } /** One needle: a toggle. Pushed in, it catches every card not notched for it. */ function KeysortFilterNeedle({ className, value, children, ...props }: KeysortFilterNeedleProps) { const { registerNeedle, selected, toggle } = useKeysortFilter() React.useEffect(() => registerNeedle({ value, label: children }), [value, children, registerNeedle]) return ( <Toggle data-slot="keysort-filter-needle" pressed={selected.includes(value)} onPressedChange={(on) => toggle(value, on)} className={cn( "group/needle flex flex-col items-center gap-1 rounded-(--bevel-radius-sm) px-0.5 pb-1 text-center text-(length:--bevel-fine) leading-tight sm:text-xs font-medium outline-none", "focus-visible:ring-3 focus-visible:ring-ring/60 data-pressed:font-semibold", className )} {...props} > {children} {/* The ring handle: it sinks as the needle goes in. */} <span aria-hidden className="bevel-metal size-4 rounded-full border-3 border-(--keysort-filter-needle-edge) bg-transparent transition-[translate] duration-(--bevel-duration-small) ease-(--bevel-ease-spring) group-data-pressed/needle:translate-y-1.5 motion-reduce:transition-none" /> </Toggle> ) } /** The deck. The needles run down it, behind the cards. */ function KeysortFilterDeck({ className, children, ...props }: React.ComponentProps<"ul">) { const { needles, selected } = useKeysortFilter() return ( <ul data-slot="keysort-filter-deck" className={cn("relative col-span-full grid grid-cols-subgrid gap-y-1.5", className)} {...props} > {needles.map((n, i) => ( <span key={n.value} aria-hidden data-in={selected.includes(n.value) ? "" : undefined} className="bevel-metal pointer-events-none absolute -top-2 bottom-0 w-1 origin-top scale-y-0 rounded-full transition-[scale] duration-(--bevel-duration-move) ease-(--bevel-ease-out) data-in:scale-y-100 motion-reduce:transition-none" style={{ right: `calc(var(--keysort-filter-column) * ${needles.length - 1 - i} + var(--keysort-filter-column) / 2 - var(--spacing) / 2)` }} /> ))} {children} </ul> ) } type KeysortFilterCardProps = React.ComponentProps<"li"> & { /** The attributes this card has: their holes are notched open. */ notches: string[] } /** One card. It falls free when it's notched for every needle in, and says so. */ function KeysortFilterCard({ className, notches, children, ...props }: KeysortFilterCardProps) { const { needles, selected, registerCard } = useKeysortFilter() const id = React.useId() const key = notches.join("\u0000") React.useEffect(() => registerCard(id, key ? key.split("\u0000") : []), [id, key, registerCard]) const match = matches(notches, selected) return ( <li data-slot="keysort-filter-card" data-card={selected.length === 0 ? undefined : match ? "match" : "held"} className={cn( "bevel-paper relative col-span-full grid grid-cols-subgrid items-center rounded-(--bevel-radius-xs) bg-(--keysort-filter-card) py-2 text-(--keysort-filter-card-foreground)", "data-[card=held]:bg-(--keysort-filter-card-held) data-[card=held]:shadow-none", className )} {...props} > <span className="flex min-w-0 items-center gap-2 pl-3"> {/* A tab marks the cards that fell free. */} <span aria-hidden className="h-4 w-1 shrink-0 scale-y-0 rounded-full bg-(--keysort-filter-match) transition-[scale] duration-(--bevel-duration-small) ease-(--bevel-ease-spring) in-data-[card=match]:scale-y-100 motion-reduce:transition-none" /> <span className="line-clamp-2 text-sm leading-tight font-semibold">{children}</span> <span className="sr-only">{match ? "Matches." : ""}</span> </span> {needles.map((n) => { const notched = notches.includes(n.value) const caught = selected.includes(n.value) && !notched return ( <span key={n.value} aria-hidden className="relative flex h-full justify-center"> {/* A notched hole is cut open to the top edge; a caught card has the needle through its hole. */} {notched ? ( <span className="absolute -top-2 h-4 w-2.5 rounded-b-full bg-(--keysort-filter-hole)" /> ) : ( <span className={cn("size-2.5 rounded-full", caught ? "bevel-metal" : "bg-(--keysort-filter-hole)")} /> )} </span> ) })} </li> ) } /** “3 of 12 cards match NDA and Signed.” Announced as needles go in and out. */ function KeysortFilterCount({ className, ...props }: React.ComponentProps<"p">) { const { needles, selected, cards } = useKeysortFilter() const all = Object.values(cards) const count = all.filter((n) => matches(n, selected)).length const names = selected.map((s) => needles.find((n) => n.value === s)?.label ?? s) const list = names.length > 1 ? `${names.slice(0, -1).join(", ")} and ${names.at(-1)}` : names[0] return ( <p role="status" data-slot="keysort-filter-count" className={cn("col-span-full text-sm text-muted-foreground", className)} {...props} > {selected.length === 0 ? `${all.length} cards. Push a needle in to sort.` : `${count} of ${all.length} cards match ${list}.`} </p> ) } export { KeysortFilter, KeysortFilterNeedles, KeysortFilterNeedle, KeysortFilterDeck, KeysortFilterCard, KeysortFilterCount } export type { KeysortFilterProps, KeysortFilterNeedleProps, KeysortFilterCardProps }Copy
keysort-filter.cssintoyour styles.styles/keysort-filter.css /* * Keysort filter: edge-notched cards. Each column of holes is an * attribute; a card with that attribute has its hole notched open. Push a * needle down a column and lift: notched cards fall free, the rest are * caught on the needle. Builds on bevel.css (paper, metal, motion). */ :root { --keysort-filter-card: oklch(0.93 0.04 85); /* manila */ --keysort-filter-card-held: oklch(0.86 0.02 85); /* a card left on the needle */ --keysort-filter-card-foreground: oklch(0.3 0.04 70); --keysort-filter-hole: oklch(0.3 0.02 70); --keysort-filter-match: oklch(0.58 0.15 150); --keysort-filter-needle: oklch(0.8 0.01 260); --keysort-filter-needle-edge: oklch(0.5 0.01 260); --keysort-filter-column: 2.5rem; /* one attribute's column */ } @media (width >= 40rem) { :root { --keysort-filter-column: 3.5rem; } } [data-slot="keysort-filter"] { --bevel-material: var(--keysort-filter-needle); --bevel-material-edge: var(--keysort-filter-needle-edge); }Import the stylesheets after Tailwind in your global CSS, foundation first.
globals.css @import "./bevel.css"; @import "./keysort-filter.css";
Anatomy
Each part is its own component. Leave out the ones you don't need.
<KeysortFilter aria-label="Contracts">
<KeysortFilterNeedles>
<KeysortFilterNeedle value="nda">NDA</KeysortFilterNeedle>
<KeysortFilterNeedle value="signed">Signed</KeysortFilterNeedle>
</KeysortFilterNeedles>
<KeysortFilterDeck>
<KeysortFilterCard notches={["nda", "signed"]}>Acme mutual NDA</KeysortFilterCard>
</KeysortFilterDeck>
<KeysortFilterCount />
</KeysortFilter>Parts
| Part | Renders | What it does |
|---|---|---|
| KeysortFilter | div (group) | value, defaultValue and onValueChange: the needles pushed through. Give it an aria-label. |
| KeysortFilterNeedles | div (group) | The needle handles, one above each column of holes. |
| KeysortFilterNeedle | Toggle | One attribute, and the column of holes under it, in the order listed. Pushed in, a rod runs down the column. |
| KeysortFilterDeck | ul | The cards, one per row, with the needles running down behind them. |
| KeysortFilterCard | li | notches: the attributes it has. Caught cards show the needle through their hole; free ones get a tab and say “Matches”. |
| KeysortFilterCount | p (status) | “2 of 8 cards match NDA and Signed.” Announced as needles go in and out. |