# MetricTiles

> A row of KPI tiles, each with the change against the previous period, a sparkline and optional selection to drive a chart.

Source: https://docs.nasaqui.com/components/metric-tiles

## Install

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

MetricTiles lays out `StatCard`s for an analytics report. Give it each metric's value and the value for the comparison period; it works out the change, tones it (up is good unless `invert`), and prints "was 42,980" next to it. With `onSelect` the tiles become a single-choice group that picks which metric a chart below shows, as Google Analytics and Search Console do.

## When to use

- The top of any analytics page: users, sessions, clicks, views, latency.
- Metrics that must be read against the previous period.
- A tile row that also switches a chart's metric.

## When not to use

- One number with no comparison: use [`StatCard`](https://docs.nasaqui.com/components/stat-card).
- Many exact rows: use [`DataTable`](https://docs.nasaqui.com/components/data-table).
- A series over time: use [`TimeSeriesPanel`](https://docs.nasaqui.com/components/time-series-panel).

## Import

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

## Quick start

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

export function Kpis() {
  return (
    <MetricTiles
      metrics={[
        { id: "users", label: "Users", value: 48210, previous: 42980, sparkline: [4, 6, 5, 9, 8, 12] },
        { id: "bounce", label: "Bounce rate", value: 0.388, previous: 0.412, format: { style: "percent", maximumFractionDigits: 1 }, invert: true },
      ]}
    />
  );
}
```

## Anatomy

```
MetricTiles           data-slot="metric-tiles"  (a StatGrid; role="group")
  StatCard            one per metric; a button with aria-pressed when onSelect is set
```

## API

### MetricTiles

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `metrics` | `readonly MetricTileData[]` | required | The tiles, in order. |
| `selected` | `string` | none | Id of the selected tile (controlled). Only used with `onSelect`. |
| `onSelect` | `(id: string) => void` | none | Makes the tiles selectable. |
| `comparisonLabel` | `ReactNode` | `vs previous period` | Text before "was …". Localise it if you override it. |
| `loading` | `boolean` | `false` | Skeleton tiles; with no `metrics` shows `skeletons` of them. |
| `skeletons` | `number` | `4` | How many skeleton tiles to show while loading with no metrics. |
| `className` | `string` | none | Extra classes on the root. |
| `labels` | `Partial<MetricTilesLabels>` | none | Replace any built-in English or Arabic string. |

### MetricTileData

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | required | Stable id, reported by `onSelect`. |
| `label` | `ReactNode` | required | What is measured. |
| `value` | `number` | required | The current value. |
| `previous` | `number` | none | The value for the comparison period. Omit to hide the change. |
| `format` | `FormatNumberOptions` | none | Intl options for `value` and `previous`. |
| `display` | `ReactNode` | none | Replaces the formatted value, for durations such as "1m 38s". |
| `previousDisplay` | `ReactNode` | none | Replaces the formatted previous value. |
| `invert` | `boolean` | `false` | Down is good (bounce rate, latency, error rate). |
| `sparkline` | `readonly number[]` | none | Trend values. |
| `sparklineLabel` | `string` | none | Screen-reader summary of the sparkline. |
| `icon` | `ReactNode` | none | A lucide icon. |

## Examples

Selectable tiles driving a chart:

```tsx
import { MetricTiles, TimeSeriesPanel } from "@fadymondy/nasaq/web";
import { useState } from "react";

export function Report({ tiles, series }: { tiles: React.ComponentProps<typeof MetricTiles>["metrics"]; series: React.ComponentProps<typeof TimeSeriesPanel>["data"] }) {
  const [metric, setMetric] = useState("clicks");
  return (
    <>
      <MetricTiles metrics={tiles} selected={metric} onSelect={setMetric} />
      <TimeSeriesPanel metrics={[{ id: "clicks", label: "Clicks" }, { id: "impressions", label: "Impressions" }]} data={series} metric={metric} onMetricChange={setMetric} />
    </>
  );
}
```

Duration shown as text:

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

export const Time = () => <MetricTiles metrics={[{ id: "t", label: "Avg. engagement time", value: 98, previous: 104, display: "1m 38s", previousDisplay: "1m 44s" }]} />;
```

Arabic:

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

export const Ar = () => <MetricTiles comparisonLabel="مقارنة بالفترة السابقة" metrics={[{ id: "u", label: "المستخدمون", value: 48210, previous: 42980 }]} />;
```

## Accessibility

Without `onSelect` the tiles are a labelled group of text. With it, each tile is a button with `aria-pressed`, reachable with Tab and toggled with Enter or Space. The change is shown by an arrow, a sign and a colour together. Localise `label`, `sparklineLabel` and `comparisonLabel`.

## RTL & i18n

- Logical properties throughout; arrows and sparklines mirror.
- Every figure uses the active locale with Western digits and is bidi-isolated (`Num`), so `+12.4%` keeps its order in Arabic.
- Built-in strings ("vs previous period", "was") are available in English and Arabic.

## Styling & tokens

- Colours come from tokens (`--primary`, `--nq-success`, `--nq-warning`, `--nq-danger`, `--nq-tag-*`); never pass raw hex.
- Target `[data-slot="metric-tiles"]`; extend with `className`.

## Do / Don't

- Do pass `previous` so every tile shows its change.
- Do set `invert` for metrics where lower is better.
- Don't put more than about six tiles in a row.
- Don't pass a percentage as `12.4`; pass the fraction `0.124`.

## Related

- [`stat-card`](https://docs.nasaqui.com/components/stat-card)
- [`time-series-panel`](https://docs.nasaqui.com/components/time-series-panel)
- [`chart`](https://docs.nasaqui.com/components/chart)
- [`numeric`](https://docs.nasaqui.com/components/numeric)

## Lab

https://docs.nasaqui.com/?path=/docs/components-analytics-metric-tiles--docs

## Code

### React

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

export function Kpis() {
  return (
    <MetricTiles
      metrics={[
        { id: "users", label: "Users", value: 48210, previous: 42980, sparkline: [4, 6, 5, 9, 8, 12] },
        { id: "bounce", label: "Bounce rate", value: 0.388, previous: 0.412, format: { style: "percent", maximumFractionDigits: 1 }, invert: true },
      ]}
    />
  );
}
```

### shadcn

```tsx
import { MetricTiles } from "@/components/ui/metric-tiles";

export function Kpis() {
  return (
    <MetricTiles
      metrics={[
        { id: "users", label: "Users", value: 48210, previous: 42980, sparkline: [4, 6, 5, 9, 8, 12] },
        { id: "bounce", label: "Bounce rate", value: 0.388, previous: 0.412, format: { style: "percent", maximumFractionDigits: 1 }, invert: true },
      ]}
    />
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { NqMetricTiles } from "@fadymondy/nasaq/vue";
</script>

<template>
  <NqMetricTiles
    :metrics="[
      { id: 'users', label: 'Users', value: 48210, previous: 42980, sparkline: [4, 6, 5, 9, 8, 12] },
      { id: 'bounce', label: 'Bounce rate', value: 0.388, previous: 0.412, format: { style: 'percent', maximumFractionDigits: 1 }, invert: true },
    ]"
  />
</template>
```

### Blade

```blade
<x-nq::metric-tiles :metrics="[
    ['id' => 'users', 'label' => 'Users', 'value' => 48210, 'previous' => 42980, 'sparkline' => [4, 6, 5, 9, 8, 12]],
    ['id' => 'bounce', 'label' => 'Bounce rate', 'value' => 0.388, 'previous' => 0.412, 'format' => ['style' => 'percent', 'maxFraction' => 1], 'invert' => true],
]" />
```

### HTML + Alpine

```html
<div data-slot="metric-tiles" role="group" aria-label="Key metrics" x-data="nqMetricTiles(null)" 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"   data-metric="users" 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">Users</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">48,210</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.2%</bdi>
                        </span>
                        <span class="text-muted-foreground">vs previous period · was 42,980</span>                    </div>
                            </div>
                            <div data-slot="sparkline"  aria-hidden="true"  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-aab44d0c" 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,20.67C10.27,19.11,18.53,16,26.8,16C35.07,16,43.33,18.33,51.6,18.33C59.87,18.33,68.13,9,76.4,9C84.67,9,92.93,11.33,101.2,11.33C109.47,11.33,117.73,5.11,126,2L126.00,30.00L2.00,30.00Z" fill="url(#nq-spark-aab44d0c)" stroke="none" />        <path d="M2,20.67C10.27,19.11,18.53,16,26.8,16C35.07,16,43.33,18.33,51.6,18.33C59.87,18.33,68.13,9,76.4,9C84.67,9,92.93,11.33,101.2,11.33C109.47,11.33,117.73,5.11,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"   data-metric="bounce" 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">Bounce 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">38.8%</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">-5.8%</bdi>
                        </span>
                        <span class="text-muted-foreground">vs previous period · was 41.2%</span>                    </div>
                            </div>
                    </div>
    </div>
                        </div>
```
