Built to be mended, not replaced.
Every pack comes with a map of where it was made and a promise to fix it for as long as you own it.
Free returns within 60 days, used or not.
Headline and actions beside a media panel; stacks when narrow, flips with one prop.
HeroSection1 dependency Original, MIT
Every pack comes with a map of where it was made and a promise to fix it for as long as you own it.
Free returns within 60 days, used or not.
Using Claude Code? Give it the agent prompt and skip these steps.
foundation.css holds the tokens every component reads.
---
// src/layouts/Layout.astro
import "../styles/foundation.css";
--- ---
import HeroSplit from "../components/viewership/hero-split/HeroSplit.astro";
---
<HeroSplit /> | Prop | Type | Default | Description |
|---|---|---|---|
title | string | · | Headline. |
accent | string | · | Closing words of the headline, drawn in the theme's accent style. |
body | string | · | Supporting paragraph. |
primary | { label: string; href: string } | · | Main call to action. |
secondary | { label: string; href: string } | · | Text-style second action. |
note | string | · | Small line under the actions. |
reverse | boolean | false | Media on the left. |
as | "h1" | "h2" | "h1" | Heading element. |
slot: media | slot | · | Image or video, cropped to 4:3. |
---
/**
* HeroSplit: copy on one side, media on the other. Stacks on narrow widths.
*
* @example
* <HeroSplit title="Built to be mended," accent="not replaced." primary={{ label: "Shop", href: "/shop" }}>
* <img slot="media" src="/map.webp" alt="" />
* </HeroSplit>
*/
import Button from "../button/Button.astro";
interface Cta {
label: string;
href: string;
}
interface Props {
title: string;
accent?: string;
body?: string;
primary?: Cta;
secondary?: Cta;
/** Small line under the actions: a guarantee, a rating, a note. */
note?: string;
/** Put the media on the left instead. */
reverse?: boolean;
as?: "h1" | "h2";
class?: string;
}
const { title, accent, body, primary, secondary, note, reverse = false, as: Heading = "h1", class: className } = Astro.props;
---
<section class:list={["vs-hero-split", { "is-reverse": reverse }, className]}>
<div class="vs-hero-split__inner">
<div class="vs-hero-split__copy">
<Heading class="vs-display vs-hero-split__title">{title}{accent && <> <em>{accent}</em></>}</Heading>
{body && <p class="vs-hero-split__body">{body}</p>}
{
(primary || secondary) && (
<div class="vs-hero-split__actions">
{primary && <Button href={primary.href} variant="primary">{primary.label}</Button>}
{secondary && <Button href={secondary.href} variant="quiet">{secondary.label}</Button>}
</div>
)
}
{note && <p class="vs-hero-split__note">{note}</p>}
</div>
{
Astro.slots.has("media") && (
<div class="vs-hero-split__media">
<slot name="media" />
</div>
)
}
</div>
</section>
<style>
.vs-hero-split {
container-type: inline-size;
background: var(--vs-page);
color: var(--vs-ink);
font-family: var(--vs-font-sans);
font-stretch: var(--vs-font-stretch);
}
.vs-hero-split__inner {
display: grid;
gap: clamp(32px, 6cqi, 72px);
max-width: var(--vs-container);
margin-inline: auto;
padding: var(--vs-section-y) var(--vs-gutter);
}
.vs-hero-split__title {
font-size: clamp(2.25rem, 7cqi, 4.75rem);
}
.vs-hero-split__body {
max-width: 30rem;
margin: 1.5rem 0 0;
font-size: 1.0625rem;
line-height: 1.55;
color: var(--vs-ink-2);
}
.vs-hero-split__actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 2rem;
}
.vs-hero-split__note {
margin: 1.25rem 0 0;
font-size: 0.8125rem;
color: var(--vs-ink-3);
}
.vs-hero-split__media {
min-width: 0;
aspect-ratio: 4 / 3;
overflow: hidden;
border: var(--vs-border-w) solid var(--vs-line);
border-radius: var(--vs-radius-card);
background: var(--vs-canvas);
box-shadow: var(--vs-pop-shadow, none);
}
.vs-hero-split__media :global(:is(img, video)) {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
@container (min-width: 760px) {
.vs-hero-split__inner {
grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
align-items: center;
}
.is-reverse .vs-hero-split__media {
order: -1;
}
}
</style> Original, MIT.