# Card

Surface container with Header, Title, Description, Content, and Footer subparts.

- Package: `@mielui/svelte`
- Component version: `1.1.0`
- Depends on Mielui components: _internal/utils, typography
- Shared utilities: components/_internal/inset-layout, utils.cn, utils.createContext

## Install

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

## 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.


### Content

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |



### Description

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |



### Footer

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |



### Header

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |



### Root

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| variant | "default" \| "panel" \| "inset" \| undefined | 'default' | No | No | `default` is one surface. `inset` puts the content on an inner surface with the footer on the frame. `panel` adds an inner ring. |
| surface | "solid" \| "glass" \| undefined | 'solid' | No | No | `glass` frosts the card so a backdrop behind it shows through. On `inset` and `panel` cards the frame is frosted and the content surface stays close to opaque. Cards are solid unless you ask for glass. |
| class | string \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |



### Title

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| class | (string & (ClassValue \| null)) \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |
| level | HeadingLevel \| undefined | 2 | No | No | Heading level in the surrounding document. Defaults to 2. |

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

### Public types

~~~~ts
import type { DefaultProps } from '@mielui/svelte/utils';
import type { HTMLAttributes } from 'svelte/elements';
import type { HeadingLevel } from '../typography';
import Root from './card.svelte';
import Content from './card-content.svelte';
import Description from './card-description.svelte';
import Footer from './card-footer.svelte';
import Header from './card-header.svelte';
import Title from './card-title.svelte';

export type CardProps = {
    /** `default` is one surface. `inset` puts the content on an inner surface with the footer on the frame. `panel` adds an inner ring. */
    variant?: 'default' | 'panel' | 'inset';
    /**
     * `glass` frosts the card so a backdrop behind it shows through. On `inset`
     * and `panel` cards the frame is frosted and the content surface stays close
     * to opaque. Cards are solid unless you ask for glass.
     */
    surface?: 'solid' | 'glass';
} & DefaultProps;

export type CardHeaderProps = DefaultProps;
export type CardTitleProps = DefaultProps &
    HTMLAttributes<HTMLHeadingElement> & {
        /** Heading level in the surrounding document. Defaults to 2. */
        level?: HeadingLevel;
    };
export type CardDescriptionProps = DefaultProps;
export type CardContentProps = DefaultProps;
export type CardFooterProps = DefaultProps;

export { Content, Description, Footer, Header, Root, Title };
~~~~

## Examples

### Content Only

~~~~svelte
<script lang="ts">
    import { Badge } from '@mielui/svelte/components/badge';
    import * as Card from '@mielui/svelte/components/card';
</script>

<Card.Root class="w-full max-w-xs">
    <Card.Content class="gap-2">
        <span class="text-sm text-foreground-muted">Monthly recurring revenue</span>
        <div class="flex items-center gap-3">
            <span class="text-3xl font-medium tabular-nums">$48,200</span>
            <Badge variant="success">+12.4%</Badge>
        </div>
        <span class="text-sm text-foreground-muted">Up from $42,900 in August</span>
    </Card.Content>
</Card.Root>
~~~~

### Full

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as Card from '@mielui/svelte/components/card';
    import { Switch } from '@mielui/svelte/components/switch';

    type Preferences = {
        mentions: boolean;
        summary: boolean;
        news: boolean;
    };

    let saved = $state<Preferences>({
        mentions: true,
        summary: true,
        news: false
    });
    let draft = $state<Preferences>({
        mentions: true,
        summary: true,
        news: false
    });
    let status = $state('');

    const dirty = $derived(
        draft.mentions !== saved.mentions ||
            draft.summary !== saved.summary ||
            draft.news !== saved.news
    );

    function save() {
        saved = { ...draft };
        status = 'Saved';
    }

    function discard() {
        draft = { ...saved };
        status = 'Changes discarded';
    }
</script>

<Card.Root class="w-full max-w-md">
    <Card.Header>
        <Card.Title>Email notifications</Card.Title>
        <Card.Description>Choose what reaches your inbox.</Card.Description>
    </Card.Header>
    <Card.Content class="gap-4">
        <Switch
            bind:checked={draft.mentions}
            label="Mentions"
            description="When someone mentions you in a comment."
        />
        <Switch
            bind:checked={draft.summary}
            label="Weekly summary"
            description="Activity across your projects, every Monday."
        />
        <Switch
            bind:checked={draft.news}
            label="Product news"
            description="New features, about once a month."
        />
    </Card.Content>
    <Card.Footer>
        <span role="status" class="me-auto text-sm text-foreground-muted">
            {dirty ? 'Unsaved changes' : status}
        </span>
        <Button variant="outline" disabled={!dirty} onclick={discard}>Discard</Button>
        <Button disabled={!dirty} onclick={save}>Save</Button>
    </Card.Footer>
</Card.Root>
~~~~

### Glass

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as Card from '@mielui/svelte/components/card';

    const stats = [
        { label: 'Merged', value: 14 },
        { label: 'Reviews', value: 9 },
        { label: 'Deploys', value: 6 }
    ];

    let subscribed = $state(false);
</script>

<div
    class="w-full rounded-[var(--radius-xl)] bg-[linear-gradient(135deg,color-mix(in_oklab,var(--chart-1)_45%,transparent),color-mix(in_oklab,var(--chart-2)_35%,transparent)_45%,color-mix(in_oklab,var(--chart-5)_45%,transparent))] p-8"
>
    <Card.Root variant="inset" surface="glass" class="mx-auto max-w-sm">
        <Card.Header>
            <Card.Title>Weekly digest</Card.Title>
            <Card.Description>What your team shipped from October 5 to 11.</Card.Description>
        </Card.Header>
        <Card.Content>
            <dl class="m-0 grid grid-cols-3 gap-4">
                {#each stats as stat (stat.label)}
                    <div class="flex flex-col-reverse gap-1">
                        <dt class="text-sm text-foreground-muted">{stat.label}</dt>
                        <dd class="m-0 text-2xl font-medium tabular-nums">{stat.value}</dd>
                    </div>
                {/each}
            </dl>
        </Card.Content>
        <Card.Footer>
            <span role="status" class="me-auto ps-2 text-sm text-foreground-muted">
                {subscribed ? 'Sent every Monday' : 'Not subscribed'}
            </span>
            <Button
                variant={subscribed ? 'outline' : 'primary'}
                onclick={() => {
                    subscribed = !subscribed;
                }}
            >
                {subscribed ? 'Unsubscribe' : 'Subscribe'}
            </Button>
        </Card.Footer>
    </Card.Root>
</div>
~~~~

### Header Footer

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as Card from '@mielui/svelte/components/card';

    type Answer = 'pending' | 'accepted' | 'declined';

    let answer = $state<Answer>('pending');
</script>

<Card.Root class="w-full max-w-sm">
    <Card.Header>
        {#if answer === 'accepted'}
            <Card.Title>You joined Northwind</Card.Title>
            <Card.Description>You can now edit every project in the workspace.</Card.Description>
        {:else if answer === 'declined'}
            <Card.Title>Invitation declined</Card.Title>
            <Card.Description
                >Ines will not be told. You can still change your mind.</Card.Description
            >
        {:else}
            <Card.Title>Join Northwind?</Card.Title>
            <Card.Description>Ines Moreau invited you as an editor.</Card.Description>
        {/if}
    </Card.Header>
    <Card.Footer>
        {#if answer === 'pending'}
            <Button
                variant="outline"
                onclick={() => {
                    answer = 'declined';
                }}
            >
                Decline
            </Button>
            <Button
                onclick={() => {
                    answer = 'accepted';
                }}
            >
                Accept
            </Button>
        {:else}
            <Button
                variant="outline"
                onclick={() => {
                    answer = 'pending';
                }}
            >
                Undo
            </Button>
        {/if}
    </Card.Footer>
</Card.Root>
~~~~

### Hero

~~~~svelte
<script lang="ts">
    import { Badge } from '@mielui/svelte/components/badge';
    import { Button } from '@mielui/svelte/components/button';
    import * as Card from '@mielui/svelte/components/card';
    import { Progress } from '@mielui/svelte/components/progress';

    const total = 24;
    const start = 18;

    let done = $state(start);

    const percent = $derived(Math.round((done / total) * 100));
    const finished = $derived(done === total);
</script>

<Card.Root class="w-full max-w-md">
    <Card.Header>
        <div class="flex items-center justify-between gap-3">
            <Card.Title>Checkout redesign</Card.Title>
            <Badge variant={finished ? 'success' : 'info'}>
                {finished ? 'Shipped' : 'In progress'}
            </Badge>
        </div>
        <Card.Description>A shorter payment flow for the web and mobile stores.</Card.Description>
    </Card.Header>
    <Card.Content class="gap-5">
        <div class="grid gap-2">
            <div class="flex items-baseline justify-between gap-3 text-sm">
                <span class="text-foreground-muted">{done} of {total} tasks done</span>
                <span class="tabular-nums">{percent}%</span>
            </div>
            <Progress value={done} max={total} aria-label="Tasks done" />
        </div>
        <dl class="m-0 grid grid-cols-2 gap-4 text-sm">
            <div>
                <dt class="text-foreground-muted">Lead</dt>
                <dd class="m-0 mt-1">Ines Moreau</dd>
            </div>
            <div>
                <dt class="text-foreground-muted">Due</dt>
                <dd class="m-0 mt-1">October 30</dd>
            </div>
        </dl>
    </Card.Content>
    <Card.Footer>
        <Button
            variant="outline"
            disabled={done === start}
            onclick={() => {
                done = start;
            }}
        >
            Reset
        </Button>
        <Button
            disabled={finished}
            onclick={() => {
                done += 1;
            }}
        >
            Complete a task
        </Button>
    </Card.Footer>
</Card.Root>
~~~~

### Inset Position

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import * as Card from '@mielui/svelte/components/card';
    import * as Tabs from '@mielui/svelte/components/tabs';

    let position = $state('bottom');
</script>

<div class="flex w-full max-w-sm flex-col gap-4">
    <Tabs.Root bind:value={position} variant="segmented">
        <div role="group" aria-label="Footer position">
            <Tabs.List>
                <Tabs.Trigger value="bottom">Bottom</Tabs.Trigger>
                <Tabs.Trigger value="top">Top</Tabs.Trigger>
            </Tabs.List>
        </div>
    </Tabs.Root>
    <Card.Root
        variant="inset"
        class={position === 'top'
            ? '[--mielui-inset-position:top]'
            : '[--mielui-inset-position:bottom]'}
    >
        <Card.Header>
            <Card.Title>Weekly report</Card.Title>
            <Card.Description>Sent to 14 people every Monday at 9:00.</Card.Description>
        </Card.Header>
        <Card.Content>
            <p class="m-0 text-sm text-foreground-muted">
                The next report covers October 5 to 11 and goes out in three days.
            </p>
        </Card.Content>
        <Card.Footer>
            <span class="me-auto ps-2 text-sm text-foreground-muted">Next: October 12</span>
            <Button variant="outline">Send now</Button>
        </Card.Footer>
    </Card.Root>
</div>
~~~~

### Inset

~~~~svelte
<script lang="ts">
    import * as Avatar from '@mielui/svelte/components/avatar';
    import { Button } from '@mielui/svelte/components/button';
    import * as Card from '@mielui/svelte/components/card';

    type Member = {
        name: string;
        initials: string;
        role: string;
    };

    const waiting: Member[] = [
        {
            name: 'Amira Diallo',
            initials: 'AD',
            role: 'Viewer'
        },
        {
            name: 'Jonas Weber',
            initials: 'JW',
            role: 'Editor'
        }
    ];

    let members = $state<Member[]>([
        {
            name: 'Ines Moreau',
            initials: 'IM',
            role: 'Owner'
        },
        {
            name: 'Theo Martin',
            initials: 'TM',
            role: 'Editor'
        },
        {
            name: 'Maya Chen',
            initials: 'MC',
            role: 'Editor'
        }
    ]);

    const next = $derived(
        waiting.find((person) => !members.some((member) => member.name === person.name))
    );

    function invite() {
        if (next) {
            members.push(next);
        }
    }
</script>

<Card.Root variant="inset" class="w-full max-w-sm">
    <Card.Header>
        <Card.Title>Project members</Card.Title>
        <Card.Description>People who can open the checkout redesign.</Card.Description>
    </Card.Header>
    <Card.Content>
        <ul class="m-0 flex list-none flex-col gap-3 p-0">
            {#each members as member (member.name)}
                <li class="flex items-center gap-3">
                    <Avatar.Root size="sm">
                        <Avatar.Fallback>{member.initials}</Avatar.Fallback>
                    </Avatar.Root>
                    <span class="min-w-0 flex-1 truncate text-sm">{member.name}</span>
                    <span class="shrink-0 text-sm text-foreground-muted">{member.role}</span>
                </li>
            {/each}
        </ul>
    </Card.Content>
    <Card.Footer>
        <span role="status" class="me-auto ps-2 text-sm text-foreground-muted">
            {members.length}
            members
        </span>
        <Button variant="outline" disabled={!next} onclick={invite}>
            {next ? `Invite ${next.name.split(' ')[0]}` : 'Everyone is in'}
        </Button>
    </Card.Footer>
</Card.Root>
~~~~

### Panel

~~~~svelte
<script lang="ts">
    import { Badge } from '@mielui/svelte/components/badge';
    import * as Card from '@mielui/svelte/components/card';

    const details = [
        { label: 'Branch', value: 'main' },
        { label: 'Commit', value: '4f2a91c' },
        { label: 'Build time', value: '48s' },
        { label: 'Region', value: 'Paris' }
    ];
</script>

<Card.Root variant="panel" class="w-full max-w-sm">
    <Card.Header>
        <div class="flex items-center justify-between gap-3">
            <Card.Title>Latest deploy</Card.Title>
            <Badge variant="success" dot>Ready</Badge>
        </div>
        <Card.Description>Published 4 minutes ago by Theo Martin.</Card.Description>
    </Card.Header>
    <Card.Content>
        <dl class="m-0 grid gap-2.5 text-sm">
            {#each details as detail (detail.label)}
                <div class="flex items-baseline justify-between gap-4">
                    <dt class="text-foreground-muted">{detail.label}</dt>
                    <dd class="m-0 font-mono tabular-nums">{detail.value}</dd>
                </div>
            {/each}
        </dl>
    </Card.Content>
</Card.Root>
~~~~

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