GridList
An interactive list component that supports keyboard navigation, single and multiple selection, disabled items, and custom content rendering. Built on React Aria for full accessibility.
The GridList component displays a list of interactive items with support for keyboard navigation and selection. Built on React Aria's GridList primitive with automatic checkbox/radio rendering based on selection mode.
Basic Usage
A simple interactive list with keyboard navigation. Items can be focused and navigated with arrow keys.
"use client";
import { GridList, GridListItem } from "@repo/ui/components/grid-list";
/* BASIC USAGE EXAMPLE */
export const Example1 = () => {
return (
<GridList aria-label="Favorite frameworks" className="w-[300px]">
<GridListItem>React</GridListItem>
<GridListItem>Next.js</GridListItem>
<GridListItem>Vue</GridListItem>
<GridListItem>Svelte</GridListItem>
<GridListItem>Angular</GridListItem>
</GridList>
);
};
Single Selection
Set selectionMode="single" to allow selecting one item at a time. A radio indicator appears automatically.
Selected: react
"use client";
import { useState } from "react";
import { GridList, GridListItem } from "@repo/ui/components/grid-list";
import type { Selection } from "react-aria-components";
/* SINGLE SELECTION EXAMPLE */
export const Example2 = () => {
const [selected, setSelected] = useState<Selection>(new Set(["react"]));
return (
<div className="space-y-3">
<GridList
aria-label="Select a framework"
selectionMode="single"
selectedKeys={selected}
onSelectionChange={setSelected}
className="w-[300px]"
>
<GridListItem id="react">React</GridListItem>
<GridListItem id="nextjs">Next.js</GridListItem>
<GridListItem id="vue">Vue</GridListItem>
<GridListItem id="svelte">Svelte</GridListItem>
<GridListItem id="angular">Angular</GridListItem>
</GridList>
<p className="text-muted-foreground text-sm">
Selected:{" "}
<span className="text-foreground font-medium">
{[...selected].join(", ") || "None"}
</span>
</p>
</div>
);
};
Multiple Selection
Set selectionMode="multiple" for multi-select with checkboxes. Hold Shift to select a range.
Selected (2): ts, react
"use client";
import { useState } from "react";
import { GridList, GridListItem } from "@repo/ui/components/grid-list";
import type { Selection } from "react-aria-components";
/* MULTIPLE SELECTION EXAMPLE */
export const Example3 = () => {
const [selected, setSelected] = useState<Selection>(
new Set(["ts", "react"]),
);
return (
<div className="space-y-3">
<GridList
aria-label="Select technologies"
selectionMode="multiple"
selectedKeys={selected}
onSelectionChange={setSelected}
className="w-[300px]"
>
<GridListItem id="ts">TypeScript</GridListItem>
<GridListItem id="react">React</GridListItem>
<GridListItem id="nextjs">Next.js</GridListItem>
<GridListItem id="tailwind">Tailwind CSS</GridListItem>
<GridListItem id="prisma">Prisma</GridListItem>
<GridListItem id="trpc">tRPC</GridListItem>
</GridList>
<p className="text-muted-foreground text-sm">
Selected ({selected === "all" ? "all" : selected.size}):{" "}
<span className="text-foreground font-medium">
{selected === "all"
? "All"
: [...selected].join(", ") || "None"}
</span>
</p>
</div>
);
};
Disabled Items
Use the disabledKeys prop to disable specific items. Disabled items cannot be selected or focused.
"use client";
import { GridList, GridListItem } from "@repo/ui/components/grid-list";
/* DISABLED ITEMS EXAMPLE */
export const Example4 = () => {
return (
<GridList
aria-label="Select a plan"
selectionMode="single"
disabledKeys={["enterprise", "custom"]}
className="w-[300px]"
>
<GridListItem id="free">
<div className="flex flex-col">
<span className="font-medium">Free</span>
<span className="text-muted-foreground text-xs">
For personal projects
</span>
</div>
</GridListItem>
<GridListItem id="pro">
<div className="flex flex-col">
<span className="font-medium">Pro</span>
<span className="text-muted-foreground text-xs">
For professional developers
</span>
</div>
</GridListItem>
<GridListItem id="team">
<div className="flex flex-col">
<span className="font-medium">Team</span>
<span className="text-muted-foreground text-xs">
For small teams
</span>
</div>
</GridListItem>
<GridListItem id="enterprise">
<div className="flex flex-col">
<span className="font-medium">Enterprise</span>
<span className="text-muted-foreground text-xs">
Coming soon
</span>
</div>
</GridListItem>
<GridListItem id="custom">
<div className="flex flex-col">
<span className="font-medium">Custom</span>
<span className="text-muted-foreground text-xs">
Coming soon
</span>
</div>
</GridListItem>
</GridList>
);
};
Custom Content
Render rich content inside each item — icons, badges, descriptions, or any layout.
"use client";
import {
FolderIcon,
ImageIcon,
FileTextIcon,
FileCodeIcon,
} from "lucide-react";
import { GridList, GridListItem } from "@repo/ui/components/grid-list";
/* CUSTOM CONTENT EXAMPLE */
export const Example5 = () => {
return (
<GridList
aria-label="Project files"
selectionMode="multiple"
className="w-[320px]"
>
<GridListItem id="src" textValue="src">
<FolderIcon className="size-4 text-blue-500" />
<div className="flex flex-1 items-center justify-between">
<span className="font-medium">src</span>
<span className="bg-muted text-muted-foreground rounded px-1.5 py-0.5 text-[10px] font-medium">
DIR
</span>
</div>
</GridListItem>
<GridListItem id="readme" textValue="README.md">
<FileTextIcon className="text-muted-foreground size-4" />
<div className="flex flex-1 items-center justify-between">
<span>README.md</span>
<span className="text-muted-foreground text-xs">
2.1 KB
</span>
</div>
</GridListItem>
<GridListItem id="package" textValue="package.json">
<FileCodeIcon className="size-4 text-yellow-500" />
<div className="flex flex-1 items-center justify-between">
<span>package.json</span>
<span className="text-muted-foreground text-xs">
1.4 KB
</span>
</div>
</GridListItem>
<GridListItem id="tsconfig" textValue="tsconfig.json">
<FileCodeIcon className="size-4 text-blue-400" />
<div className="flex flex-1 items-center justify-between">
<span>tsconfig.json</span>
<span className="text-muted-foreground text-xs">
0.8 KB
</span>
</div>
</GridListItem>
<GridListItem id="logo" textValue="logo.png">
<ImageIcon className="size-4 text-green-500" />
<div className="flex flex-1 items-center justify-between">
<span>logo.png</span>
<span className="text-muted-foreground text-xs">24 KB</span>
</div>
</GridListItem>
</GridList>
);
};
Empty State
Use renderEmptyState to display a custom message when the list has no items.
No notifications
You're all caught up! Check back later.
"use client";
import { InboxIcon } from "lucide-react";
import { GridList } from "@repo/ui/components/grid-list";
/* EMPTY STATE EXAMPLE */
export const Example6 = () => {
return (
<GridList
aria-label="Notifications"
selectionMode="multiple"
renderEmptyState={() => (
<div className="flex flex-col items-center justify-center gap-2 py-8 text-center">
<InboxIcon className="text-muted-foreground/40 size-10" />
<div>
<p className="text-foreground text-sm font-medium">
No notifications
</p>
<p className="text-muted-foreground text-xs">
You're all caught up! Check back later.
</p>
</div>
</div>
)}
className="w-[300px]"
>
{[]}
</GridList>
);
};
Component Code
"use client";
import * as React from "react";
import { CheckIcon } from "lucide-react";
import {
GridList as AriaGridList,
GridListItem as AriaGridListItem,
composeRenderProps,
type GridListProps as AriaGridListProps,
type GridListItemProps as AriaGridListItemProps,
Checkbox as AriaCheckbox,
} from "react-aria-components";
import { cn, tv } from "tailwind-variants";
import { focusRing } from "@repo/ui/lib/utils";
/* -----------------------------------------------------------------------------
* GridList (Root)
* ---------------------------------------------------------------------------*/
const gridListStyles = tv({
base: "group/grid-list relative overflow-auto rounded-lg border",
});
interface GridListProps<T extends object> extends AriaGridListProps<T> {}
export const GridList = <T extends object>({
className,
children,
...props
}: GridListProps<T>) => {
return (
<AriaGridList
data-slot="grid-list"
className={cn(gridListStyles(), className)}
{...props}
>
{children}
</AriaGridList>
);
};
/* -----------------------------------------------------------------------------
* GridListItem
* ---------------------------------------------------------------------------*/
const gridListItemStyles = tv({
extend: focusRing,
base: [
"relative flex cursor-default gap-3 border-b px-3 py-2 text-sm outline-none",
"last:border-b-0",
"-outline-offset-2",
],
variants: {
isSelected: {
false: "hover:bg-accent/50 dark:hover:bg-accent/30",
true: "bg-primary/10 dark:bg-primary/20 z-20",
},
isDisabled: {
true: "text-muted-foreground/50 cursor-not-allowed opacity-50",
},
isFocusVisible: {
true: "outline-ring outline-2",
false: "outline-0",
},
},
});
interface GridListItemProps extends AriaGridListItemProps {}
export const GridListItem = ({
className,
children,
...props
}: GridListItemProps) => {
const textValue =
props.textValue ||
(typeof children === "string" ? children : undefined);
return (
<AriaGridListItem
data-slot="grid-list-item"
textValue={textValue}
className={composeRenderProps(
className,
(className, renderProps) =>
cn(gridListItemStyles({ ...renderProps }), className) || "",
)}
{...props}
>
{composeRenderProps(children, (children, { selectionMode }) => (
<>
{selectionMode === "multiple" && (
<AriaCheckbox
slot="selection"
className="flex items-center"
>
{({ isSelected }) => (
<div
className={cn(
"flex size-4 shrink-0 items-center justify-center rounded border transition-colors",
isSelected
? "border-primary bg-primary text-primary-foreground"
: "border-input",
)}
>
{isSelected && (
<CheckIcon className="size-3" />
)}
</div>
)}
</AriaCheckbox>
)}
{selectionMode === "single" && (
<AriaCheckbox
slot="selection"
className="flex items-center"
>
{({ isSelected }) => (
<div
className={cn(
"flex size-4 shrink-0 items-center justify-center rounded-full border transition-colors",
isSelected
? "border-primary bg-primary"
: "border-input",
)}
>
{isSelected && (
<div className="bg-primary-foreground size-2 rounded-full" />
)}
</div>
)}
</AriaCheckbox>
)}
<div className="flex flex-1 items-center gap-2">
{children}
</div>
</>
))}
</AriaGridListItem>
);
};
Dialog
A modal dialog overlay that blocks interaction with the rest of the page. Supports focus trapping, keyboard dismiss, accessibility, multiple sizes, and composable header/body/footer layout.
Input
A highly versatile input component for text fields, search boxes, and forms. Includes support for prefixes, suffixes, validation states, and accessible labels.