Media
Overview
The Media is a layout primitive used to align visual content (image, avatar, icon) with adjacent text content — the canonical "media object" pattern popularized by Bootstrap. It is composed of four recipe parts: useMediaRecipe() for the flex container, useMediaFigureRecipe() for the visual holder, useMediaBodyRecipe() for the text container, and useMediaTitleRecipe() for the heading. Each composable creates a fully configured recipe with its own variant axes — no color or surface styling, so the Media composes cleanly inside other containers like Card or Callout.
The Media 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 Media recipe?
The Media recipe helps you:
- Ship the canonical media object pattern: Get the image-plus-text layout that powers comments, posts, notifications, and list items out of the box.
- Control orientation and alignment declaratively: Switch between horizontal and vertical layouts, and align the figure to the start, center, or end of the body, all through props.
- Scale typography and spacing together: Pass a single
sizeprop to each part to keep figure radius, body gap, and title font-size in lockstep. - Compose inside other recipes: Layout-only with no color or background means the Media slots into Cards, Callouts, or any container without conflicting styles.
- Stay type-safe: Full TypeScript support means your editor catches invalid orientation, align, or size values at compile time.
- Customize without forking: Override base styles, default variants, or filter out options you don't need — all through the options API.
Usage
Register the recipes
Add the Media 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 {
useMediaRecipe,
useMediaFigureRecipe,
useMediaBodyRecipe,
useMediaTitleRecipe,
} from '@styleframe/theme';
const s = styleframe();
const media = useMediaRecipe(s);
const mediaFigure = useMediaFigureRecipe(s);
const mediaBody = useMediaBodyRecipe(s);
const mediaTitle = useMediaTitleRecipe(s);
export default s;
Build the component
Import the media, mediaFigure, mediaBody, and mediaTitle runtime functions from the virtual module and pass variant props to compute class names:
import { media, mediaFigure, mediaBody, mediaTitle } from "virtual:styleframe";
interface MediaProps {
orientation?: "horizontal" | "vertical";
align?: "start" | "center" | "end";
size?: "sm" | "md" | "lg";
figure?: React.ReactNode;
title?: string;
children?: React.ReactNode;
}
export function Media({
orientation = "horizontal",
align = "start",
size = "md",
figure,
title,
children,
}: MediaProps) {
return (
<div className={media({ orientation, align, size })}>
{figure && (
<div className={mediaFigure({ size })}>{figure}</div>
)}
<div className={mediaBody({ size })}>
{title && <h3 className={mediaTitle({ size })}>{title}</h3>}
{children}
</div>
</div>
);
}
<script setup lang="ts">
import { media, mediaFigure, mediaBody, mediaTitle } from "virtual:styleframe";
const {
orientation = "horizontal",
align = "start",
size = "md",
} = defineProps<{
orientation?: "horizontal" | "vertical";
align?: "start" | "center" | "end";
size?: "sm" | "md" | "lg";
}>();
</script>
<template>
<div :class="media({ orientation, align, size })">
<div :class="mediaFigure({ size })">
<slot name="figure" />
</div>
<div :class="mediaBody({ size })">
<h3 :class="mediaTitle({ size })">
<slot name="title" />
</h3>
<slot />
</div>
</div>
</template>
See it in action
Variants
Two layout axes shape the media object: orientation and align. Together they cover the full set of media-object arrangements without forcing a color or surface choice.
Orientation
Two orientations control how the figure and body are arranged. horizontal places the figure to the left (the default media-object layout); vertical stacks the figure on top of the body, useful for media cards or compact list items where vertical space is plentiful.
Align
Three alignment options control how the figure aligns with the body on the cross-axis. start (the default) aligns the top of the figure with the top of the body, ideal for comments and posts where the avatar should sit next to the title. center vertically centers the figure for compact list items. end aligns the figure to the bottom of the body, useful when the visual is decorative.
| Align | CSS Value | Use Case |
|---|---|---|
start | align-items: flex-start | Default. Comments, posts, list rows where avatar aligns with title |
center | align-items: center | Compact list items, single-line content |
end | align-items: flex-end | Decorative figures, icon-as-suffix layouts |
Sizes
Three size variants from sm to lg scale the gap between figure and body, the figure's border radius, the body's internal gap, and the title's font size in lockstep.
Size Reference
| Size | Container Gap | Figure Radius | Body Gap | Title Font Size |
|---|---|---|---|---|
sm | @0.5 | @border-radius.sm | @0.25 | @font-size.sm |
md | @0.75 | @border-radius.md | @0.375 | @font-size.md |
lg | @1 | @border-radius.lg | @0.5 | @font-size.lg |
size prop must be passed to each sub-recipe individually. The container controls the figure-to-body gap, the figure controls its own border radius, the body controls the title-to-content gap, and the title controls its font size.Anatomy
The Media recipe is composed of four independent recipes that work together to form the layout:
| Part | Recipe | Role |
|---|---|---|
| Container | useMediaRecipe() | Outer flex wrapper with orientation, align, and gap |
| Figure | useMediaFigureRecipe() | Visual holder with flex-shrink: 0 and rounded crop |
| Body | useMediaBodyRecipe() | Text container with flex-grow: 1 and safe min-width: 0 |
| Title | useMediaTitleRecipe() | Heading typography (element-agnostic) |
Each part is a standalone recipe with its own size axis. The container's orientation and align props are independent of the size axis, so you can freely combine all three.
<!-- All four parts working together -->
<div class="media(...)">
<div class="mediaFigure(...)"><!-- avatar / image --></div>
<div class="mediaBody(...)">
<h3 class="mediaTitle(...)">Heading</h3>
<p>Body content</p>
</div>
</div>
mediaTitle class to whichever heading element fits your document outline (<h2>, <h3>, <h4>, or even <strong>). The recipe only sets typography, not semantics.Nesting
Media objects can be nested inside one another to model parent-child relationships such as comment threads, social-post replies, or activity feeds. Place a Media inside another Media's body and the layout naturally indents the child by the figure's width plus the container's gap.
<template>
<Media>
<MediaFigure>
<img src="/avatar/alex.png" alt="Alex Grozav" />
</MediaFigure>
<MediaBody>
<MediaTitle>Alex Grozav</MediaTitle>
<p>Just shipped the new Media recipe...</p>
<!-- Nested reply -->
<Media>
<MediaFigure size="sm">
<img src="/avatar/jane.png" alt="Jane Smith" />
</MediaFigure>
<MediaBody size="sm">
<MediaTitle size="sm">Jane Smith</MediaTitle>
<p>Love it — the layout-only design...</p>
</MediaBody>
</Media>
</MediaBody>
</Media>
</template>
Media directly inside the parent's MediaBody. The parent body is already a flex column with min-width: 0, so child media items wrap correctly without extra wrappers. Reduce the nested media's size (e.g., sm) to visually distinguish replies from the parent thread.Accessibility
- Choose the right heading level.
useMediaTitleRecipeonly styles typography — pick a heading element (<h2>,<h3>,<h4>) that fits the surrounding document outline. Don't skip levels just because the title is small. - Provide alt text for figures. When the figure contains an
<img>, supply a meaningfulaltattribute. Decorative figures should usealt=""andaria-hidden="true"so screen readers skip them. - Group related media in a list. Repeated media items (comments, feed posts) belong inside a
<ul>/<ol>so assistive tech can announce the count. - Don't lose focus order with
verticalorientation. Switching orientation with CSS doesn't reorder the DOM, so the figure is always announced before the body. Place actions and links inside the body for predictable keyboard navigation.
Customization
Overriding Defaults
Each media 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 {
useMediaRecipe,
useMediaFigureRecipe,
useMediaBodyRecipe,
useMediaTitleRecipe,
} from '@styleframe/theme';
const s = styleframe();
const media = useMediaRecipe(s, {
base: {
gap: '@1',
},
defaultVariants: {
orientation: 'horizontal',
align: 'center',
size: 'lg',
},
});
const mediaFigure = useMediaFigureRecipe(s, {
defaultVariants: {
size: 'lg',
},
});
const mediaBody = useMediaBodyRecipe(s, {
defaultVariants: {
size: 'lg',
},
});
const mediaTitle = useMediaTitleRecipe(s, {
base: {
fontWeight: '@font-weight.bold',
},
defaultVariants: {
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 { useMediaRecipe } from '@styleframe/theme';
const s = styleframe();
// Only generate horizontal orientation with start and center alignment
const media = useMediaRecipe(s, {
filter: {
orientation: ['horizontal'],
align: ['start', 'center'],
},
});
export default s;
API Reference
useMediaRecipe(s, options?)
Creates the media container recipe with orientation, alignment, and gap.
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 media container |
options.variants | Variants | Custom variant definitions for the recipe |
options.defaultVariants | Record<keyof Variants, string> | Default variant values for the recipe |
options.filter | Record<string, string[]> | Limit which variant values are generated |
Variants:
| Variant | Options | Default |
|---|---|---|
orientation | horizontal, vertical | horizontal |
align | start, center, end | start |
size | sm, md, lg | md |
useMediaFigureRecipe(s, options?)
Creates the media figure recipe for the visual holder. The base styles set flex-shrink: 0 so the figure never collapses, plus overflow: hidden so contained images crop to the rounded radius.
Variants:
| Variant | Options | Default |
|---|---|---|
size | sm, md, lg | md |
useMediaBodyRecipe(s, options?)
Creates the media body recipe for the text content area. The base styles set flex-grow: 1 and the critical min-width: 0 so long titles wrap correctly inside the parent flex container instead of overflowing.
Variants:
| Variant | Options | Default |
|---|---|---|
size | sm, md, lg | md |
useMediaTitleRecipe(s, options?)
Creates the media title recipe for heading typography. The base styles set semibold weight, snug line height, and zero margin. The recipe is element-agnostic — apply the class to any heading element (<h2>, <h3>, <h4>) that fits your document outline.
Variants:
| Variant | Options | Default |
|---|---|---|
size | sm, md, lg | md |
Best Practices
- Pass
sizeto every part: The container, figure, body, and title each control their own size-scaled property. Pass the samesizevalue to all four for visually consistent results. - Use
align: "start"for comments and posts: Top-aligning the avatar with the title is the most readable pattern when the body has multiple lines of text. Reservecenterfor single-line list items. - Pick a meaningful heading element:
useMediaTitleRecipeonly styles typography — choose<h3>or<h4>based on where the media sits in your document outline. - Use
verticalorientation sparingly: Vertical media is essentially a card-like figure-on-top layout. If your design needs that pattern, also consider whetheruseCardRecipeis a better fit. - Don't add background or border to the Media root: Media is layout-only by design. Wrap it inside a Card, Callout, or other container if you need surface styling — this keeps composition predictable.
- Filter what you don't need: If your component only uses one orientation or alignment, pass a
filteroption to reduce generated CSS. - Reduce
sizefor nested replies: When nesting Media inside Media (comment threads, reply chains), drop the nested media'ssizetosmso the indentation hierarchy reads at a glance.
FAQ
flex-shrink: 0 and rounded clipping, the body needs min-width: 0 for safe text wrapping, and the title needs heading typography. Four separate recipes give each part its own focused base while sharing the size axis. This also means you can use a subset (just the body, just figure-plus-body) without paying for unused styles.orientation controls the main flex axis: horizontal puts the figure to the left of the body, vertical stacks the figure on top. align controls the cross-axis alignment of the figure relative to the body — in horizontal orientation, start aligns the top of the figure with the top of the body; in vertical orientation, start left-aligns the figure within the column. The two axes are independent and can be combined freely.min-width: auto, which means a long unbroken word or a child with white-space: nowrap can force the body to grow beyond its parent and break the layout. Setting min-width: 0 lets the body shrink as needed and lets text wrap correctly. The Media body recipe sets this in its base styles automatically.useCardBodyRecipe to get a card with an image-and-content row, or use multiple Media items as list rows inside a Card. Because Media has no color or background, it will pick up the Card's surface styling without conflicts.Media directly inside another Media's MediaBody; the body is already a flex column with min-width: 0, so the child wraps correctly and inherits the parent body's indentation. Drop the nested media's size to sm to visually distinguish replies from the parent thread. See the Nesting section for an example.filter option, default variants that reference filtered-out values are automatically removed. For example, if you filter orientation to only ['horizontal'], the defaultVariants.orientation is preserved (since horizontal is the default); but if you filter to ['vertical'], the default is removed because horizontal is no longer available.@0.75, @border-radius.md, and @font-size.md through string refs. These tokens need to be defined in your Styleframe instance for the recipes to generate valid CSS. The easiest way is to use useDesignTokensPreset(s), but you can also define the required tokens manually.Hamburger Menu
A three-bar toggle button that animates into a different glyph (X, arrow, plus, or minus) when opened. Supports three colors, three sizes, seven animations, and an active state through the recipe system.
Overview
Explore Styleframe's comprehensive design token system. Create consistent, scalable design systems with composable functions for colors, typography, spacing, and more.