component — display
Grid List
Keyboard-navigable, selectable list built on react-aria-components and styled with the quebi design system. Supports single and multiple selection with row checkboxes, drag handles, sections, and labels/descriptions.
- list
- selection
- collection
- drag
- interactive
04 examples
Default
A single-selection list. Click or use the keyboard to select a row.
React
Solid
Svelte
Vue
Multiple selection
selectionMode="multiple" renders a checkbox on each row.
Read
Write
Administer
Label and description
Compose rows with GridListLabel and GridListDescription.
Ada Lovelaceada@quebi.de
Alan Turingalan@quebi.de
Disabled item
Disabled rows are dimmed and not selectable.
Free
Pro
Enterprise (coming soon)
source
grid-list.tsxCopy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.
"use client"
import type { GridListItemProps, GridListProps, TextProps } from "react-aria-components"
import {
Button,
composeRenderProps,
GridListHeader as GridListHeaderPrimitive,
GridListItem as GridListItemPrimitive,
GridList as GridListPrimitive,
GridListSection as GridListSectionPrimitive,
Text,
} from "react-aria-components"
import { Checkbox } from "@/components/checkbox"
import { cn } from "@/lib/utils"
/**
* GridList — quebi design system
*
* Built on react-aria-components. A keyboard-navigable, selectable list with
* optional drag handles and per-row checkboxes, drawn as the index list: a
* strong rule on top, a hairline under every row, no boxes. A hovered row is
* raised, a selected one takes the `selected` ground with its icon in signal;
* a row with an `href` also shifts right on hover. A drop target is outlined
* in signal. Focus is an inset ring, since the rows have no room around them.
*/
const GridList = <T extends object>({ className, ...props }: GridListProps<T>) => (
<GridListPrimitive
data-slot="grid-list"
className={composeRenderProps(className, (resolved) =>
cn(
"relative flex flex-col sm:text-sm/6",
"border-t border-quebi-rule has-data-[slot=grid-list-section]:border-t-0",
"*:data-[drop-target]:border *:data-[drop-target]:border-quebi-signal",
"has-data-[slot=grid-list-section]:gap-y-8",
resolved,
),
)}
{...props}
/>
)
const GridListSection = <T extends object>({
className,
...props
}: React.ComponentProps<typeof GridListSectionPrimitive<T>>) => {
return (
<GridListSectionPrimitive
data-slot="grid-list-section"
className={cn("flex flex-col", className)}
{...props}
/>
)
}
const GridListHeader = ({
className,
...props
}: React.ComponentProps<typeof GridListHeaderPrimitive>) => {
return (
<GridListHeaderPrimitive
data-slot="grid-list-header"
className={cn("quebi-eyebrow border-b border-quebi-rule pb-2", className)}
{...props}
/>
)
}
const GridListItem = ({ className, children, ...props }: GridListItemProps) => {
const textValue = typeof children === "string" ? children : undefined
return (
<GridListItemPrimitive
textValue={textValue}
{...props}
className={composeRenderProps(
className,
(className, { isHovered, isFocusVisible, isSelected, isDisabled }) =>
cn(
"group relative min-w-0 outline-hidden",
"border-b border-quebi-hairline px-1 py-3.5",
"flex min-w-0 cursor-default items-center gap-2 sm:gap-2.5",
"text-quebi-fg transition-[padding,background-color] duration-300",
"data-[dragging]:cursor-grab data-[dragging]:opacity-70",
"**:data-[slot=icon]:size-5 **:data-[slot=icon]:shrink-0 **:data-[slot=icon]:text-quebi-fg-muted sm:**:data-[slot=icon]:size-4",
(isHovered || isFocusVisible) && "bg-quebi-raised",
isSelected &&
"bg-quebi-selected text-quebi-on-selected **:data-[slot=icon]:text-quebi-signal",
isFocusVisible && "ring-2 ring-inset ring-quebi-focus",
isDisabled && "opacity-50",
"href" in props && "cursor-pointer",
"href" in props && isHovered && "pl-3",
className,
),
)}
>
{(values) => (
<>
{values.allowsDragging && (
<Button slot="drag" className="text-quebi-fg-muted outline-hidden">
<svg
aria-hidden="true"
data-slot="drag-icon"
className="size-5 sm:size-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
>
<path
d="M11 5.5C11 6.32843 10.3284 7 9.5 7C8.67157 7 8 6.32843 8 5.5C8 4.67157 8.67157 4 9.5 4C10.3284 4 11 4.67157 11 5.5Z"
fill="currentColor"
/>
<path
d="M16 5.5C16 6.32843 15.3284 7 14.5 7C13.6716 7 13 6.32843 13 5.5C13 4.67157 13.6716 4 14.5 4C15.3284 4 16 4.67157 16 5.5Z"
fill="currentColor"
/>
<path
d="M11 18.5C11 19.3284 10.3284 20 9.5 20C8.67157 20 8 19.3284 8 18.5C8 17.6716 8.67157 17 9.5 17C10.3284 17 11 17.6716 11 18.5Z"
fill="currentColor"
/>
<path
d="M16 18.5C16 19.3284 15.3284 20 14.5 20C13.6716 20 13 19.3284 13 18.5C13 17.6716 13.6716 17 14.5 17C15.3284 17 16 17.6716 16 18.5Z"
fill="currentColor"
/>
<path
d="M11 12C11 12.8284 10.3284 13.5 9.5 13.5C8.67157 13.5 8 12.8284 8 12C8 11.1716 8.67157 10.5 9.5 10.5C10.3284 10.5 11 11.1716 11 12Z"
fill="currentColor"
/>
<path
d="M16 12C16 12.8284 15.3284 13.5 14.5 13.5C13.6716 13.5 13 12.8284 13 12C13 11.1716 13.6716 10.5 14.5 10.5C15.3284 10.5 16 11.1716 16 12Z"
fill="currentColor"
/>
</svg>
</Button>
)}
{values.selectionMode === "multiple" && values.selectionBehavior === "toggle" && (
<Checkbox slot="selection" />
)}
{typeof children === "function" ? children(values) : children}
</>
)}
</GridListItemPrimitive>
)
}
const GridListEmptyState = ({ ref, className, ...props }: React.ComponentProps<"div">) => (
<div ref={ref} className={cn("p-6 text-quebi-fg-muted", className)} {...props} />
)
const GridListSpacer = ({ className, ref, ...props }: React.ComponentProps<"div">) => {
return <div ref={ref} aria-hidden className={cn("-ms-4 flex-1", className)} {...props} />
}
const GridListStart = ({ className, ref, ...props }: React.ComponentProps<"div">) => {
return (
<div
ref={ref}
className={cn("relative flex items-center gap-x-2.5 sm:gap-x-3", className)}
{...props}
/>
)
}
interface GridListTextProps extends TextProps {
ref?: React.Ref<HTMLDivElement>
}
const GridListLabel = ({ className, ref, ...props }: GridListTextProps) => (
<Text
ref={ref}
className={cn("font-display text-lg/6 font-light tracking-wide text-quebi-fg", className)}
{...props}
/>
)
const GridListDescription = ({ className, ref, ...props }: GridListTextProps) => (
<Text
slot="description"
ref={ref}
className={cn("text-quebi-body-s text-quebi-fg-muted", className)}
{...props}
/>
)
export type { GridListItemProps, GridListProps }
export {
GridList,
GridListDescription,
GridListEmptyState,
GridListHeader,
GridListItem,
GridListLabel,
GridListSection,
GridListSpacer,
GridListStart,
}