# StatCard

> KPI tile with a label, a large tabular figure, a good/bad delta, an optional sparkline and icon, a loading skeleton, and a responsive StatGrid.

Source: https://docs.nasaqui.com/components/stat-card

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/stat-card.json
```

A single metric on a `Card`: what it is, its value, how it changed since the last period, and optionally a trend line. The value
is a `Num` (tabular digits, locale formatting). The delta is toned by whether the change is good: up is good by default; set
`invert` for cost-style metrics where down is good. The tone is shown with an arrow and a sign as well as colour.

## When to use

- Headline numbers at the top of a dashboard or report.
- A metric that needs its change over time next to it.

## When not to use

- A series of values: use [`ChartContainer`](https://docs.nasaqui.com/components/chart).
- Many exact rows: use [`DataTable`](https://docs.nasaqui.com/components/data-table).
- A generic container: use [`Card`](https://docs.nasaqui.com/components/card).

## Import

```tsx
import { StatCard, StatGrid } from "@fadymondy/nasaq/web";
```

## Quick start

```tsx
import { StatCard, StatGrid } from "@fadymondy/nasaq/web";
import { Wallet } from "lucide-react";

export function Kpis() {
  return (
    <StatGrid>
      <StatCard
        icon={<Wallet />}
        label="Revenue"
        value={48210}
        format={{ style: "currency", currency: "SAR", maximumFractionDigits: 0 }}
        delta={0.124}
        deltaLabel="vs last month"
        sparkline={[4, 6, 5, 9, 8, 12]}
      />
      <StatCard label="Cloud cost" value={9120} delta={-0.06} invert deltaLabel="vs last month" />
    </StatGrid>
  );
}
```

## Anatomy

```
StatCard                data-slot="stat-card"  data-trend="up|down|flat"  data-tone="positive|negative|neutral"
  stat-card-icon        data-slot="stat-card-icon"    (optional)
  stat-card-label       data-slot="stat-card-label"
  stat-card-value       data-slot="stat-card-value"
  stat-card-delta       data-slot="stat-card-delta"   (when delta is set)
  Sparkline             data-slot="sparkline"         (when sparkline is set)
  stat-card-skeleton    data-slot="stat-card-skeleton" (when loading)
StatGrid                data-slot="stat-grid"
```

## API

### StatCard

Extends `Card` props (except `children`).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `ReactNode` | required | What is measured. Localise it. |
| `value` | `number \| ReactNode` | required | A number is rendered with `Num`; a node is rendered as is. |
| `format` | `FormatNumberOptions` | none | Intl options for a numeric `value`. |
| `delta` | `number` | none | Change as a fraction: `0.124` is +12.4%. `0` shows a flat state. |
| `deltaFormat` | `FormatNumberOptions` | percent, 1 decimal, explicit sign | Override the delta format. |
| `deltaLabel` | `ReactNode` | none | Text after the delta ("vs last month"). Localise it. |
| `invert` | `boolean` | `false` | Down is good, up is bad (costs, churn, errors). |
| `sparkline` | `readonly number[]` | none | Trend line values; coloured by the delta tone. |
| `sparklineLabel` | `string` | none | Screen-reader summary of the sparkline; without it, it is decorative. |
| `icon` | `ReactNode` | none | Glyph in a soft tile, for example `<Wallet />`. |
| `loading` | `boolean` | `false` | Skeleton with the same layout; sets `aria-busy`. |

### StatGrid

`div` props. A grid of equal columns, each at least 14rem wide; use `className` to cap the width or change the gap.

## Examples

Cost metric (down is good):

```tsx
import { StatCard } from "@fadymondy/nasaq/web";

export const Cost = () => <StatCard label="Refund rate" value={0.032} format={{ style: "percent", minimumFractionDigits: 1 }} delta={-0.4} invert />;
```

Loading:

```tsx
import { StatCard, StatGrid } from "@fadymondy/nasaq/web";

export const Loading = ({ ready }: { ready: boolean }) => (
  <StatGrid>
    <StatCard label="Users" value={3204} loading={!ready} />
  </StatGrid>
);
```

Arabic:

```tsx
import { StatCard } from "@fadymondy/nasaq/web";

export const Ar = () => (
  <StatCard label="الإيرادات" value={48210} format={{ style: "currency", currency: "SAR", maximumFractionDigits: 0 }} delta={0.124} deltaLabel="مقارنة بالشهر الماضي" />
);
```

## Accessibility

The card is a plain group of text; there is no keyboard interaction. Wrap it in a link or button yourself if it navigates.
Trend is conveyed by the arrow, the explicit sign and the tone colour together. `loading` sets `aria-busy` and the skeleton
is `aria-hidden`. Localise `label`, `deltaLabel` and `sparklineLabel`.

## RTL & i18n

- Everything uses logical properties; the layout mirrors in RTL and the trend arrow flips.
- Figures use the active locale with Western digits and are bidi-isolated (`Num`), so "+12.4%" keeps its order.
- The sparkline runs right to left in RTL.

## Styling & tokens

- Tone: `--nq-success-text` / `--nq-danger-text` for the delta, `--nq-success` / `--nq-danger` / `--primary` for the sparkline.
- Target `[data-slot="stat-card"][data-tone="negative"]` for state styling; extend with `className`.

## Do / Don't

- Do set `invert` for metrics where lower is better.
- Do give a `deltaLabel` so the comparison period is clear.
- Don't put more than about six StatCards in one row of a dashboard.
- Don't pass a percentage as `12.4`; pass the fraction `0.124`.

## Related

- [`ChartContainer`](https://docs.nasaqui.com/components/chart)
- [`Card`](https://docs.nasaqui.com/components/card)
- [`Num`](https://docs.nasaqui.com/components/numeric)

## Lab

https://docs.nasaqui.com/?path=/docs/components-data-display-stat-card--docs

## Code

### React

```tsx
import { StatCard, StatGrid } from "@fadymondy/nasaq/web";
import { Wallet } from "lucide-react";

export function Kpis() {
  return (
    <StatGrid>
      <StatCard
        icon={<Wallet />}
        label="Revenue"
        value={48210}
        format={{ style: "currency", currency: "SAR", maximumFractionDigits: 0 }}
        delta={0.124}
        deltaLabel="vs last month"
        sparkline={[4, 6, 5, 9, 8, 12]}
      />
      <StatCard label="Cloud cost" value={9120} delta={-0.06} invert deltaLabel="vs last month" />
    </StatGrid>
  );
}
```

### shadcn

```tsx
import { StatCard, StatGrid } from "@/components/ui/stat-card";
import { Wallet } from "lucide-react";

export function Kpis() {
  return (
    <StatGrid>
      <StatCard
        icon={<Wallet />}
        label="Revenue"
        value={48210}
        format={{ style: "currency", currency: "SAR", maximumFractionDigits: 0 }}
        delta={0.124}
        deltaLabel="vs last month"
        sparkline={[4, 6, 5, 9, 8, 12]}
      />
      <StatCard label="Cloud cost" value={9120} delta={-0.06} invert deltaLabel="vs last month" />
    </StatGrid>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqStatCard, NqStatGrid } from "@fadymondy/nasaq/vue";
import { Wallet } from "lucide-vue-next";
</script>

<template>
  <NqStatGrid>
    <NqStatCard label="Revenue" :value="48210" :format="{ style: 'currency', currency: 'USD', notation: 'compact' }" :delta="0.124" delta-label="vs last month" :sparkline="[18, 24, 21, 30, 28, 36]" sparkline-label="Revenue, last 6 weeks, up 12%">
      <template #icon><Wallet /></template>
    </NqStatCard>
    <NqStatCard label="Refund rate" :value="0.031" :format="{ style: 'percent', maximumFractionDigits: 1 }" :delta="-0.03" invert delta-label="vs last month" />
    <NqStatCard label="Orders" loading />
  </NqStatGrid>
</template>
```

### Blade

```blade
<x-nq::stat-card.grid>
    <x-nq::stat-card label="Revenue" :value="48210" :format="['style' => 'currency', 'currency' => 'USD', 'compact' => true]" :delta="0.124" delta-label="vs last month" :sparkline="[18, 24, 21, 30, 28, 36]" sparkline-label="Revenue, last 6 weeks, up 12%">
        <x-slot:icon><x-lucide-wallet /></x-slot:icon>
    </x-nq::stat-card>
    <x-nq::stat-card label="Refund rate" :value="0.031" :format="['style' => 'percent', 'maxFraction' => 1]" :delta="-0.03" invert delta-label="vs last month" />
    <x-nq::stat-card label="Orders" loading />
</x-nq::stat-card.grid>
```

### HTML + Alpine

```html
<div data-slot="stat-grid" class="grid grid-cols-[repeat(auto-fit,minmax(min(100%,14rem),1fr))] gap-3"><div data-slot="stat-card"  data-trend="up" data-tone="positive"   class="flex flex-col gap-3 rounded-card border border-border bg-card px-4 py-4 text-card-foreground">
            <div class="flex items-center gap-2">
                            <span data-slot="stat-card-icon" aria-hidden="true" class="grid size-8 shrink-0 place-items-center rounded-control bg-secondary text-muted-foreground [&_svg]:size-4"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1"/>
  <path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4"/>
</svg></span>
                        <div data-slot="stat-card-label" class="min-w-0 truncate text-body-sm text-muted-foreground">Revenue</div>
        </div>
        <div class="flex items-end justify-between gap-3">
            <div class="flex min-w-0 flex-col gap-1">
                <div data-slot="stat-card-value" class="text-h2 leading-tight text-foreground tabular-nums">
                                            <bdi data-slot="num" data-numeric="" class="tabular-nums">$48K</bdi>
                                    </div>
                                    <div data-slot="stat-card-delta" class="flex flex-wrap items-center gap-x-1.5 text-caption">
                        <span class="inline-flex items-center gap-1 text-label text-nq-success-text">
                            <svg aria-hidden="true" class="size-3.5 shrink-0 rtl:-scale-x-100" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M16 7h6v6"/>
  <path d="m22 7-8.5 8.5-5-5L2 17"/>
</svg>                            <bdi data-slot="num" data-numeric="" class="tabular-nums">+12.4%</bdi>
                        </span>
                        <span class="text-muted-foreground">vs last month</span>                    </div>
                            </div>
                            <div data-slot="sparkline"  role="img" aria-label="Revenue, last 6 weeks, up 12%"  class="h-8 shrink-0 w-24">
    <svg viewBox="0 0 128 32" preserveAspectRatio="none" class="size-full overflow-visible rtl:-scale-x-100" aria-hidden="true" focusable="false">
        <defs>
            <linearGradient id="nq-spark-7f2b5f1e" x1="0" y1="0" x2="0" y2="1">
                <stop offset="0%" stop-color="var(--nq-success)" stop-opacity="0.3" />
                <stop offset="100%" stop-color="var(--nq-success)" stop-opacity="0" />
            </linearGradient>
        </defs>
        <path d="M2,16C10.27,14.44,18.53,11.33,26.8,11.33C35.07,11.33,43.33,13.67,51.6,13.67C59.87,13.67,68.13,6.67,76.4,6.67C84.67,6.67,92.93,8.22,101.2,8.22C109.47,8.22,117.73,4.07,126,2L126.00,30.00L2.00,30.00Z" fill="url(#nq-spark-7f2b5f1e)" stroke="none" />        <path d="M2,16C10.27,14.44,18.53,11.33,26.8,11.33C35.07,11.33,43.33,13.67,51.6,13.67C59.87,13.67,68.13,6.67,76.4,6.67C84.67,6.67,92.93,8.22,101.2,8.22C109.47,8.22,117.73,4.07,126,2" fill="none" stroke="var(--nq-success)" stroke-width="1.5" vector-effect="non-scaling-stroke" stroke-linejoin="round" stroke-linecap="round" />
    </svg>
</div>
                    </div>
    </div>
    <div data-slot="stat-card"  data-trend="down" data-tone="positive"   class="flex flex-col gap-3 rounded-card border border-border bg-card px-4 py-4 text-card-foreground">
            <div class="flex items-center gap-2">
                        <div data-slot="stat-card-label" class="min-w-0 truncate text-body-sm text-muted-foreground">Refund rate</div>
        </div>
        <div class="flex items-end justify-between gap-3">
            <div class="flex min-w-0 flex-col gap-1">
                <div data-slot="stat-card-value" class="text-h2 leading-tight text-foreground tabular-nums">
                                            <bdi data-slot="num" data-numeric="" class="tabular-nums">3.1%</bdi>
                                    </div>
                                    <div data-slot="stat-card-delta" class="flex flex-wrap items-center gap-x-1.5 text-caption">
                        <span class="inline-flex items-center gap-1 text-label text-nq-success-text">
                            <svg aria-hidden="true" class="size-3.5 shrink-0 rtl:-scale-x-100" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M16 17h6v-6"/>
  <path d="m22 17-8.5-8.5-5 5L2 7"/>
</svg>                            <bdi data-slot="num" data-numeric="" class="tabular-nums">-3%</bdi>
                        </span>
                        <span class="text-muted-foreground">vs last month</span>                    </div>
                            </div>
                    </div>
    </div>
    <div data-slot="stat-card"   aria-busy="true"  class="flex flex-col gap-3 rounded-card border border-border bg-card px-4 py-4 text-card-foreground">
            <div data-slot="stat-card-skeleton" class="flex flex-col gap-3">
            <div data-slot="skeleton" aria-hidden="true" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-3.5 w-24"></div>
            <div data-slot="skeleton" aria-hidden="true" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-8 w-32"></div>
            <div data-slot="skeleton" aria-hidden="true" class="rounded-[4px] bg-secondary motion-safe:animate-pulse h-3.5 w-20"></div>
        </div>
    </div></div>
```
