Scroll area
Listas acotadas dentro de un panel. Scrollbar fina, nunca doble scroll vertical.
Contenedor con altura o ancho fijo y scroll propio.
Instalación
shadcn
npx shadcn@latest add https://design.moodinary.com/r/scroll-area.jsonimport
import { ScrollArea, ScrollAreaItem } from "@moodinary/ui/components/scroll-area";Ejemplos
Grilla equipo por semana
El uso real en Analíticas: tabla nativa con scope dentro de un ScrollArea horizontal, con la columna de equipos fija.
– sin pulsos esa semana (una celda con color puede ser de un solo pulso)
Código
heatmap.tsx
import { ScrollArea } from "@moodinary/ui/components/scroll-area";
const weeks = [
"4 ago",
"11 ago",
"18 ago",
"25 ago",
"1 sep",
"8 sep",
"15 sep",
"22 sep",
"29 sep",
];
const rows: [string, (number | null)[]][] = [
["Equipo Ventas", [3.8, 3.6, 3.9, null, 4.1, 4.3, 3.7, 3.5, 3.9]],
["Equipo Soporte", [3.1, 2.9, 3.2, 3.4, 3.0, 2.7, 3.1, 3.3, 3.6]],
["4.º A", [4.0, 4.2, 3.9, 3.8, null, 4.1, 4.4, 4.0, 3.9]],
];
function level(avg: number) {
return Math.min(5, Math.max(1, Math.round(avg)));
}
export default function ScrollAreaHeatmap() {
return (
<figure className="w-full min-w-0">
<ScrollArea
aria-label="Promedio por equipo y semana"
className="block"
orientation="horizontal"
>
<table className="w-full min-w-max border-separate border-spacing-1 text-xs">
<thead>
<tr>
<th
className="sticky left-0 z-10 bg-card pr-4 pb-3 text-left font-medium text-ink-3"
scope="col"
>
Equipo
</th>
{weeks.map((week) => (
<th
className="whitespace-nowrap px-1 pb-3 text-center font-normal text-ink-3"
key={week}
scope="col"
>
{week}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map(([name, cells]) => (
<tr key={name}>
<th
className="sticky left-0 z-10 whitespace-nowrap bg-card py-2 pr-4 text-left font-medium"
scope="row"
>
{name}
</th>
{cells.map((avg, index) => (
<td className="p-1 text-center" key={weeks[index]}>
{avg === null ? (
<span
className="inline-flex h-[30px] w-full min-w-10 items-center justify-center rounded-md bg-bg-soft text-ink-3"
title={`${name}, semana ${weeks[index]}: sin pulsos`}
>
–
</span>
) : (
<span
className="inline-flex h-[30px] w-full min-w-10 items-center justify-center rounded-md font-medium text-ink/70 tabular-nums"
style={{
backgroundColor: `color-mix(in oklab, var(--m${level(avg)}) ${level(avg) === 3 ? 18 : 28}%, var(--card))`,
}}
title={`${name}, semana ${weeks[index]}: ${avg.toLocaleString("es-AR")}`}
>
{avg.toLocaleString("es-AR", {
minimumFractionDigits: 1,
})}
</span>
)}
</td>
))}
</tr>
))}
</tbody>
</table>
</ScrollArea>
<p className="mt-2.5 text-ink-3 text-xs">
– sin pulsos esa semana (una celda con color puede ser de un solo pulso)
</p>
</figure>
);
}Vertical
Listas de actividad con alto fijo.
Actividad ilustrativa.
Código
vertical.tsx
import { MoodDot } from "@moodinary/ui/components/mood-dot";
import {
ScrollArea,
ScrollAreaItem,
} from "@moodinary/ui/components/scroll-area";
const activity = [
"4.º A recibió un pulso",
"Se creó una acción",
"Equipo Ventas pasó a Bien",
"Nuevo comentario moderado",
"Informe de septiembre disponible",
"3.º A volvió a Bien",
"Se sumó un responsable",
"Equipo Soporte recibió un pulso",
"Se agregó un evento",
];
export default function ScrollAreaVertical() {
return (
<div className="w-full max-w-sm">
<p className="mb-2 text-ink-3 text-xs">Actividad ilustrativa.</p>
<ScrollArea aria-label="Actividad reciente">
{activity.map((item, index) => (
<ScrollAreaItem key={item}>
<MoodDot level={index % 2 === 0 ? 3 : 4} />
<span>{item}</span>
<span className="ml-auto whitespace-nowrap text-ink-3 text-xs">
{index + 1} h
</span>
</ScrollAreaItem>
))}
</ScrollArea>
</div>
);
}Horizontal
orientation="horizontal" para cards en fila.
Equipos y climas ilustrativos.
Código
horizontal.tsx
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@moodinary/ui/components/card";
import { MoodWord } from "@moodinary/ui/components/mood-dot";
import { ScrollArea } from "@moodinary/ui/components/scroll-area";
export default function ScrollAreaHorizontal() {
return (
<div className="w-full max-w-sm">
<p className="mb-2 text-ink-3 text-xs">Equipos y climas ilustrativos.</p>
<ScrollArea aria-label="Equipos" orientation="horizontal">
{[
"1.º A",
"2.º A",
"3.º B",
"4.º A",
"Equipo Ventas",
"Equipo Soporte",
].map((team) => (
<Card className="w-[140px] shrink-0" key={team} size="sm">
<CardHeader>
<CardTitle className="text-sm">{team}</CardTitle>
</CardHeader>
<CardContent>
<MoodWord level={3} />
</CardContent>
</Card>
))}
</ScrollArea>
</div>
);
}Props
ScrollArea
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
| orientation | "vertical" | "horizontal" | "vertical" | Dirección del scroll. |
| aria-label | string | - | Nombre de la región. |
| className | string | - | Clases extra. |
ScrollAreaItem
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
| ...props | ComponentProps<"div"> | - | Fila con estilos de lista. |
| className | string | - | Clases extra. |
Cuándo sí y cuándo no
Hacé
- Dale un nombre con aria-label.
- Usalo cuando el alto es fijo.
- Mostrá que hay más contenido.
Evitá
- No anides áreas de scroll.
- No lo uses para toda la página.
- No escondas acciones importantes al final.
Accesibilidad
- La región es focusable para scrollear con teclado.
- Lleva un nombre accesible.