Card
Slot-based container. Compose Header, Title, Description, Content, Footer, and Action — every slot optional, stacked on a rounded surface that inherits the project's radius token.
Composition
Header only
Just a title and description.
Header + content
Title, description, body copy.
Anything goes in the content slot — text, inputs, lists, media.
With footer
Actions anchor the bottom.
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
</script>
<Card.Root>
<Card.Header>
<Card.Title>Title</Card.Title>
<Card.Description>Supporting copy.</Card.Description>
</Card.Header>
<Card.Content>Body content.</Card.Content>
<Card.Footer>
<Button size="sm">Continue</Button>
</Card.Footer>
</Card.Root>With Action slot
Unstake $BLOOM
Withdrawing resets the cooldown. You can re-stake anytime.
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { untrack } from 'svelte';
import { cn, type WithElementRef } from '$lib/utils.js';
import { setSurfaceContext, surfaceClass } from '$lib/components/ui/surface';
type CardLevel = 3 | 4 | 5;
let {
ref = $bindable(null),
class: className,
children,
size = 'default',
level = 3,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
size?: 'default' | 'sm';
level?: CardLevel;
} = $props();
// Pin the substrate level for descendants at mount. Changing `level` after
// mount is not supported — `untrack` makes that contract explicit.
setSurfaceContext(untrack(() => level));
</script>
<div
bind:this={ref}
data-slot="card"
data-substrate={level}
data-size={size}
data-surface-level={level}
class={cn(
'group/card flex flex-col gap-6 overflow-hidden rounded-(--radius-panel) py-6 text-sm text-card-foreground has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
surfaceClass[level],
className
)}
{...restProps}
>
{@render children?.()}
</div>
Sizes
Default size
Standard padding and gap.
Small size
Tighter padding via data-size="sm".
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { untrack } from 'svelte';
import { cn, type WithElementRef } from '$lib/utils.js';
import { setSurfaceContext, surfaceClass } from '$lib/components/ui/surface';
type CardLevel = 3 | 4 | 5;
let {
ref = $bindable(null),
class: className,
children,
size = 'default',
level = 3,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
size?: 'default' | 'sm';
level?: CardLevel;
} = $props();
// Pin the substrate level for descendants at mount. Changing `level` after
// mount is not supported — `untrack` makes that contract explicit.
setSurfaceContext(untrack(() => level));
</script>
<div
bind:this={ref}
data-slot="card"
data-substrate={level}
data-size={size}
data-surface-level={level}
class={cn(
'group/card flex flex-col gap-6 overflow-hidden rounded-(--radius-panel) py-6 text-sm text-card-foreground has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
surfaceClass[level],
className
)}
{...restProps}
>
{@render children?.()}
</div>
Login card
Sign in to bloom
Use your work email to continue.
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { untrack } from 'svelte';
import { cn, type WithElementRef } from '$lib/utils.js';
import { setSurfaceContext, surfaceClass } from '$lib/components/ui/surface';
type CardLevel = 3 | 4 | 5;
let {
ref = $bindable(null),
class: className,
children,
size = 'default',
level = 3,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
size?: 'default' | 'sm';
level?: CardLevel;
} = $props();
// Pin the substrate level for descendants at mount. Changing `level` after
// mount is not supported — `untrack` makes that contract explicit.
setSurfaceContext(untrack(() => level));
</script>
<div
bind:this={ref}
data-slot="card"
data-substrate={level}
data-size={size}
data-surface-level={level}
class={cn(
'group/card flex flex-col gap-6 overflow-hidden rounded-(--radius-panel) py-6 text-sm text-card-foreground has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
surfaceClass[level],
className
)}
{...restProps}
>
{@render children?.()}
</div>
With image
Season release
A curated pass of components, motion and token refinements.
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { untrack } from 'svelte';
import { cn, type WithElementRef } from '$lib/utils.js';
import { setSurfaceContext, surfaceClass } from '$lib/components/ui/surface';
type CardLevel = 3 | 4 | 5;
let {
ref = $bindable(null),
class: className,
children,
size = 'default',
level = 3,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
size?: 'default' | 'sm';
level?: CardLevel;
} = $props();
// Pin the substrate level for descendants at mount. Changing `level` after
// mount is not supported — `untrack` makes that contract explicit.
setSurfaceContext(untrack(() => level));
</script>
<div
bind:this={ref}
data-slot="card"
data-substrate={level}
data-size={size}
data-surface-level={level}
class={cn(
'group/card flex flex-col gap-6 overflow-hidden rounded-(--radius-panel) py-6 text-sm text-card-foreground has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
surfaceClass[level],
className
)}
{...restProps}
>
{@render children?.()}
</div>
Nested cards
Pass level={4} on a nested card so the hierarchy doesn't collapse. The inner card
paints on the next surface step and re-provides that level — any descendant overlay (popover, dropdown)
walks from there.
Outer card
Default level 3 — sits on the page.
Nested card
Level 4 — visibly raised against its parent.
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
import { untrack } from 'svelte';
import { cn, type WithElementRef } from '$lib/utils.js';
import { setSurfaceContext, surfaceClass } from '$lib/components/ui/surface';
type CardLevel = 3 | 4 | 5;
let {
ref = $bindable(null),
class: className,
children,
size = 'default',
level = 3,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
size?: 'default' | 'sm';
level?: CardLevel;
} = $props();
// Pin the substrate level for descendants at mount. Changing `level` after
// mount is not supported — `untrack` makes that contract explicit.
setSurfaceContext(untrack(() => level));
</script>
<div
bind:this={ref}
data-slot="card"
data-substrate={level}
data-size={size}
data-surface-level={level}
class={cn(
'group/card flex flex-col gap-6 overflow-hidden rounded-(--radius-panel) py-6 text-sm text-card-foreground has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
surfaceClass[level],
className
)}
{...restProps}
>
{@render children?.()}
</div>
Card.Root props
Sub-components (Header, Title, Description, Content, Footer, Action) each accept standard HTML div attributes plus `class`.
| Prop | Type | Default | Description |
|---|---|---|---|
'default' | 'sm' | 'default' | Adjusts padding and internal gap via data-size. Small is tighter for dense UIs. | |
3 | 4 | 5 | 3 | Surface elevation level. Pass 4 or 5 when nesting cards so the hierarchy reads. Re-provides the level for descendant overlays via Svelte context. | |
string | — | Merged onto the root via tailwind-merge. | |
HTMLDivElement | null | null | Two-way-bindable element reference. |