List
Rows split by hairlines inset to the text, with a tabular trailing value.
Top customers
Ana López
Studio Norte
Q 4,820.00
Marco Ruiz
Café Lumen
Q 1,265.50
Sofía García
sofia@ferreteria.gt
Q 312.00
Diego Castillo
Casa Barrios
Q 98.75
Installation
$ pnpm dlx shadcn@latest add @ssntl/listUsage
import { List, ListItem, ListItemContent, ListItemMedia, ListItemMeta, ListItemTitle, ListItemValue } from "@/components/ui/list"
<List>
<ListItem>
<ListItemMedia aria-hidden>AL</ListItemMedia>
<ListItemContent>
<ListItemTitle>Ana López</ListItemTitle>
<ListItemMeta>Studio Norte</ListItemMeta>
</ListItemContent>
<ListItemValue>Q 4,820.00</ListItemValue>
</ListItem>
</List>- Section separators: a 1px border-token line between rows, so a list of similar rows reads as separate items.
- The line starts where the text does (56px with media), keeping the avatars in one clean column.
- Values align right with tabular figures and the same precision.
- Long names truncate on one line; put the full value in the detail view.
Code
import * as React from "react"
import { cn } from "@/lib/utils"
// A list of rows split by hairlines. The divider starts where the text does,
// past the media, so rows read as one block and the faces or icons stay in
// one clean column. Dividers are structure, the one place a border belongs.
function List({ className, ...props }: React.ComponentProps<"ul">) {
return <ul data-slot="list" className={cn("grid", className)} {...props} />
}
function ListItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="list-item"
className={cn(
"relative flex min-h-16 items-center gap-4 py-3",
// Every row after the first draws a 1px line on top. With media it
// starts at 56px (40 media + 16 gap), where the text starts.
"not-first:before:absolute not-first:before:inset-x-0 not-first:before:top-0 not-first:before:h-px not-first:before:bg-border has-data-[slot=list-item-media]:not-first:before:start-14",
className
)}
{...props}
/>
)
}
/** A 40px avatar or icon well; decorative unless it carries meaning. */
function ListItemMedia({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="list-item-media"
className={cn(
"grid size-10 shrink-0 place-items-center overflow-hidden rounded-full bg-secondary text-caption font-medium text-foreground [&_svg]:size-5",
className
)}
{...props}
/>
)
}
function ListItemContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="list-item-content"
className={cn("grid min-w-0 flex-1 gap-0.5", className)}
{...props}
/>
)
}
function ListItemTitle({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="list-item-title"
className={cn("truncate text-label", className)}
{...props}
/>
)
}
function ListItemMeta({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="list-item-meta"
className={cn("truncate text-caption text-muted-foreground", className)}
{...props}
/>
)
}
/** A trailing number: right-aligned with tabular figures, so values line up. */
function ListItemValue({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="list-item-value"
className={cn("shrink-0 text-end text-label tabular-nums", className)}
{...props}
/>
)
}
export {
List,
ListItem,
ListItemContent,
ListItemMedia,
ListItemMeta,
ListItemTitle,
ListItemValue,
}