# Heatmap

Daily activity in a keyboard-navigable contribution calendar.

- Package: `@mielui/svelte`
- Component version: `1.1.0`
- Depends on Mielui components: _internal/utils, skeleton
- Shared utilities: components/_internal/theme-changes, components/_internal/surface, components/_internal/chart-tooltip-surface, utils.cn, transition, utils.createContext

## Install

~~~~sh
pnpm dlx @mielui/svelte add heatmap
~~~~

## API

This reference is generated at build time from the component manifest, public `index.ts`, and documentation examples below. Changes to those source files are reflected here in the published Markdown. Standard Svelte and HTML attributes accepted by the exported prop types are supported.


### Root

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| labels | HeatmapLabels \| undefined | - | No | No | Overrides the built-in text and accessible names. Every key is optional and English is the fallback. |
| days | readonly Day[] | - | Yes | No | Daily counts keyed by ISO date. Missing dates count as zero. |
| weeks | number \| undefined | 26 | No | No | Number of weeks shown, from 1 to 104. |
| loading | boolean \| undefined | false | No | No | Shows the loading state in place of the content. |
| animation | "rows" \| "columns" \| "live" \| "none" \| undefined | 'rows' | No | No | `rows` and `columns` stagger the entrance, `live` repeats a column highlight, and `none` is static. |
| endDate | string \| undefined | - | No | No | Last date shown. Defaults to the latest supplied date, or today for an empty dataset. |
| weekStartsOn | 0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| undefined | 0 | No | No | First day of each week, from 0 (Sunday) to 6. |
| locale | string \| undefined | 'en-US' | No | No | Locale for month and weekday names. |
| onDaySelect | ((day: Cell) => void) \| undefined | - | No | No | Called with the computed day when a cell is activated. |
| children | Snippet<[{ days: readonly Cell[]; total: number; }]> \| undefined | - | No | No | Custom layout. Omit it for the built-in calendar, receiving the visible days and their total. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Calendar

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Cell

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| day | { date: string; count: number; level: number; column: number; row: number; label: string; } | - | Yes | No | Visible day this cell represents. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Detail

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[Cell \| undefined]> \| undefined | - | No | No | Renders the detail line for the hovered or focused day. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Footer

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Grid

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[readonly Cell[]]> \| undefined | - | No | No | Renders the cells. Omit it for one Cell per visible day. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Header

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Legend

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### MonthLabels

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Summary

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| children | Snippet<[number]> \| undefined | - | No | No | Renders the summary from the total of the visible days. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Tooltip

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### WeekdayLabels

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Public types

~~~~ts
import type { Snippet } from 'svelte';
import type { HTMLAttributes, HTMLButtonAttributes } from 'svelte/elements';
import type { Cell, Day } from './calendar';

export type HeatmapLabels = {
    unit?: (count: number) => string;
    tooltipValue?: string;
    less?: string;
    more?: string;
    grid?: string;
    gridLoading?: string;
    gridEmpty?: string;
    detailLoading?: string;
    detailEmpty?: string;
    /** Text Summary shows before the data is ready. */
    summaryPending?: string;
};

export type HeatmapProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
    /**
     * Overrides the built-in text and accessible names. Every key is optional and English is the
     * fallback.
     */
    labels?: HeatmapLabels;
    /** Daily counts keyed by ISO date. Missing dates count as zero. */
    days: readonly Day[];
    /** Number of weeks shown, from 1 to 104. */
    weeks?: number;
    /** Shows the loading state in place of the content. */
    loading?: boolean;
    /** `rows` and `columns` stagger the entrance, `live` repeats a column highlight, and `none` is static. */
    animation?: 'rows' | 'columns' | 'live' | 'none';
    /** Last date shown. Defaults to the latest supplied date, or today for an empty dataset. */
    endDate?: string;
    /** First day of each week, from 0 (Sunday) to 6. */
    weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
    /** Locale for month and weekday names. */
    locale?: string;
    /** Called with the computed day when a cell is activated. */
    onDaySelect?: (day: Cell) => void;
    /** Custom layout. Omit it for the built-in calendar, receiving the visible days and their total. */
    children?: Snippet<[{ days: readonly Cell[]; total: number }]>;
};

export type HeatmapHeaderProps = HTMLAttributes<HTMLDivElement>;
export type HeatmapCalendarProps = HTMLAttributes<HTMLDivElement>;
export type HeatmapMonthLabelsProps = HTMLAttributes<HTMLDivElement>;
export type HeatmapWeekdayLabelsProps = HTMLAttributes<HTMLDivElement>;
export type HeatmapFooterProps = HTMLAttributes<HTMLDivElement>;
export type HeatmapLegendProps = HTMLAttributes<HTMLDivElement>;
export type HeatmapTooltipProps = HTMLAttributes<HTMLDivElement>;

export type HeatmapGridProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
    /** Renders the cells. Omit it for one Cell per visible day. */
    children?: Snippet<[readonly Cell[]]>;
};

export type HeatmapCellProps = HTMLButtonAttributes & {
    /** Visible day this cell represents. */
    day: Cell;
};

export type HeatmapDetailProps = Omit<HTMLAttributes<HTMLParagraphElement>, 'children'> & {
    /** Renders the detail line for the hovered or focused day. */
    children?: Snippet<[Cell | undefined]>;
};

export type HeatmapSummaryProps = Omit<HTMLAttributes<HTMLParagraphElement>, 'children'> & {
    /** Renders the summary from the total of the visible days. */
    children?: Snippet<[number]>;
};

export { default as Root } from './heatmap.svelte';
export { default as Calendar } from './heatmap-calendar.svelte';
export { default as Cell } from './heatmap-cell.svelte';
export { default as Detail } from './heatmap-detail.svelte';
export { default as Footer } from './heatmap-footer.svelte';
export { default as Grid } from './heatmap-grid.svelte';
export { default as Header } from './heatmap-header.svelte';
export { default as Legend } from './heatmap-legend.svelte';
export { default as MonthLabels } from './heatmap-month-labels.svelte';
export { default as Summary } from './heatmap-summary.svelte';
export { default as Tooltip } from './heatmap-tooltip.svelte';
export { default as WeekdayLabels } from './heatmap-weekday-labels.svelte';
~~~~

## Examples

### Composed

~~~~svelte
<script lang="ts">
    import * as Heatmap from '@mielui/svelte/components/heatmap';
    import { days } from './data';

    let selected = $state('Choose a day');
</script>

<Heatmap.Root
    {days}
    weeks={12}
    endDate="2026-09-15"
    weekStartsOn={1}
    class="max-w-sm"
    onDaySelect={(day) => {
        selected = `${day.date}: ${day.count} contributions`;
    }}
>
    {#snippet children({ total })}
        <Heatmap.Header>
            <span class="text-sm font-medium">Last 12 weeks</span>
            <span class="text-xs text-foreground-muted">{total} contributions</span>
        </Heatmap.Header>
        <Heatmap.Calendar>
            <Heatmap.MonthLabels />
            <Heatmap.Grid />
        </Heatmap.Calendar>
        <Heatmap.Tooltip />
        <Heatmap.Footer>
            <Heatmap.Legend />
            <Heatmap.Detail />
        </Heatmap.Footer>
        <p aria-live="polite" class="text-xs text-foreground-muted">{selected}</p>
    {/snippet}
</Heatmap.Root>
~~~~

### Range

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as Heatmap from '@mielui/svelte/components/heatmap';
    import { days } from './data';

    let weeks = $state(12);

    function switchRange() {
        weeks = weeks === 12 ? 26 : 12;
    }
</script>

<div class="w-full space-y-4">
    <Button variant="secondary" onclick={switchRange}>
        {weeks === 12 ? 'Show six months' : 'Show twelve weeks'}
    </Button>
    <Heatmap.Root {days} {weeks} endDate="2026-09-15" weekStartsOn={1} />
</div>
~~~~

### States

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as Heatmap from '@mielui/svelte/components/heatmap';
    import * as Tabs from '@mielui/svelte/components/tabs';
    import { days } from './data';

    let dataState = $state('ready');
    let animation = $state<'rows' | 'columns' | 'live' | 'none'>('rows');
    let replay = $state(0);

    function changeAnimation(value: string) {
        if (value === 'rows' || value === 'columns' || value === 'live' || value === 'none') {
            animation = value;
        }
    }

    function replayCalendar() {
        replay += 1;
    }
</script>

<div class="w-full space-y-6">
    <div class="flex flex-wrap items-center gap-3">
        <Tabs.Root bind:value={dataState} variant="ghost">
            <div role="group" aria-label="Activity data state">
                <Tabs.List>
                    <Tabs.Trigger value="ready">Ready</Tabs.Trigger>
                    <Tabs.Trigger value="loading">Loading</Tabs.Trigger>
                    <Tabs.Trigger value="empty">No data</Tabs.Trigger>
                </Tabs.List>
            </div>
        </Tabs.Root>
        <Tabs.Root value={animation} onValueChange={changeAnimation} variant="ghost">
            <div role="group" aria-label="Activity animation">
                <Tabs.List>
                    <Tabs.Trigger value="rows">Rows</Tabs.Trigger>
                    <Tabs.Trigger value="columns">Columns</Tabs.Trigger>
                    <Tabs.Trigger value="live">Live</Tabs.Trigger>
                    <Tabs.Trigger value="none">None</Tabs.Trigger>
                </Tabs.List>
            </div>
        </Tabs.Root>
        <Button variant="secondary" onclick={replayCalendar}>Replay</Button>
    </div>
    {#key `${animation}-${replay}`}
        <Heatmap.Root
            days={dataState === 'empty' ? [] : days}
            loading={dataState === 'loading'}
            weeks={26}
            endDate="2026-09-15"
            {animation}
        />
    {/key}
</div>
~~~~

### data.ts

The examples import this module as `./data`. It sits beside them in the same folder.

~~~~ts
export const days = Array.from({ length: 182 }, (_, index) => {
    const date = new Date(Date.UTC(2026, 2, 18 + index));

    return {
        date: date.toISOString().slice(0, 10),
        count: index % 7 === 0 ? 0 : (index * 17 + (index % 11)) % 24
    };
});
~~~~

For the rendered reference, visit [/docs/components/heatmap](/docs/components/heatmap).
