Composables

Card

A structured container component for grouping related content with header, body, and footer sections. Supports multiple colors, visual styles, and sizes through the recipe system.

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:

src/components/card.styleframe.ts
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:

src/components/Card.tsx
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>
    );
}

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

ColorTokenUse Case
light@color.white / @color.gray-*Light surfaces, stays light in dark mode
dark@color.gray-900Dark surfaces, stays dark in light mode
neutralAdaptive (light ↔ dark)Default color, adapts to the current color scheme
Pro tip: Use 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

SizeBorder RadiusHeader/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
Good to know: The 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:

PartRecipeRole
ContaineruseCardRecipe()Outer wrapper with background, border, border radius, and shadow
HeaderuseCardHeaderRecipe()Top section with a bottom separator border
BodyuseCardBodyRecipe()Main content area with vertical flex layout
FooteruseCardFooterRecipe()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>
Pro tip: You don't have to use all four parts. A card with only a body is perfectly valid for simple content blocks. Add the header and footer only when your content needs distinct sections.

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.

Good practice: When cards appear in a grid or list, ensure each card has a clear heading so users can scan the page structure. Screen reader users often navigate by headings to find the content they need.

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:

src/components/card.styleframe.ts
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:

src/components/card.styleframe.ts
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;
Good to know: Filtering also removes compound variants and adjusts default variants that reference filtered-out values, so your recipe stays consistent.

API Reference

useCardRecipe(s, options?)

Creates the card container recipe with background, border, border radius, and shadow styling.

Parameters:

ParameterTypeDescription
sStyleframeThe Styleframe instance
optionsDeepPartial<RecipeConfig>Optional overrides for the recipe configuration
options.baseVariantDeclarationsBlockCustom base styles for the card container
options.variantsVariantsCustom variant definitions for the recipe
options.defaultVariantsRecord<keyof Variants, string>Default variant values for the recipe
options.compoundVariantsCompoundVariant[]Custom compound variant definitions for the recipe
options.filterRecord<string, string[]>Limit which variant values are generated

Variants:

VariantOptionsDefault
colorlight, dark, neutralneutral
variantsolid, outline, soft, subtlesolid
sizesm, md, lgmd

useCardHeaderRecipe(s, options?)

Creates the card header recipe with a bottom separator border. Accepts the same parameters and variant axes as useCardRecipe.

Variants:

VariantOptionsDefault
colorlight, dark, neutralneutral
variantsolid, outline, soft, subtlesolid
sizesm, md, lgmd

useCardBodyRecipe(s, options?)

Creates the card body recipe for the main content area. Accepts the same parameters as useCardRecipe.

Variants:

VariantOptionsDefault
colorlight, dark, neutralneutral
variantsolid, outline, soft, subtlesolid
sizesm, md, lgmd

useCardFooterRecipe(s, options?)

Creates the card footer recipe with a top separator border. Accepts the same parameters and variant axes as useCardRecipe.

Variants:

VariantOptionsDefault
colorlight, dark, neutralneutral
variantsolid, outline, soft, subtlesolid
sizesm, md, lgmd

Learn more about recipes →

Best Practices

  • Pass color and variant consistently: The container, header, and footer all need the same color and variant values so that separator borders match the card's visual style.
  • Pass size to each sub-recipe: The card container controls the border radius, but each section (header, body, footer) manages its own padding and gap based on the size prop.
  • Use neutral for general-purpose cards: The neutral color adapts to light and dark mode automatically, making it the safest default.
  • Prefer solid or outline for primary content: Reserve soft and subtle for 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 filter option to reduce generated CSS.
  • Override defaults at the recipe level: Set your most common variant combination as defaultVariants so component consumers write less code.

FAQ