Docs

This page isn't translated yet

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

ComponentLayoutMedia prop
HeroSimpleContent in the container, optional media below it.media (optional)
HeroBackgroundContent over a full-bleed background, with a darkening overlay.background (required)
HeroFeatureMedia 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.

Left-aligned, no media
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.

Dark HeroSimple
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>
  )
}
Reversed 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.

PropTypeDefaultDescription
themeColorSystem (dark, default)-
mediaReactNode-
alignContentfalse | 'left' | 'center' | 'right' | null'left'
prependReactNode-
appendReactNode-
…and all <section> attributes

HeroBackgroundLink to this section

PropTypeDefaultDescription
themeColorSystem (dark, default)-
background (required)ReactNode-
alignContentfalse | 'left' | 'center' | 'right' | null'left'
disableOverlaybooleanfalse
prependReactNode-
appendReactNode-
…and all <section> attributes

HeroFeatureLink to this section

PropTypeDefaultDescription
themeColorSystem (dark, default)-
mediaReactNode-
reversedbooleanfalse
prependReactNode-
appendReactNode-
…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:

ClassStyles
hero-simplepadding-top: var(--hero-simple-margin-top, 0px);background-color: var(--color-hero-simple-bg);
hero-simple-containerdisplay: 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-containerdisplay: 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-lefttext-align: left;margin-right: auto;
hero-background-content-lefttext-align: left;margin-right: auto;
hero-simple-content-centertext-align: center;margin-inline: auto;
hero-background-content-centertext-align: center;margin-inline: auto;
hero-simple-content-righttext-align: right;margin-left: auto;
hero-background-content-righttext-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-backgroundisolation: 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-overlayz-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-coverobject-fit: cover;
object-containobject-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 in HeroSimple and HeroFeature that carries meaning needs a real alt.
  • 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.