Text with Media
Two-column section pairing copy with an image or video. Sides can be flipped.
Compose
Blocks, not lock-in
Every block ships its own source. Copy it, edit it, own it.
media slot
import { TextWithMedia } from '@/foundations/blocks/text-with-media';
import { Button } from '@/components/button';
export default function TextWithMediaPreview() {
return (
<TextWithMedia
eyebrow="Compose"
title="Blocks, not lock-in"
description="Every block ships its own source. Copy it, edit it, own it."
action={<Button>Browse the catalog</Button>}
media={
<div className="grid aspect-video w-full place-items-center text-foreground-secondary/40 text-sm">
media slot
</div>
}
/>
);
}
export const meta = {
layout: 'fullscreen',
}; Dependencies
Source Code
import { Container } from '@/components/container';
import { cn } from '@/lib/utils/classnames';
interface TextWithMediaProps
extends Omit<React.ComponentPropsWithRef<'section'>, 'title'> {
eyebrow?: React.ReactNode;
title: React.ReactNode;
description?: React.ReactNode;
action?: React.ReactNode;
/** Media slot — image, video, or any node. Rendered in the opposite half. */
media: React.ReactNode;
/** Flip the column order — media on the right by default, on the left when reversed. */
reversed?: boolean;
}
const TextWithMedia = ({
ref,
className,
eyebrow,
title,
description,
action,
media,
reversed,
...rest
}: TextWithMediaProps) => (
<section
ref={ref}
className={cn('w-full py-16 md:py-24', className)}
{...rest}
>
<Container>
<div
className={cn(
'flex flex-col gap-10 lg:flex-row lg:items-center lg:gap-16',
reversed && 'lg:flex-row-reverse'
)}
>
<div className="flex flex-col gap-4 lg:w-1/2">
{eyebrow && (
<p className="font-medium text-foreground-secondary text-sm uppercase tracking-wider">
{eyebrow}
</p>
)}
<h3 className="text-balance text-4xl md:text-5xl">{title}</h3>
{description && (
<p className="text-pretty text-2xl text-foreground-secondary">
{description}
</p>
)}
{action && <div className="mt-2">{action}</div>}
</div>
<div className="lg:w-1/2">
<div className="overflow-hidden rounded-xl bg-surface-sunken">
{media}
</div>
</div>
</div>
</Container>
</section>
);
export type { TextWithMediaProps };
export { TextWithMedia }; TextWithMedia puts a column of copy beside a column of media. Use reversed
to alternate sides between consecutive blocks — the signature “zig-zag” pattern
on long marketing pages.
Anatomy
<TextWithMedia
eyebrow="Compose"
title="Blocks, not lock-in"
description="Every block ships its own source. Copy it, edit it, own it."
media={<img src="..." alt="" />}
/>
API Reference
Extends the section element.
| Prop | Default | Type |
|---|---|---|
eyebrow | - | ReactNode |
title * | - | ReactNode |
description | - | ReactNode |
action | - | ReactNode |
media * | - | ReactNode |
reversed | - | boolean |
Examples
Default
Compose
Blocks, not lock-in
Every block ships its own source. Copy it, edit it, own it.
media slot
import { TextWithMedia } from '@/foundations/blocks/text-with-media';
import { Button } from '@/components/button';
export default function TextWithMediaPreview() {
return (
<TextWithMedia
eyebrow="Compose"
title="Blocks, not lock-in"
description="Every block ships its own source. Copy it, edit it, own it."
action={<Button>Browse the catalog</Button>}
media={
<div className="grid aspect-video w-full place-items-center text-foreground-secondary/40 text-sm">
media slot
</div>
}
/>
);
}
export const meta = {
layout: 'fullscreen',
}; Previous
Steps
Next
Changelog