Card
Overview
The Card is a structured container element used for grouping related content into a visually distinct surface. It is composed of four recipe parts: useCardRecipe() for the container, useCardHeaderRecipe() for the top section with a bottom separator, useCardBodyRecipe() for the main content area, and useCardFooterRecipe() for the bottom section with a top separator. Each composable creates a fully configured recipe with color, variant, and size options — plus compound variants that handle the color-variant combinations automatically.
The Card recipes integrate directly with the default design tokens preset and generate type-safe utility classes at build time with zero runtime CSS.
Why use the Card recipe?
The Card recipe helps you:
- Ship faster with sensible defaults: Get 3 colors, 4 visual styles, and 3 sizes out of the box with a single set of composable calls.
- Compose structured layouts: Four coordinated recipes (container, header, body, footer) share the same variant axes, so your cards stay internally consistent.
- Maintain consistency: Compound variants ensure every color-variant combination follows the same design rules, including separator colors and dark mode overrides.
- Customize without forking: Override base styles, default variants, or filter out options you don't need — all through the options API.
- Stay type-safe: Full TypeScript support means your editor catches invalid color, variant, or size values at compile time.
- Integrate with your tokens: Every value references the design tokens preset, so theme changes propagate automatically.
Usage
Register the recipes
Add the Card recipes to a local Styleframe instance. The global styleframe.config.ts provides design tokens and utilities, while the component-level file registers the recipes themselves:
import { styleframe } from 'virtual:styleframe';
import {
useCardRecipe,
useCardHeaderRecipe,
useCardBodyRecipe,
useCardFooterRecipe,
} from '@styleframe/theme';
const s = styleframe();
const card = useCardRecipe(s);
const cardHeader = useCardHeaderRecipe(s);
const cardBody = useCardBodyRecipe(s);
const cardFooter = useCardFooterRecipe(s);
export default s;
Build the component
Import the card, cardHeader, cardBody, and cardFooter runtime functions from the virtual module and pass variant props to compute class names:
import { card, cardHeader, cardBody, cardFooter } from "virtual:styleframe";
interface CardProps {
color?: "light" | "dark" | "neutral";
variant?: "solid" | "outline" | "soft" | "subtle";
size?: "sm" | "md" | "lg";
title?: string;
description?: string;
footer?: React.ReactNode;
children?: React.ReactNode;
}
export function Card({
color = "neutral",
variant = "solid",
size = "md",
title,
description,
footer,
children,
}: CardProps) {
return (
<div className={card({ color, variant, size })}>
{title && (
<div className={cardHeader({ color, variant, size })}>
<strong>{title}</strong>
</div>
)}
<div className={cardBody({ size })}>
{description && <p>{description}</p>}
{children}
</div>
{footer && (
<div className={cardFooter({ color, variant, size })}>
{footer}
</div>
)}
</div>
);
}
<script setup lang="ts">
import { card, cardHeader, cardBody, cardFooter } from "virtual:styleframe";
const {
color = "neutral",
variant = "solid",
size = "md",
} = defineProps<{
color?: "light" | "dark" | "neutral";
variant?: "solid" | "outline" | "soft" | "subtle";
size?: "sm" | "md" | "lg";
}>();
</script>
<template>
<div :class="card({ color, variant, size })">
<div :class="cardHeader({ color, variant, size })">
<slot name="header" />
</div>
<div :class="cardBody({ size })">
<slot />
</div>
<div :class="cardFooter({ color, variant, size })">
<slot name="footer" />
</div>
</div>
</template>
See it in action
Colors
The Card recipe includes 3 color variants: light, dark, and neutral. Unlike semantic component recipes (Badge, Button, Callout), the Card uses neutral-spectrum colors designed for content surfaces rather than status communication. Each color is combined with every visual style variant through compound variants, so you get consistent, predictable styling across all combinations — including dark mode overrides.
The neutral color adapts automatically: it uses a light appearance in light mode and a dark appearance in dark mode, making it the safest default for general-purpose cards.
Color Reference
| Color | Token | Use Case |
|---|---|---|
light | @color.white / @color.gray-* | Light surfaces, stays light in dark mode |
dark | @color.gray-900 | Dark surfaces, stays dark in light mode |
neutral | Adaptive (light ↔ dark) | Default color, adapts to the current color scheme |
neutral as your default card color. It adapts automatically to the user's color scheme, so you don't need to manage light and dark variants separately.Variants
Four visual style variants control how the card is rendered. Each variant is combined with the selected color through compound variants, so you always get the correct background, text, border, and separator colors for your chosen color.
Solid
Filled background with a subtle border. The most prominent style, ideal for primary content areas and featured sections.
Outline
Transparent background with a colored border. Useful for secondary content groups that shouldn't dominate the visual hierarchy.
Soft
Light tinted background with no visible border. A gentle, borderless style that works well for grouped content in dense layouts.
Subtle
Light tinted background with a matching border. Combines the softness of the soft variant with the definition of outline.
Sizes
Three size variants from sm to lg control the border radius of the card container and the padding and gap of the header, body, and footer sections.
Size Reference
| Size | Border Radius | Header/Footer Padding (V / H) | Body Padding (V / H) | Gap |
|---|---|---|---|---|
sm | @border-radius.sm | @0.5 / @0.75 | @0.5 / @0.75 | @0.375 – @0.5 |
md | @border-radius.md | @0.75 / @1 | @0.75 / @1 | @0.5 – @0.75 |
lg | @border-radius.lg | @1 / @1.25 | @1 / @1.25 | @0.75 – @1 |
size prop must be passed to each sub-recipe individually. The card container controls the border radius, while the header, body, and footer control their own padding and gap.Anatomy
The Card recipe is composed of four independent recipes that work together to form a structured layout:
| Part | Recipe | Role |
|---|---|---|
| Container | useCardRecipe() | Outer wrapper with background, border, border radius, and shadow |
| Header | useCardHeaderRecipe() | Top section with a bottom separator border |
| Body | useCardBodyRecipe() | Main content area with vertical flex layout |
| Footer | useCardFooterRecipe() | Bottom section with a top separator border |
Each part is a standalone recipe with its own set of variants. The color and variant props should be passed consistently to the container, header, and footer so that separator border colors match the card's visual style. The body recipe only requires a size prop for padding.
<!-- All four parts working together -->
<div class="card(...)">
<div class="cardHeader(...)">Header content</div>
<div class="cardBody(...)">Body content</div>
<div class="cardFooter(...)">Footer content</div>
</div>
Accessibility
Cards are presentational containers. Follow these guidelines to ensure the content inside them is accessible to everyone.
Use semantic HTML
Cards should use appropriate HTML elements for their content. Use heading elements (<h2>, <h3>, etc.) for card titles, <p> for descriptions, and <button> or <a> for interactive elements inside the card.
<!-- Correct: semantic elements inside the card -->
<div class="...">
<div class="...">
<h3>Project Status</h3>
</div>
<div class="...">
<p>The deployment completed successfully.</p>
</div>
</div>
<!-- Avoid: no semantic structure -->
<div class="...">
<div class="...">
<span>Project Status</span>
<span>The deployment completed successfully.</span>
</div>
</div>
Heading levels
Card titles should follow the document heading hierarchy. If a card appears inside a section with an <h2> heading, the card title should use <h3>. Skipping heading levels (e.g., jumping from <h2> to <h4>) creates a confusing experience for screen reader users navigating by headings.
Interactive cards
If the entire card is clickable (e.g., linking to a detail page), wrap it in an <a> or <button> element or use role="link" with a keyboard handler. Ensure the interactive target has an accessible name, either from the card's title text or an aria-label.
<!-- Correct: clickable card with accessible name from title -->
<a href="/project/123" class="...">
<div class="..."><h3>Project Alpha</h3></div>
<div class="..."><p>Last updated 2 hours ago</p></div>
</a>
Contrast ratios
The solid variant with the dark color places light text on a dark background. All default color token combinations meet the WCAG AA minimum contrast ratio of 4.5:1 for normal text. If you override the default colors, verify contrast ratios with a tool like the WebAIM Contrast Checker.
Customization
Overriding Defaults
Each card composable accepts an optional second argument to override any part of the recipe configuration. Overrides are deep-merged with the defaults, so you only need to specify the properties you want to change:
import { styleframe } from 'virtual:styleframe';
import {
useCardRecipe,
useCardHeaderRecipe,
useCardBodyRecipe,
useCardFooterRecipe,
} from '@styleframe/theme';
const s = styleframe();
const card = useCardRecipe(s, {
base: {
borderRadius: '@border-radius.lg',
boxShadow: '@box-shadow.md',
},
defaultVariants: {
color: 'neutral',
variant: 'outline',
size: 'lg',
},
});
const cardHeader = useCardHeaderRecipe(s, {
defaultVariants: {
color: 'neutral',
variant: 'outline',
size: 'lg',
},
});
const cardBody = useCardBodyRecipe(s, {
defaultVariants: {
size: 'lg',
},
});
const cardFooter = useCardFooterRecipe(s, {
defaultVariants: {
color: 'neutral',
variant: 'outline',
size: 'lg',
},
});
export default s;
Filtering Variants
If you only need a subset of the available variants, use the filter option to limit which values are generated. This reduces the output CSS and keeps your component API focused:
import { styleframe } from 'virtual:styleframe';
import { useCardRecipe } from '@styleframe/theme';
const s = styleframe();
// Only generate neutral color with solid and outline styles
const card = useCardRecipe(s, {
filter: {
color: ['neutral'],
variant: ['solid', 'outline'],
},
});
export default s;
API Reference
useCardRecipe(s, options?)
Creates the card container recipe with background, border, border radius, and shadow styling.
Parameters:
| Parameter | Type | Description |
|---|---|---|
s | Styleframe | The Styleframe instance |
options | DeepPartial<RecipeConfig> | Optional overrides for the recipe configuration |
options.base | VariantDeclarationsBlock | Custom base styles for the card container |
options.variants | Variants | Custom variant definitions for the recipe |
options.defaultVariants | Record<keyof Variants, string> | Default variant values for the recipe |
options.compoundVariants | CompoundVariant[] | Custom compound variant definitions for the recipe |
options.filter | Record<string, string[]> | Limit which variant values are generated |
Variants:
| Variant | Options | Default |
|---|---|---|
color | light, dark, neutral | neutral |
variant | solid, outline, soft, subtle | solid |
size | sm, md, lg | md |
useCardHeaderRecipe(s, options?)
Creates the card header recipe with a bottom separator border. Accepts the same parameters and variant axes as useCardRecipe.
Variants:
| Variant | Options | Default |
|---|---|---|
color | light, dark, neutral | neutral |
variant | solid, outline, soft, subtle | solid |
size | sm, md, lg | md |
useCardBodyRecipe(s, options?)
Creates the card body recipe for the main content area. Accepts the same parameters as useCardRecipe.
Variants:
| Variant | Options | Default |
|---|---|---|
color | light, dark, neutral | neutral |
variant | solid, outline, soft, subtle | solid |
size | sm, md, lg | md |
useCardFooterRecipe(s, options?)
Creates the card footer recipe with a top separator border. Accepts the same parameters and variant axes as useCardRecipe.
Variants:
| Variant | Options | Default |
|---|---|---|
color | light, dark, neutral | neutral |
variant | solid, outline, soft, subtle | solid |
size | sm, md, lg | md |
Best Practices
- Pass
colorandvariantconsistently: The container, header, and footer all need the samecolorandvariantvalues so that separator borders match the card's visual style. - Pass
sizeto each sub-recipe: The card container controls the border radius, but each section (header, body, footer) manages its own padding and gap based on thesizeprop. - Use
neutralfor general-purpose cards: The neutral color adapts to light and dark mode automatically, making it the safest default. - Prefer
solidoroutlinefor primary content: Reservesoftandsubtlefor secondary or nested cards to create visual hierarchy. - Don't use all sections if you don't need them: A card with only a body is perfectly valid. Add headers and footers only when your content has distinct sections.
- Filter what you don't need: If your component only uses one color, pass a
filteroption to reduce generated CSS. - Override defaults at the recipe level: Set your most common variant combination as
defaultVariantsso component consumers write less code.
FAQ
useCardRecipe() with a useCardBodyRecipe() for content. Add the header and footer recipes only when your design calls for separated sections with visible dividers.light, dark, and neutral to provide surface variations that work across all content types without implying a specific status.light always uses white and gray-100 backgrounds regardless of the color scheme. dark always uses gray-800 and gray-900 backgrounds. neutral adapts to the current color scheme: it appears light in light mode and dark in dark mode. Use neutral when you want the card to blend naturally with the surrounding interface.subtle also adds a matching border, giving the card more visual definition. Use soft when you want a borderless, gentler appearance, and subtle when the card needs slightly more structure.The Card recipe uses compound variants to map each color-variant combination to specific styles. For example, when color is neutral and variant is solid, the compound variant applies background: @color.white, color: @color.text, and borderColor: @color.gray-200, along with dark mode overrides. The header and footer recipes use compound variants to set separator border colors that match the card's visual style. This approach keeps the individual color and variant definitions clean while handling all 12 combinations (3 colors × 4 variants) automatically.
filter option, compound variants that reference filtered-out values are automatically removed. For example, if you filter variant to only ['solid', 'outline'], all compound variants matching soft or subtle are excluded from the generated output. Default variants are also adjusted if they reference a removed value.@color.white, @border-radius.md, and @box-shadow.sm through string refs. These tokens need to be defined in your Styleframe instance for the recipe to generate valid CSS. The easiest way is to use useDesignTokensPreset(s), but you can also define the required tokens manually.Callout
A contextual feedback component for alerts, notifications, and inline messages. Supports multiple colors, visual styles, sizes, and orientations through the recipe system.
Overview
Explore Styleframe's utility composables for generating CSS utility classes. Create flexible, reusable styling primitives with full type safety.