blocksection
Hero — split
Copy on one side, media on the other. Reading order stays copy-first on every screen size.
Also ships a motion variant: Hero — split (motion). The static one is the default.
Preview
Installation
pnpm dlx shadcn@latest add https://facadeui.dev/r/hero-split.jsonPulls in container, cta-group, eyebrow, heading, section, types, utils. The CLI installs them for you.
Usage
The exact source of the preview above.
import { HeroSplit } from "@registry/sections/hero-split"
import { ACTIONS, MediaPlaceholder } from "./content"
export function Demo() {
return (
<HeroSplit
eyebrow="Sections"
title="Build the page, not the primitives"
description="Every section takes typed content and slots, so assembling a landing page is composition rather than another round of copy-paste."
actions={ACTIONS}
note="Works in any React 19 project."
media={<MediaPlaceholder label="Product screenshot" />}
headingLevel={1}
spacing="md"
/>
)
}Source
What the CLI copies into your project, byte for byte.
/**
* HeroSplit — copy on one side, media on the other.
*
* `media` is a plain `ReactNode` slot rather than an image prop, because a split
* hero's media is a one-off: a screenshot, a video, an illustration, a live
* demo. Passing `<Image … />` straight in is simpler than any indirection this
* file could offer, and it keeps the section free of framework imports.
*
* a11y: on narrow screens the copy comes first in both DOM and visual order, so
* reading order and tab order agree. `reverse` swaps the columns on large
* screens only, with `lg:order-*`, which keeps that guarantee intact — reversing
* the DOM instead would put the media ahead of the headline for screen readers.
*
* Dependencies: react, @/lib/types, @/lib/utils,
* @/components/ui/container, @/components/ui/cta-group, @/components/ui/eyebrow,
* @/components/ui/heading, @/components/ui/section.
*/
import type { ElementType, ReactNode } from "react"
import type {
CtaItem,
HeadingLevel,
LinkComponent,
StackSlotProps,
} from "@/lib/types"
import { cn, slugId } from "@/lib/utils"
import { Container } from "@/components/ui/container"
import { CtaGroup } from "@/components/ui/cta-group"
import { Eyebrow } from "@/components/ui/eyebrow"
import { Heading } from "@/components/ui/heading"
import { Section } from "@/components/ui/section"
export interface HeroSplitProps extends StackSlotProps {
title: string
description?: string
eyebrow?: ReactNode
actions?: CtaItem[]
link?: LinkComponent
note?: ReactNode
banner?: ReactNode
/** The right-hand column. Pass `<Image />`, a video, or anything else. */
media?: ReactNode
/** Extra content under the buttons — a logo strip, a stat row. */
children?: ReactNode
/** Puts the media on the left at `lg` and up. Does not change DOM order. */
reverse?: boolean
headingLevel?: HeadingLevel
size?: "md" | "lg"
as?: "section" | "div"
spacing?: "sm" | "md" | "lg" | "none"
className?: string
id?: string
}
const titleSizes = { md: "text-display-sm", lg: "text-display-md" } as const
export function HeroSplit({
title,
description,
eyebrow,
actions,
link,
note,
banner,
media,
children,
reverse = false,
stackAs,
blockAs,
headingLevel = 1,
size = "lg",
as = "div",
spacing = "lg",
className,
id,
}: HeroSplitProps) {
const Stack = (stackAs ?? "div") as ElementType
const Block = (blockAs ?? "div") as ElementType
const titleId = id ? `${id}-title` : slugId(title)
return (
<Section
as={as}
spacing={spacing}
labelledBy={as === "section" ? titleId : undefined}
id={id}
className={className}
>
<Container className="grid items-center gap-12 lg:grid-cols-2 lg:gap-16">
<Stack className={cn("flex flex-col gap-6", reverse && "lg:order-2")}>
{banner ? <Block>{banner}</Block> : null}
{eyebrow ? (
<Block>
<Eyebrow tone="primary">{eyebrow}</Eyebrow>
</Block>
) : null}
<Block>
<Heading
level={headingLevel}
id={titleId}
className={cn("text-balance font-semibold", titleSizes[size])}
>
{title}
</Heading>
</Block>
{description ? (
<Block>
<p className="text-muted-foreground text-pretty text-lg">{description}</p>
</Block>
) : null}
{actions?.length ? (
<Block>
<CtaGroup items={actions} link={link} size="lg" stackOnMobile />
</Block>
) : null}
{note ? (
<Block>
<p className="text-muted-foreground text-pretty text-sm">{note}</p>
</Block>
) : null}
{children ? <Block>{children}</Block> : null}
</Stack>
{media ? (
<div className={cn("min-w-0", reverse && "lg:order-1")}>{media}</div>
) : null}
</Container>
</Section>
)
}Props
HeroSplitProps
Extends StackSlotProps.
| Prop | Type | Default |
|---|---|---|
title*Required | string | — |
description | string | — |
eyebrow | ReactNode | — |
actions | CtaItem[] | — |
link | LinkComponent | — |
note | ReactNode | — |
banner | ReactNode | — |
mediaThe right-hand column. Pass `<Image />`, a video, or anything else. | ReactNode | — |
childrenExtra content under the buttons — a logo strip, a stat row. | ReactNode | — |
reversePuts the media on the left at `lg` and up. Does not change DOM order. | boolean | false |
headingLevel | HeadingLevel | 1 |
size | "md" | "lg" | "lg" |
as | "section" | "div" | "div" |
spacing | "sm" | "md" | "lg" | "none" | "lg" |
className | string | — |
id | string | — |