StatsCard
A metric with label and optional delta in a compact card. Dashboards and KPI strips.
Editorial
<!-- F12c editorial — non-derivable only. Review: Karen. -->
## Ejemplos
KPI with delta:
```tsx import { StatsCard } from '@/components/atoms/StatsCard';
<StatsCard value="12.4k" label="Active users" trend="up" trendValue="+8%" /> ```
## Accesibilidad
- `value` and `label` are required. Trend icons are `aria-hidden` — put meaning in `trendValue` text (e.g. `+8%`). - Do not rely on trend color alone for up/down.
## Cuándo no usar
- Full data tables → `Table`. - Marketing hero copy without a metric → Typography / layout blocks, not a stats card.
Uso
import { StatsCard } from '@/components/atoms/StatsCard';
<StatsCard value="12.4k" label="Active users" trend="up" trendValue="+8%" />Props
| Prop | Tipo | Default | Rango / opciones | What | How |
|---|---|---|---|---|---|
| trend | select | — | `up`, `down`, `neutral` | Direction of the trend chip (with trendValue). | up for positive KPIs; down for regressions; neutral for flat. Omit both trend and trendValue when no delta. |
| compact | boolean | — | `true` / `false` | Denser padding and type scale. | true in dense dashboards/tables; false for hero KPI strips. |
| gradient | boolean | — | `true` / `false` | Gradient surface treatment. | true sparingly for featured metrics; false for data-dense grids. |
Gotchas
- react
value and label are required strings. Trend UI only renders when both trend and trendValue are set.
- a11y
Trend icons are aria-hidden; put meaning in trendValue text (e.g. +12%).
Anatomía CSS
<div class="stats-card"> <span class="stats-card__label"></span> <span class="stats-card__trend"></span> <span class="stats-card__trend--down"></span> <span class="stats-card__trend--neutral"></span> <span class="stats-card__trend--up"></span> <span class="stats-card__trend-icon"></span> <span class="stats-card__value"></span> </div>
| Clase | Propósito |
|---|---|
stats-card | root |
stats-card--compact | modifier |
stats-card--gradient | modifier |
stats-card__label | element |
stats-card__trend | element |
stats-card__trend--down | element |
stats-card__trend--neutral | element |
stats-card__trend--up | element |
stats-card__trend-icon | element |
stats-card__value | element |
Tokens resueltos
Valores finales tras seguir la cadena de tokens. Derivados del source: si un token cambia, esta tabla cambia sola.
| Variante | Prop | Valor | Token |
|---|---|---|---|
| all | bg | #ffffff | card |
| all | border | 1px | stroke.hairline |
| all | fg | #525252 | muted.foreground |
| gradient | bg | var(--gradient-brand) | --gradient-brand |
Animaciones
| Propiedad | Duración | Easing |
|---|---|---|
border-color | var(--duration-150) | var(--easing-out) |
CSS mínimo funcional
Autocontenido: sin imports ni tokens. Para previews y prototipos — en producción se consume el CSS del DS (@atom-uikit/css/components.css</code>, <code>@atom-uikit/tokens/tokens.css).
/* ============================================================
StatsCard — metric number + label + optional trend indicator
============================================================ */
.stats-card {
display: flex;
flex-direction: column;
gap: 12px;
padding: 32px;
border-radius: 16px;
background-color: #ffffff;
border: 1px solid #e5e5e5;
transition: border-color 150ms cubic-bezier(0.22, 1, 0.36, 1);
}
.stats-card--compact {
padding: 16px;
gap: 8px;
border-radius: 12px;
}
/* ---- Value (big number) ---- */
.stats-card__value {
font-size: 48.83px;
font-weight: 700;
line-height: 1;
letter-spacing: -0.03em;
color: #0a0a0a;
font-variant-numeric: tabular-nums;
}
.stats-card--compact .stats-card__value {
font-size: 31.25px;
}
.stats-card--gradient .stats-card__value {
/* fallback keeps standalone installs working without the utilities item */
background: linear-gradient(135deg, var(--color-forest 0%, #ff6600 100%));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
/* ---- Label ---- */
.stats-card__label {
font-size: 12.8px;
font-weight: 500;
color: #525252;
line-height: 1.4;
}
/* ---- Trend indicator ---- */
.stats-card__trend {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10.24px;
font-weight: 500;
line-height: 1;
margin-top: 4px;
}
.stats-card__trend--up {
color: #25d366;
}
.stats-card__trend--down {
color: #f84131;
}
.stats-card__trend--neutral {
color: #525252;
}
.stats-card__trend-icon {
display: flex;
width: 1em;
height: 1em;
}
.stats-card__trend-icon svg {
width: 100%;
height: 100%;
}
Codigo fuente
import { forwardRef, type ReactNode } from 'react'; export type StatsTrend = 'up' | 'down' | 'neutral'; export type StatsCardProps = { value: string; label: string; trend?: StatsTrend; trendValue?: string; compact?: boolean; gradient?: boolean; className?: string; }; function cn(...classes: (string | false | undefined | null)[]) { return classes.filter(Boolean).join(' '); } function TrendIcon({ direction }: { direction: 'up' | 'down' }) { if (direction === 'up') { return ( <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"> <polyline points="22 7 13.5 15.5 8.5 10.5 2 17" /> <polyline points="16 7 22 7 22 13" /> </svg> ); } return ( <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"> <polyline points="22 17 13.5 8.5 8.5 13.5 2 7" /> <polyline points="16 17 22 17 22 11" /> </svg> ); } export const StatsCard = forwardRef<HTMLDivElement, StatsCardProps>( ({ value, label, trend, trendValue, compact, gradient, className }, ref) => { return ( <div ref={ref} className={cn( 'stats-card', compact && 'stats-card--compact', gradient && 'stats-card--gradient', className, )} > <span className="stats-card__value">{value}</span> <span className="stats-card__label">{label}</span> {trend && trendValue && ( <span className={cn('stats-card__trend', `stats-card__trend--${trend}`)}> {trend !== 'neutral' && ( <span className="stats-card__trend-icon" aria-hidden="true"> <TrendIcon direction={trend} /> </span> )} {trendValue} </span> )} </div> ); }, ); StatsCard.displayName = 'StatsCard';
Webflow
Pega en el Designer como application/json, luego convierte a Component (Atom / StatsCard) y publica. Formato interno no documentado de Webflow — regenerable, no dependencia de runtime.
Setup del sitio (una vez)
Custom Code → Head:
<link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/tokens.css"> <link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/components.css">
Unsupported (no silencioso)
selectoron.stats-card--compact .stats-card__value— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.stats-card--gradient .stats-card__value— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.stats-card__trend-icon svg— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)--gradient-brandon:root— token not found in tokens-nested.json — resolve upstream or the declaration stays invalid on paste
Tras pegar: Create component → nombre Atom / StatsCard → Publish.