Hero
Open a page with HeroSimple, HeroBackground or HeroFeature, with media, alignment, overlays and extra slots.
On this page
The hero is the first band of a page: its title, a short lead and the main calls to action. Systhema ships three layouts. HeroSimple stacks content above optional media, HeroBackground lays content over a full-bleed image or video, and HeroFeature splits the band into media and content halves. The content slot of each is a flex column on the same --typography-block-margin-y rhythm as rich text, so a heading, a paragraph and a row of buttons space themselves.
import { Button, Heading, HeroSimple, Image, Paragraph, Stack } from '@systhemaui/next'
export default function Demo() {
return (
<HeroSimple
alignContent="center"
media={
<Image
src="/demo-assets/mountains.webp"
alt="A mountain range at dusk"
width={1920}
height={1080}
aspectRatio="21/9"
priority
/>
}
>
<Heading.h1>Websites that stay on brand</Heading.h1>
<Paragraph type="lead">
Design tokens, components and a CMS that share one source of truth.
</Paragraph>
<Stack gap="sm" justifyContent="center" wrap>
<Button.a href="#" variant="primary">
Start a project
</Button.a>
<Button.a href="#" variant="secondary">
See our work
</Button.a>
</Stack>
</HeroSimple>
)
}ImportLink to this section
import { HeroBackground, HeroFeature, HeroSimple } from '@systhemaui/next'In a React app without Next.js, import them from @systhemaui/react.
VariantsLink to this section
| Component | Layout | Media prop |
|---|---|---|
HeroSimple | Content in the container, optional media below it. | media (optional) |
HeroBackground | Content over a full-bleed background, with a darkening overlay. | background (required) |
HeroFeature | Media on one half, content on the other; stacked below md. | media (optional) |
Each hero adds its own class to the element you pass as media (hero-simple-media, hero-background-media, hero-feature-media), so pass a single element such as an Image, a Video or a MediaWrapper. All three take theme, prepend and append.
HeroSimpleLink to this section
AlignmentLink to this section
alignContent is left (the default), center or right. It sets the text alignment and pushes the content block to that side.
import { Heading, HeroSimple, Paragraph } from '@systhemaui/next'
export default function Demo() {
return (
<HeroSimple>
<Paragraph type="label">Journal</Paragraph>
<Heading.h1>Notes on building for the long term</Heading.h1>
<Paragraph type="lead">Essays on design systems, content and maintenance.</Paragraph>
</HeroSimple>
)
}ThemeLink to this section
theme switches the hero to another color-system mode. The hero's own background and text colors come from the hero.simple.bg and hero.simple.text color tokens of that mode.
import { Button, Heading, HeroSimple, Paragraph } from '@systhemaui/next'
export default function Demo() {
return (
<HeroSimple theme="dark" alignContent="center">
<Heading.h1>Launch week</Heading.h1>
<Paragraph type="lead">Five days, five releases, one changelog.</Paragraph>
<Paragraph>
<Button.a href="#" variant="primary">
Read the announcement
</Button.a>
</Paragraph>
</HeroSimple>
)
}HeroBackgroundLink to this section
The background fills the band (object-fit: cover), an overlay in --color-hero-background-overlay keeps the text readable, and the band is at least --hero-background-min-height tall (512px when the token is unset).
import { Button, Heading, HeroBackground, Image, Paragraph } from '@systhemaui/next'
export default function Demo() {
return (
<HeroBackground
alignContent="center"
background={
<Image
src="/demo-assets/coast.webp"
alt=""
width={1800}
height={1200}
priority
/>
}
>
<Heading.h1>Where the land ends</Heading.h1>
<Paragraph type="lead">A photo series from the northern coast.</Paragraph>
<Paragraph>
<Button.a href="#" variant="primary">
View the gallery
</Button.a>
</Paragraph>
</HeroBackground>
)
}Without the overlayLink to this section
disableOverlay removes the overlay element. Use it when the image is already dark or the text sits on a calm part of it.
import { Heading, HeroBackground, Image } from '@systhemaui/next'
export default function Demo() {
return (
<HeroBackground
disableOverlay
alignContent="left"
background={
<Image src="/demo-assets/city.webp" alt="" width={1920} height={1080} priority />
}
>
<Heading.h1>City lights</Heading.h1>
</HeroBackground>
)
}A background video works the same way: pass a muted, looping Video as background.
HeroFeatureLink to this section
The media takes one half of the band at full height (at least --hero-feature-media-min-height, 320px when unset) and the content column aligns with the page container on the other half. reversed moves the media to the right from md up.
import { Button, Heading, HeroFeature, Image, Paragraph } from '@systhemaui/next'
export default function Demo() {
return (
<HeroFeature
media={
<Image
src="/demo-assets/workspace.webp"
alt="A desk with a laptop and design sketches"
width={1800}
height={1200}
priority
/>
}
>
<Paragraph type="label">Studio</Paragraph>
<Heading.h1>A small team for big websites</Heading.h1>
<Paragraph type="lead">Strategy, design and development under one roof.</Paragraph>
<Paragraph>
<Button.a href="#" variant="primary">
Work with us
</Button.a>
</Paragraph>
</HeroFeature>
)
}import { Heading, HeroFeature, Image, Paragraph } from '@systhemaui/next'
export default function Demo() {
return (
<HeroFeature
reversed
theme="dark"
media={
<Image
src="/demo-assets/forest-portrait.webp"
alt="Sunlight through a pine forest"
width={1200}
height={1600}
priority
/>
}
>
<Heading.h1>Slow travel</Heading.h1>
<Paragraph type="lead">Routes that take a week and are worth every day.</Paragraph>
</HeroFeature>
)
}Extra slotsLink to this section
prepend renders before the hero's container and append after the content. In HeroSimple the appended node sits inside the container after the media; in HeroBackground and HeroFeature it sits after everything else in the band. Use them for breadcrumbs, a scroll cue or a logo strip without wrapping the hero:
import { Heading, HeroSimple, Paragraph } from '@systhemaui/next'
export function PostHero() {
return (
<HeroSimple append={<Paragraph type="small">Trusted by 120 teams.</Paragraph>}>
<Heading.h1>Field notes</Heading.h1>
</HeroSimple>
)
}PropsLink to this section
HeroSimpleLink to this section
All other props go to the <section> element.
| Prop | Type | Default | Description |
|---|---|---|---|
theme | ColorSystem (dark, default) | - | |
media | ReactNode | - | |
alignContent | false | 'left' | 'center' | 'right' | null | 'left' | |
prepend | ReactNode | - | |
append | ReactNode | - | |
…and all <section> attributes |
HeroBackgroundLink to this section
| Prop | Type | Default | Description |
|---|---|---|---|
theme | ColorSystem (dark, default) | - | |
background (required) | ReactNode | - | |
alignContent | false | 'left' | 'center' | 'right' | null | 'left' | |
disableOverlay | boolean | false | |
prepend | ReactNode | - | |
append | ReactNode | - | |
…and all <section> attributes |
HeroFeatureLink to this section
| Prop | Type | Default | Description |
|---|---|---|---|
theme | ColorSystem (dark, default) | - | |
media | ReactNode | - | |
reversed | boolean | false | |
prepend | ReactNode | - | |
append | ReactNode | - | |
…and all <section> attributes |
HTML and CSSLink to this section
The three heroes in plain HTML:
<section class="hero hero-simple">
<div class="hero-simple-container container">
<div class="hero-simple-content hero-simple-content-center">
<h1 class="color-heading text-h1">Websites that stay on brand</h1>
</div>
<img class="media hero-simple-media aspect-21/9 object-cover" src="/hero.webp" alt="…" />
</div>
</section>
<section class="hero hero-background">
<img class="media hero-background-media" src="/cover.webp" alt="" />
<div class="hero-background-overlay"></div>
<div class="hero-background-container container">
<div class="hero-background-content hero-background-content-center">…</div>
</div>
</section>
<section class="hero hero-feature hero-feature-reversed">
<img class="media hero-feature-media" src="/feature.webp" alt="…" />
<div class="hero-feature-content">…</div>
</section>Spacing comes from the hero.simple, hero.background and hero.feature sizing tokens, colors from the color tokens of the same names:
| Class | Styles |
|---|---|
hero-simple | padding-top: var(--hero-simple-margin-top, 0px);background-color: var(--color-hero-simple-bg); |
hero-simple-container | display: flex;flex-direction: column;row-gap: var(--hero-simple-gap-y, 24px);padding-top: var(--hero-simple-padding-top, 48px);padding-bottom: var(--hero-simple-padding-bottom, 48px); |
hero-background-container | display: flex;flex-direction: column;position: relative;z-index: 15;padding-top: var(--hero-background-padding-top, 48px);padding-bottom: var(--hero-background-padding-bottom, 48px); |
hero-simple-content | & { display: flex; flex-direction: column; row-gap: var(--typography-block-margin-y, 16px); padding-inline: var(--hero-simple-content-padding-x, 48px); max-width: var(--hero-simple-content-max-width, 100%); color: var(--color-hero-simple-text, inherit); }& > *:is(h1, h2, h3, h4, h5, h6, p, ul, ol, li, blockquote, pre, code, span) { color: var(--color-hero-simple-text, inherit); } |
hero-background-content | & { display: flex; flex-direction: column; row-gap: var(--typography-block-margin-y, 16px); padding-inline: var(--hero-background-content-padding-x, 0px); max-width: var(--hero-background-content-max-width, 100%); color: var(--color-hero-background-text, inherit); }& > *:is(h1, h2, h3, h4, h5, h6, p, ul, ol, li, blockquote, pre, code, span) { color: var(--color-hero-background-text, inherit); } |
hero-feature-content | & { display: flex; flex-direction: column; row-gap: var(--typography-block-margin-y, 16px); width: 100%; padding-left: var(--hero-feature-content-padding-center); padding-right: var(--hero-feature-content-padding-side); padding-block: var(--hero-feature-content-padding-y, 48px); margin-top: var(--hero-feature-margin-top, 0px); align-self: stretch; color: var(--color-hero-feature-text, inherit); }+ 3 more rules |
hero-simple-content-left | text-align: left;margin-right: auto; |
hero-background-content-left | text-align: left;margin-right: auto; |
hero-simple-content-center | text-align: center;margin-inline: auto; |
hero-background-content-center | text-align: center;margin-inline: auto; |
hero-simple-content-right | text-align: right;margin-left: auto; |
hero-background-content-right | text-align: right;margin-left: auto; |
hero-simple-media | & { max-width: unset; left: 50%; translate: -50% 0; width: var(--hero-simple-media-width, 100%); }&:is(.media-wrapper) { max-width: unset; left: 50%; translate: -50% 0; width: var(--hero-simple-media-width, 100%); }&:is(.media) { max-width: unset; left: 50%; translate: -50% 0; width: var(--hero-simple-media-width, 100%); } |
hero-background | isolation: isolate;position: relative;align-content: center;padding-top: var(--hero-background-margin-top, 0px);min-height: var(--hero-background-min-height, 512px);background-color: var(--color-hero-background-bg-fallback); |
hero-background-media | & { z-index: 5; position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; border-radius: 0px; box-shadow: none; }& > * { width: 100%; height: 100%; border-radius: 0px; box-shadow: none; }&:is(.media-wrapper) { z-index: 5; position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; border-radius: 0px; box-shadow: none; }+ 3 more rules |
hero-background-overlay | z-index: 10;position: absolute;inset: 0;width: 100%;height: 100%;background-color: var(--color-hero-background-overlay, rgba(0, 0, 0, 0.5)); |
hero-feature | & { width: 100%; display: flex; flex-direction: column; background-color: var(--color-hero-feature-bg); }@media (width >= 640px) { & { flex-direction: row; justify-content: flex-start; } }@media (width >= 640px) { &.hero-feature-reversed { flex-direction: row-reverse; } }+ 1 more rule |
hero-feature-media | & { flex-grow: 0; flex-shrink: 0; display: flex; width: 100%; min-height: var(--hero-feature-media-min-height, 320px); height: unset; border-radius: 0; box-shadow: none; }@media (width >= 640px) { & { width: 50%; } }&:not(.object-cover):not(.object-contain) { object-fit: cover; }&:is(.media-wrapper) { flex-grow: 0; flex-shrink: 0; display: flex; width: 100%; min-height: var(--hero-feature-media-min-height, 320px); height: unset; border-radius: 0; box-shadow: none; }+ 5 more rules |
object-cover | object-fit: cover; |
object-contain | object-fit: contain; |
hero-feature-reversed | @media (width >= 640px) { .hero-feature.hero-feature-reversed { flex-direction: row-reverse; } }@media (width >= 640px) { .hero-feature.hero-feature-reversed .hero-feature-content { margin-right: 0; margin-left: var(--container-margin); padding-left: var(--hero-feature-content-padding-side); padding-right: var(--hero-feature-content-padding-center); } } |
Next.jsLink to this section
@systhemaui/next re-exports the three heroes from @systhemaui/react unchanged. Pass the @systhemaui/next Image as media or background to get next/image, and set priority on it: the hero image is usually the page's largest contentful paint.
AccessibilityLink to this section
- Give each page one title, as a real
<Heading.h1>in the hero. The page templates do this. - A background image is decoration behind the title, so give it
alt="". Media inHeroSimpleandHeroFeaturethat carries meaning needs a realalt. - The overlay exists for text contrast. With
disableOverlay, check the contrast of the title against the image yourself. - Hero content never animates in on scroll: core cancels reveal animations inside the content slot, so the title is visible on first paint.
See Headings.