Lists
Lists are continuous, vertical indexes of text and images
Usage
Cards are used to display content and actions on a single topic.
Instalation
Run the following command if is not already installed:
npm i @radix-ui/react-slot
Copy and paste the following code into your project.
import * as React from 'react'
import { Slot, Slottable } from '@radix-ui/react-slot'
import { cn } from '@/lib/utils'
const ListRoot = React.forwardRef<
HTMLUListElement,
React.HTMLAttributes<HTMLUListElement> & { asChild?: boolean }
>(({ className, asChild, ...props }, ref) => {
const Comp = asChild ? Slot : 'ul'
return (
<Comp
ref={ref}
className={cn('w-full bg-surface py-2', className)}
{...props}
/>
)
})
ListRoot.displayName = 'ListRoot'
const ListItem = React.forwardRef<
HTMLLIElement,
React.HTMLAttributes<HTMLLIElement> & { asChild?: boolean }
>(({ className, asChild, ...props }, ref) => {
const Comp = asChild ? Slot : 'li'
return (
<Comp
ref={ref}
className={cn(
'group/item relative flex min-h-14 w-full items-center gap-4 bg-surface px-4 py-2 text-onSurfaceVariant outline-none last:border-none focus:outline-none',
className
)}
{...props}
/>
)
})
ListItem.displayName = 'ListItem'
const ListActionArea = React.forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement> & { asChild?: boolean }
>(({ className, children, asChild, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<>
<Comp
ref={ref}
className={cn(
'group/action z-0 flex h-full w-full items-center gap-4 text-onSurfaceVariant outline-none focus:outline-none',
className
)}
{...props}
>
<Slottable>{children}</Slottable>
<span className="state-layer absolute inset-0 z-[-1] bg-onSurfaceVariant opacity-0 transition-opacity group-focus-within/item:opacity-4 group-hover/item:opacity-4 group-active/action:opacity-8" />
</Comp>
</>
)
})
ListActionArea.displayName = 'ListActionArea'
const ListHeadline = React.forwardRef<
HTMLHeadingElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn('text-body-lg text-onSurface', className)}
{...props}
/>
))
ListHeadline.displayName = 'ListHeadline'
const ListSupportingText = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p ref={ref} className={cn('text-body-md', className)} {...props} />
))
ListSupportingText.displayName = 'ListSupportingText'
const ListEdgeSection = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex shrink-0 items-center', className)}
{...props}
/>
))
const ListContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('grow', className)} {...props} />
))
const List = Object.assign(ListRoot, {
Item: ListItem,
Headline: ListHeadline,
SupportingText: ListSupportingText,
ActionArea: ListActionArea,
EdgeSection: ListEdgeSection,
Content: ListContent,
})
export {
List,
ListRoot,
ListItem,
ListHeadline,
ListSupportingText,
ListActionArea,
ListEdgeSection,
ListContent,
}
Update the import paths to match your project setup.
Examples
Thumbnail and trailing supporting text
Cactus
In Stock
$9.99
Succulent
In Stock
$19.99
Bamboo
In Stock
$29.99
Orchid
Out of Stock
$39.99
Bonsai
In Stock
$49.99
import { List } from '@/components/ui/list'
import { mockStore } from './data'
export const ListWithThumbnailAndTrailingSupportingText = () => {
return (
<List>
{mockStore.map(({ sku, name, stock, price, thumbnail }) => (
<List.Item key={sku}>
<img
src={thumbnail}
alt={name}
className="bg-outlineVariant size-14 shrink-0"
/>
<div className="grow">
<List.Headline>{name}</List.Headline>
<List.SupportingText>
{stock ? 'In Stock' : 'Out of Stock'}
</List.SupportingText>
</div>
<p className="text-label-lg">${price}</p>
</List.Item>
))}
</List>
)
}
Action area
import { Icon } from '@/components/ui/icon'
import { IconButton } from '@/components/ui/icon-button'
import { List } from '@/components/ui/list'
import { mockFolders } from './data'
export const LinksListExample = () => {
return (
<List asChild>
<nav>
{mockFolders.map(({ url, name, updated, icon }) => (
<List.Item asChild key={url}>
<div>
<List.ActionArea asChild>
<a href={url}>
<div className="bg-primaryContainer text-onPrimaryContainer grid size-10 shrink-0 place-items-center rounded-full">
<Icon symbol={icon} />
</div>
<div className="grow">
<List.Headline>{name}</List.Headline>
<List.SupportingText>{`updated ${updated}`}</List.SupportingText>
</div>
</a>
</List.ActionArea>
<IconButton
variant="standard"
onClick={() => console.log({ name })}
>
<Icon symbol="info" />
</IconButton>
</div>
</List.Item>
))}
</nav>
</List>
)
}
With dividers
Place a divider between rows with lots of content, such as those with two- or three-line lists
Festivals
Festivals Food, music, arts, community, tradition, culture, and celebration - a collection of events around the world.
May 8
Arts
Literature, games, music, physical and visual forms - explore the art that has inspired and moved humanity through history and today.
May 7
Family & Friends
The relationships that bring and bind us together and shape our world.
May 4
import { List } from '@/components/ui/list'
import { mockCategories } from './data'
export const DoubleLineListExample = () => {
return (
<List className="max-w-md">
{mockCategories.map(({ thumbnail, name, description, date }) => (
<List.Item key={name} className="border-outlineVariant border-b">
<img
src={thumbnail}
alt={name}
className="bg-outlineVariant size-14 shrink-0 rounded-md object-cover"
/>
<div className="grow">
<List.Headline>{name}</List.Headline>
<List.SupportingText className="line-clamp-2">
{description}
</List.SupportingText>
</div>
<p className="text-label-sm shrink-0 self-start">{date}</p>
</List.Item>
))}
</List>
)
}
Sortable
The drag-and-drop sortable functionality utilizes the @dnd-kit
library. To use this feature, you will have to follow these additional steps:
Run the following command:
npm i @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
Copy and paste the following code into your project.
'use client'
import { createContext, Fragment, useContext, useMemo, useState } from 'react'
import {
defaultDropAnimationSideEffects,
DndContext,
DragOverlay,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
type Active,
type DraggableSyntheticListeners,
type DragOverlayProps,
type DropAnimation,
type UniqueIdentifier,
} from '@dnd-kit/core'
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
useSortable,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { cn } from '@/lib/utils'
import { List, ListItem } from '@/components/ui/list'
interface Context {
attributes: Record<string, any>
listeners: DraggableSyntheticListeners
ref(node: HTMLElement | null): void
}
interface BaseItemProps {
id: UniqueIdentifier
}
interface SortableListProps<T extends BaseItemProps> {
items: T[]
onChange(items: T[]): void
renderItem(item: T): React.ReactNode
}
const SortableItemContext = createContext<Context>({
attributes: {},
listeners: undefined,
ref() {},
})
const dropAnimationConfig: DropAnimation = {
sideEffects: defaultDropAnimationSideEffects({
styles: {
active: {
opacity: '0.2',
},
},
}),
}
export function SortableOverlay(props: DragOverlayProps) {
return <DragOverlay dropAnimation={dropAnimationConfig} {...props} />
}
const SortableListRoot = <T extends BaseItemProps>({
items,
onChange,
renderItem,
}: SortableListProps<T>) => {
const [active, setActive] = useState<Active | null>(null)
const activeItem = useMemo(
() => items.find((item) => item.id === active?.id),
[active, items]
)
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
)
return (
<DndContext
sensors={sensors}
onDragStart={({ active }) => {
setActive(active)
}}
onDragEnd={({ active, over }) => {
if (over && active.id !== over?.id) {
const activeIndex = items.findIndex(({ id }) => id === active.id)
const overIndex = items.findIndex(({ id }) => id === over.id)
onChange(arrayMove(items, activeIndex, overIndex))
}
setActive(null)
}}
onDragCancel={() => {
setActive(null)
}}
>
<SortableContext items={items}>
<List>
{items.map((item) => (
<Fragment key={item.id}>{renderItem(item)}</Fragment>
))}
</List>
</SortableContext>
<SortableOverlay>
{activeItem ? renderItem(activeItem) : null}
</SortableOverlay>
</DndContext>
)
}
interface SorteableItemProps
extends BaseItemProps,
Omit<React.ComponentPropsWithoutRef<typeof ListItem>, 'id'> {}
const SortableItem = ({ className, id, ...props }: SorteableItemProps) => {
const {
attributes,
isDragging,
listeners,
setNodeRef,
setActivatorNodeRef,
transform,
transition,
} = useSortable({ id })
const context = useMemo(
() => ({
attributes,
listeners,
ref: setActivatorNodeRef,
}),
[attributes, listeners, setActivatorNodeRef]
)
const style: React.CSSProperties = {
opacity: isDragging ? 0.2 : undefined,
transform: CSS.Translate.toString(transform),
transition,
}
return (
<SortableItemContext.Provider value={context}>
<ListItem
ref={setNodeRef}
className={className}
style={style}
{...props}
/>
</SortableItemContext.Provider>
)
}
SortableItem.displayName = 'SortableItem'
const SortableHandle = (props: React.ComponentPropsWithoutRef<'button'>) => {
const { attributes, listeners, ref } = useContext(SortableItemContext)
return (
<button
ref={ref}
className={cn('shrink-0 cursor-move', props.className)}
{...attributes}
{...listeners}
{...props}
/>
)
}
const SortableList = Object.assign(SortableListRoot, {
Item: SortableItem,
Handle: SortableHandle,
})
export { SortableList, SortableListRoot, SortableItem, SortableHandle }
Update the import paths to match your project setup.
import { useState } from 'react'
import { Icon } from '@/components/ui/icon'
import { List } from '@/components/ui/list'
import { SortableList } from '@/components/ui/sortable-list'
import { mockDogs } from './data'
export const SortableListExample = () => {
const [items, setItems] = useState(mockDogs)
return (
<SortableList
items={items}
onChange={setItems}
renderItem={({ id, headline, supportingText }) => (
<SortableList.Item key={id} id={id}>
<div className="bg-primaryContainer text-onPrimaryContainer grid size-10 shrink-0 place-items-center rounded-full">
{headline.slice(0, 1).toLocaleUpperCase()}
</div>
<div className="grow">
<List.Headline>{headline}</List.Headline>
<List.SupportingText className="line-clamp-1">
{supportingText}
</List.SupportingText>
</div>
<SortableList.Handle>
<Icon symbol="drag_handle" />
</SortableList.Handle>
</SortableList.Item>
)}
/>
)
}