Skip to content
edmi-ui

Card

Surface with optional elevation (sunken, raised, floating). Header, title, description, action, content and footer; size default or sm.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Join MAG4
Magnificent Four, 4 tokens
Amount 1,000 USDC
Scheduled reports
Weekly, Monday 09:00
Join MAG4
Magnificent Four, 4 tokens
Amount 1,000 USDC
Scheduled reports
Weekly, Monday 09:00
Join MAG4

Magnificent Four, 4 tokens

Amount 1,000 USDC
Scheduled reports

Weekly, Monday 09:00

Terminal window
npx shadcn@latest add @edmi-ui/card
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/card
Terminal window
yarn dlx shadcn@latest add @edmi-ui/card
Terminal window
bunx shadcn@latest add @edmi-ui/card

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/card
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/card
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/card
Terminal window
bunx shadcn-vue@latest add @edmi-ui/card

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/card.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/card.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/card.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/card.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import { Button } from "@/components/ui/button";
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
<div className="flex flex-wrap items-start gap-5">
<Card className="w-80">
<CardHeader>
<CardTitle>Join MAG4</CardTitle>
<CardDescription>Magnificent Four, 4 tokens</CardDescription>
<CardAction>
<Button size="sm" variant="outline">
Edit
</Button>
</CardAction>
</CardHeader>
<CardContent className="font-mono text-sm">
Amount 1,000 USDC
</CardContent>
<CardFooter>
<Button size="sm" variant="outline">
Cancel
</Button>
<Button size="sm">Join index</Button>
</CardFooter>
</Card>
{/* … */}
</div>
<script setup lang="ts">
import { Button } from "@/components/ui/button";
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
</script>
<template>
<div class="flex flex-wrap items-start gap-5">
<Card class="w-80">
<CardHeader>
<CardTitle>Join MAG4</CardTitle>
<CardDescription>Magnificent Four, 4 tokens</CardDescription>
<CardAction>
<Button size="sm" variant="outline">Edit</Button>
</CardAction>
</CardHeader>
<CardContent class="font-mono text-sm">Amount 1,000 USDC</CardContent>
<CardFooter>
<Button size="sm" variant="outline">Cancel</Button>
<Button size="sm">Join index</Button>
</CardFooter>
</Card>
<Card size="sm" class="w-64">
<CardHeader>
<CardTitle>Scheduled reports</CardTitle>
<CardDescription>Weekly, Monday 09:00</CardDescription>
</CardHeader>
</Card>
</div>
</template>
<script lang="ts">
import * as Card from "$lib/components/ui/card";
import { Button } from "$lib/components/ui/button";
</script>
<div class="flex flex-wrap items-start gap-5">
<Card.Root class="w-80">
<Card.Header>
<Card.Title>Join MAG4</Card.Title>
<Card.Description>Magnificent Four, 4 tokens</Card.Description>
<Card.Action>
<Button size="sm" variant="outline">Edit</Button>
</Card.Action>
</Card.Header>
<Card.Content class="font-mono text-sm">Amount 1,000 USDC</Card.Content>
<Card.Footer>
<Button size="sm" variant="outline">Cancel</Button>
<Button size="sm">Join index</Button>
</Card.Footer>
</Card.Root>
<Card.Root size="sm" class="w-64">
<Card.Header>
<Card.Title>Scheduled reports</Card.Title>
<Card.Description>Weekly, Monday 09:00</Card.Description>
</Card.Header>
</Card.Root>
</div>
registry/ui/card.tsx
"use client";
import { cn } from "cn";
import type * as React from "react";
import {
type Elevation,
SurfaceProvider,
useElevation,
} from "@/registry/edmi/ui/elevation";
// ✦ depth (v4): surface role. A card inside a raised/floating surface resolves flat (no bevel on bevel).
const cardElevation = {
sunken: "border-sk-bd bg-sk-bg shadow-sunken",
flat: "",
raised: "border-transparent shadow-raised",
floating: "border-transparent shadow-floating",
};
function Card({
className,
size = "default",
elevation,
children,
...props
}: React.ComponentProps<"div"> & {
size?: "default" | "sm";
/** ✦ depth: sunken -1, flat 0, raised +1 (bevel), floating +2 (bevel + drop). */
elevation?: Elevation;
}) {
const level = useElevation(elevation, "surface");
return (
<div
data-slot="card"
data-size={size}
className={cn(
// Flat by default (border only). Spacing: 22px default, 16px sm.
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl border border-border bg-card py-(--card-spacing) text-sm text-card-foreground [--card-spacing:22px] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:16px] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
cardElevation[level],
className,
)}
{...props}
>
<SurfaceProvider level={level}>{children}</SurfaceProvider>
</div>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn(
"text-base leading-snug font-semibold tracking-[-0.2px] group-data-[size=sm]/card:text-sm",
className,
)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-[13px] text-muted-foreground", className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className,
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-(--card-spacing)", className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center gap-2 rounded-b-xl border-t border-border bg-[color-mix(in_srgb,var(--muted)_60%,var(--card))] p-(--card-spacing)",
className,
)}
{...props}
/>
);
}
export {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
};
registry/ui/card/Card.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
import { type Elevation, provideSurface, useElevation } from '@/registry/edmi/ui/elevation'
const props = withDefaults(defineProps<{
class?: HTMLAttributes['class']
size?: 'default' | 'sm'
/** ✦ depth: sunken -1, flat 0, raised +1 (bevel), floating +2 (bevel + drop) */
elevation?: Elevation
}>(), {
size: 'default',
elevation: undefined,
})
// ✦ depth (v4): surface role. A card inside a raised/floating surface resolves flat (no bevel on bevel).
const cardElevation = {
sunken: 'border-sk-bd bg-sk-bg shadow-sunken',
flat: '',
raised: 'border-transparent shadow-raised',
floating: 'border-transparent shadow-floating',
}
const level = useElevation(() => props.elevation, 'surface')
provideSurface(() => level.value)
</script>
<template>
<div
data-slot="card"
:data-size="size"
:class="cn('group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl border border-border bg-card py-(--card-spacing) text-sm text-card-foreground [--card-spacing:22px] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:16px] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl', cardElevation[level], props.class)"
>
<slot />
</div>
</template>
registry/ui/card/CardAction.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="card-action"
:class="cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', props.class)"
>
<slot />
</div>
</template>
registry/ui/card/CardContent.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="card-content"
:class="cn('px-(--card-spacing)', props.class)"
>
<slot />
</div>
</template>
registry/ui/card/CardDescription.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="card-description"
:class="cn('text-[13px] text-muted-foreground', props.class)"
>
<slot />
</div>
</template>
registry/ui/card/CardFooter.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="card-footer"
:class="cn('flex items-center gap-2 rounded-b-xl border-t border-border bg-[color-mix(in_srgb,var(--muted)_60%,var(--card))] p-(--card-spacing)', props.class)"
>
<slot />
</div>
</template>
registry/ui/card/CardHeader.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="card-header"
:class="cn('group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)', props.class)"
>
<slot />
</div>
</template>
registry/ui/card/CardTitle.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="card-title"
:class="cn('text-base leading-snug font-semibold tracking-[-0.2px] group-data-[size=sm]/card:text-sm', props.class)"
>
<slot />
</div>
</template>
registry/ui/card/index.ts
export { default as Card } from "./Card.vue";
export { default as CardAction } from "./CardAction.vue";
export { default as CardContent } from "./CardContent.vue";
export { default as CardDescription } from "./CardDescription.vue";
export { default as CardFooter } from "./CardFooter.vue";
export { default as CardHeader } from "./CardHeader.vue";
export { default as CardTitle } from "./CardTitle.vue";
src/lib/registry/ui/card/card.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import { type Elevation, setSurface, useElevation } from "$lib/registry/ui/elevation/index.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
size = "default",
elevation = "auto",
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
size?: "default" | "sm";
/** ✦ depth: sunken -1, flat 0, raised +1 (bevel), floating +2 (bevel + drop). */
elevation?: Elevation;
} = $props();
// ✦ depth (v4): surface role. A card inside a raised/floating surface resolves flat (no bevel on bevel).
const cardElevation = {
sunken: "border-sk-bd bg-sk-bg shadow-sunken",
flat: "",
raised: "border-transparent shadow-raised",
floating: "border-transparent shadow-floating",
};
const level = useElevation(() => elevation, "surface");
setSurface(() => level.current);
</script>
<div
bind:this={ref}
data-slot="card"
data-size={size}
class={cn(
// Flat by default (border only); elevation ✦ adds depth. Spacing: 22px default, 16px sm.
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl border border-border bg-card py-(--card-spacing) text-sm text-card-foreground [--card-spacing:22px] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:16px] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
cardElevation[level.current],
className
)}
{...restProps}
>
{@render children?.()}
</div>
src/lib/registry/ui/card/card-header.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-header"
class={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
className
)}
{...restProps}
>
{@render children?.()}
</div>
src/lib/registry/ui/card/card-title.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-title"
class={cn("text-base leading-snug font-semibold tracking-[-0.2px] group-data-[size=sm]/card:text-sm", className)}
{...restProps}
>
{@render children?.()}
</div>
src/lib/registry/ui/card/card-description.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
</script>
<p
bind:this={ref}
data-slot="card-description"
class={cn("text-[13px] text-muted-foreground", className)}
{...restProps}
>
{@render children?.()}
</p>
src/lib/registry/ui/card/card-action.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-action"
class={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...restProps}
>
{@render children?.()}
</div>
src/lib/registry/ui/card/card-content.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-content"
class={cn("px-(--card-spacing)", className)}
{...restProps}
>
{@render children?.()}
</div>
src/lib/registry/ui/card/card-footer.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="card-footer"
class={cn("flex items-center gap-2 rounded-b-xl border-t border-border bg-[color-mix(in_srgb,var(--muted)_60%,var(--card))] p-(--card-spacing)", className)}
{...restProps}
>
{@render children?.()}
</div>
src/lib/registry/ui/card/index.ts
import Root from "./card.svelte";
import Action from "./card-action.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 {
Action,
Action as CardAction,
Content,
Content as CardContent,
Description,
Description as CardDescription,
Footer,
Footer as CardFooter,
Header,
Header as CardHeader,
Root,
//
Root as Card,
Title,
Title as CardTitle,
};

See the Elevation guide for the levels, roles and layered mode.

Flat by default (border only). sunken (-1) turns the card into a soft inset well, raised (+1) adds the bevel (inner rim, top highlight, dark hairline) and floating (+2) the bevel plus one soft drop. A card inside a raised or floating surface resolves flat (no bevel on bevel).

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Sunken (-1)

Join MAG4
Magnificent Four, 4 tokens
Amount 1,000 USDC

Flat (0)

Join MAG4
Magnificent Four, 4 tokens
Amount 1,000 USDC

Raised (+1)

Join MAG4
Magnificent Four, 4 tokens
Amount 1,000 USDC

Floating (+2)

Join MAG4
Magnificent Four, 4 tokens
Amount 1,000 USDC

Nested: a card inside a raised card resolves flat

Portfolio
Nested card
Border only, no bevel on bevel.

Sunken (-1)

Join MAG4
Magnificent Four, 4 tokens
Amount 1,000 USDC

Flat (0)

Join MAG4
Magnificent Four, 4 tokens
Amount 1,000 USDC

Raised (+1)

Join MAG4
Magnificent Four, 4 tokens
Amount 1,000 USDC

Floating (+2)

Join MAG4
Magnificent Four, 4 tokens
Amount 1,000 USDC

Nested: a card inside a raised card resolves flat

Portfolio
Nested card
Border only, no bevel on bevel.

Sunken (-1)

Join MAG4

Magnificent Four, 4 tokens

Amount 1,000 USDC

Flat (0)

Join MAG4

Magnificent Four, 4 tokens

Amount 1,000 USDC

Raised (+1)

Join MAG4

Magnificent Four, 4 tokens

Amount 1,000 USDC

Floating (+2)

Join MAG4

Magnificent Four, 4 tokens

Amount 1,000 USDC

Nested: a card inside a raised card resolves flat

Portfolio
Nested card

Border only, no bevel on bevel.

Prop Type Default
size default | sm default
elevation "auto" | "sunken" | "flat" | "raised" | "floating" "auto"

Parts: Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter.

  • Flat (border only); elevation adds the bevel / drop (surface role: raised in layered mode). Footer on a muted tint.
  • Publishes its level to nested cards, which drop to flat.