Fiber UI LogoFiberUI

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.

React
Next.js
Vue
Svelte
Angular
"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.

React
Next.js
Vue
Svelte
Angular

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.

TypeScript
React
Next.js
Tailwind CSS
Prisma
tRPC

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.

FreeFor personal projects
ProFor professional developers
TeamFor small teams
EnterpriseComing soon
CustomComing soon
"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.

srcDIR
README.md2.1 KB
package.json1.4 KB
tsconfig.json0.8 KB
"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&apos;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>
    );
};