Listbox
A list of options with arrow-key selection.
Preview
Loading...
import { Listbox } from "@/components/ui/Listbox";
export function ListboxDemo() {
return (
<Listbox
defaultValue="type"
items={[
{ value: "color", label: "Color" },
{ value: "type", label: "Typography" },
{ value: "space", label: "Spacing" },
]}
/>
);
}Installation
Make sure that namespace is set in your component.json file. Namespace docs: Learn more about namespaces
pnpm dlx shadcn@latest add @aura/listboxManual
Copy and paste the class names utility into your utils/class-names.ts file.
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Copy and paste the Listbox component into your components/ui/Listbox.tsx file.
"use client";
import * as React from "react";
import { cn } from "@/utils/class-names";
type ListboxItem = {
value: string;
label: string;
disabled?: boolean;
};
function Listbox({
className,
items,
value,
defaultValue,
onValueChange,
label = "Options",
}: {
className?: string;
items: ListboxItem[];
value?: string;
defaultValue?: string;
onValueChange?: (value: string) => void;
label?: string;
}) {
const [uncontrolled, setUncontrolled] = React.useState(defaultValue ?? items[0]?.value);
const current = value ?? uncontrolled;
const enabled = items.filter((item) => !item.disabled);
const select = (next: string) => {
if (value === undefined) setUncontrolled(next);
onValueChange?.(next);
};
return (
<div
role="listbox"
aria-label={label}
tabIndex={0}
data-slot="listbox"
className={cn(
"flex w-full max-w-xl flex-col rounded-sm border border-gray-6 bg-gray-1 p-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-8",
className,
)}
onKeyDown={(event) => {
const index = enabled.findIndex((item) => item.value === current);
if (event.key === "ArrowDown") {
event.preventDefault();
const next = enabled[(index + 1) % enabled.length];
if (next) select(next.value);
} else if (event.key === "ArrowUp") {
event.preventDefault();
const next = enabled[(index - 1 + enabled.length) % enabled.length];
if (next) select(next.value);
}
}}
>
{items.map((item) => {
const selected = item.value === current;
return (
<button
key={item.value}
type="button"
role="option"
aria-selected={selected}
disabled={item.disabled}
className="rounded-sm px-1 py-0.5 text-start text-gray-12 hover:bg-gray-3 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-8 disabled:opacity-50 aria-selected:bg-accent-3"
onClick={() => select(item.value)}
>
{item.label}
</button>
);
})}
</div>
);
}
export { Listbox };
export type { ListboxItem };