List
Filas separadas por líneas finas alineadas al texto, con un valor final en dígitos tabulares.
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
Instalación
$ pnpm dlx shadcn@latest add @ssntl/listUso
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>- Separadores de sección: una línea de 1px con el token border entre filas, así una lista de filas parecidas se lee como elementos separados.
- La línea empieza donde empieza el texto (56px con media), y los avatares quedan en una columna limpia.
- Los valores se alinean a la derecha con dígitos tabulares y la misma precisión.
- Los nombres largos se truncan en una línea; el valor completo va en la vista de detalle.
Código
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,
}