Select
A card-catalogue drawer of options.
Install
$ npx shadcn@latest add https://ephemera.kerrshift.com/r/select.jsonOr copy the source below into components/ui/select.tsx. Core components need the Paper Theme and Radix where noted.
Usage
example.tsx
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SelectGroup, SelectLabel, SelectSeparator } from "@/components/ui/select";
<Select defaultValue="kraft">
<SelectTrigger>
<SelectValue placeholder="Choose paper" />
</SelectTrigger>
<SelectContent>
<SelectItem value="cream">Cream</SelectItem>
<SelectItem value="kraft">Kraft</SelectItem>
<SelectItem value="ledger">Ledger</SelectItem>
</SelectContent>
</Select>Built on the Radix Select, so keyboard navigation and screen reader support come for free. The menu opens inside the nearest data-paper container so it keeps its theme.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value / defaultValue / onValueChange | string | — | Selected value. |
| SelectContent position | "popper" | "item-aligned" | "popper" | Drop below the trigger, or align over the selected item like a native select. |
| SelectItem value / disabled | string / boolean | — | One drawer 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/select.tsx
"use client";
import * as React from "react";
import { Select as SelectPrimitive } from "radix-ui";
const type = { fontFamily: "var(--font-type, 'Courier New', ui-monospace, monospace)" };
// Portal into the nearest data-paper container so the menu keeps its paper theme.
function usePaperContainer() {
const anchor = React.useRef<HTMLSpanElement>(null);
const [container, setContainer] = React.useState<HTMLElement | undefined>();
React.useEffect(() => {
const el = anchor.current?.closest<HTMLElement>("[data-paper]");
setContainer(el && el !== document.documentElement ? el : undefined);
}, []);
return [anchor, container] as const;
}
/** A card-catalogue drawer of options. */
export const Select = SelectPrimitive.Root;
export const SelectGroup = SelectPrimitive.Group;
export const SelectValue = SelectPrimitive.Value;
export function SelectTrigger({ className = "", style, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Trigger>) {
return (
<SelectPrimitive.Trigger
className={`group flex h-10 w-full cursor-pointer items-center justify-between gap-2 border-0 border-b-[1.5px] border-(--rule) bg-transparent px-0.5 pb-1 pt-2 text-left text-[15px] text-(--ink) outline-none transition-colors focus-visible:border-(--pen) disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-(--danger) data-[placeholder]:text-(--ink-faint) data-[state=open]:border-(--pen) [&>span]:truncate ${className}`}
style={{ ...type, ...style }}
{...props}
>
{children}
{/* no asChild inside registry files: the shadcn CLI rewrites it to `render` in Base UI projects */}
<SelectPrimitive.Icon className="shrink-0 text-[12px] text-(--ink-muted) transition-transform group-data-[state=open]:rotate-180">
▼
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
export function SelectContent({
className = "",
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
const [anchor, container] = usePaperContainer();
return (
<>
<span ref={anchor} hidden />
<SelectPrimitive.Portal container={container}>
<SelectPrimitive.Content
position={position}
sideOffset={6}
data-paper-motion
className={`paper relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] overflow-y-auto rounded-[3px] border border-(--edge) bg-(--paper-raised) text-(--ink) shadow-[0_14px_30px_-12px_rgb(0_0_0/.45)] data-[state=open]:animate-[paper-pop-in_.16s_ease-out] ${
position === "popper" ? "w-(--radix-select-trigger-width)" : ""
} ${className}`}
{...props}
>
<SelectPrimitive.Viewport className="p-1">{children}</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
</>
);
}
export function SelectItem({ className = "", style, children, ...props }: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
className={`relative flex w-full cursor-pointer select-none items-center border-b border-dashed border-(--edge) py-2 pl-8 pr-3 text-[14px] outline-none last:border-b-0 data-[disabled]:pointer-events-none data-[highlighted]:bg-(--paper-sunk) data-[disabled]:opacity-45 ${className}`}
style={{ ...type, ...style }}
{...props}
>
<span className="absolute left-2 flex size-4 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<svg viewBox="0 0 26 26" className="size-4 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="3" strokeLinecap="round" />
</svg>
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
export function SelectLabel({ className = "", style, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
className={`px-3 pb-1 pt-2.5 text-[10px] font-bold uppercase tracking-[0.2em] text-(--ink-muted) ${className}`}
style={{ ...type, ...style }}
{...props}
/>
);
}
export function SelectSeparator({ className = "", ...props }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return <SelectPrimitive.Separator className={`-mx-1 my-1 h-px bg-(--margin) ${className}`} {...props} />;
}