GODRICH

Hero Section Design: 9 Patterns, No JS Libraries

Hero section design is the layout of the first screen a visitor lands on, and these nine patterns build that screen from plain HTML and CSS with no JS

Auto-plays · click a tile to jump to its section · all nine in one zip

The nine run in the order a first screen is actually read, not in order of flashiness. First the split between words and product (01), then the announcement pill above the headline (02), then the headline selling one sentence (03), then the ground underneath it when there are no photos (04), then the features being pointed at (05), then the numbers backing the claim (06). Once the screen itself is settled: the seam where it hands the visitor to the page body (07), the rotation when several subjects deserve the same spot (08), and what remains when everything dispensable is deleted (09). This edition covers hero layout, whereas the earlier hero animations collection covered how an existing hero moves. Each demo was rendered to 24 frames at 480×300 and measured frame by frame; every count and percentage below is read from those renders, and not one of the nine spilled out of its frame at desktop or phone width.

01Split product hero

An eyebrow pill, a title, a lead line, and two buttons stand on the left while three product cards rise from below in turn, .12s and .24s apart, and the scroll cue at the foot keeps bouncing on its own smaller arc. The layers rise fast over the first 18% of the loop and lower slowly after 60%, and every delayed layer carries animation-fill-mode: backwards so the finished stack never flashes at frame zero. Reach for it on the first screen of a product landing page; it moved in 22 of 24 frames (11.672% cumulative changed area, intensity 95.9), and at 320px it shrinks the art and the 22px rise distance rather than stacking the two columns.

translateYanimation-fill-mode:focus-visible
.hs__art {
  --rise: 22px;
  position: relative;
  width: 156px;
  height: 126px;
}

.hs.is-demo .hs__card {
  animation-name: hsRise;
  animation-duration: $duration;
  animation-timing-function: $easing;
  animation-iteration-count: infinite;
  animation-fill-mode: backwards;
}
.hs.is-demo .hs__card--b { animation-delay: .12s; }
.hs.is-demo .hs__card--a { animation-delay: .24s; }

@keyframes hsRise {
  0%        { transform: translateY(var(--rise)); }
  18%, 60%  { transform: translateY(0); }
  88%, 100% { transform: translateY(var(--rise)); }
}

02Center-stack badge hero

A sheen crosses the news badge once per loop, and below the headline, its lead line, and a solid-and-line button pair the four trust tiles lift 7px and settle, .14s apart. The sheen ends its run parked at translateX(320%), outside an overflow: hidden pill, so the rewind to the left edge never becomes visible. This is the shape for launch announcements and signup screens, and it kept moving in 23 of 24 frames at intensity 74.7 with 3.342% cumulative changed area.

translateXanimation-delay:focus-visible
.hb__sheen {
  position: absolute;
  left: 0;
  top: 0;
  width: 40%;
  height: 100%;
  background: linear-gradient(100deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, .62) 50%, rgba(255, 255, 255, 0) 100%);
  transform: translateX(-160%);
}

@keyframes hbSheen {
  0%        { transform: translateX(-160%); }
  70%, 100% { transform: translateX(320%); }
}

@keyframes hbLift {
  0%        { transform: translateY(0); }
  16%, 46%  { transform: translateY(-7px); }
  88%, 100% { transform: translateY(0); }
}

03Word-by-word headline

The headline refuses to arrive at once: each word rises 115% out of its own overflow: hidden box on a .16s stagger, and once the third word lands, a yellow band .78em tall fills from the left behind the key word. The band rests at scaleX(1) in the static state, so a screenshot taken without the loop still shows the emphasis. Choose it when one sentence carries the whole brand screen; it is among the smallest movers by area at 3.274%, yet its 16 moving frames change hard enough to average intensity 131.3.

scaleXtransform-originanimation-fill-mode
.hk__word {
  position: relative;
  z-index: 1;
  overflow: hidden;
  display: inline-block;
  padding-bottom: $sp-1;
  margin-bottom: -$sp-1;
  vertical-align: bottom;
}
.hk__wi { display: inline-block; }

.hk__key { position: relative; display: inline-block; }
.hk__band {
  position: absolute;
  left: -3px;
  right: -3px;
  bottom: 3px;
  height: .78em;
  border-radius: $r-xs;
  background: $color;
  transform: scaleX(1);
  transform-origin: left center;
}

04Gradient glass hero

Three radial-gradient blobs drift over an ink floor with no photo and no blur filter, since a radial gradient already arrives with soft edges, while a glass card floats above them on backdrop-filter: blur(12px) saturate(140%) and a diagonal beam sweeps its surface once, held inside the card's own corner radius by a clip-path: inset(). It is the widest-moving of the nine at 34.261% cumulative changed area across 19 moving frames, and at the same time the calmest pixel for pixel at intensity 32.2. Paste it onto a service screen that has no image assets at all.

backdrop-filterradial-gradientclip-path
.hg__card {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  width: 100%;
  padding: clamp(12px, 3.2vw, 20px);
  border: 1px solid rgba(255, 255, 255, .28);
  border-radius: $r-card;
  background: rgba(255, 255, 255, .10);
  backdrop-filter: blur(12px) saturate(140%);
  -webkit-backdrop-filter: blur(12px) saturate(140%);
}

.hg__sheen {
  position: absolute;
  inset: 0;
  clip-path: inset(0 round #{$r-card});
  pointer-events: none;
}

05Product zoom hotspot hero

The product art zooms slowly to 1.12 with its origin at 30% 34% and eases back to 1 by 88%, two hotspots swell to 1.28 and burst rings out to 2.8 times their size, and where each burst lands a caption note cuts in on steps(1, end) rather than fading, so no half-transparent ghost of a caption ever exists. The hotspots are real buttons that open their notes on click, which means the same captions work with the autoplay switched off. It belongs on product detail screens that point features out, and it ran 21 of 24 frames at intensity 64.5.

scalesteps(1, end)transform-origin
.hz.is-demo .hz__note {
  visibility: visible;
  animation-duration: $duration;
  animation-timing-function: steps(1, end);
  animation-iteration-count: infinite;
  transition: none;
}

@keyframes hzZoom {
  0%   { transform: scale(1); }
  52%  { transform: scale(1.12); }
  88%, 100% { transform: scale(1); }
}

@keyframes hzNote1 {
  0%   { opacity: 0; }
  26%  { opacity: 1; }
  86%  { opacity: 0; }
  100% { opacity: 0; }
}

06Stat counter hero

Three figures roll seven-digit strips like a slot machine on steps(6, end), so only whole digits ever pass the window while the cells stagger .16s and .32s and the second window of each figure trails a further .08s. When the roll stops at 60% of the loop, an underline fills from the left to mark each value as settled, and font-variant-numeric: tabular-nums keeps the figures from jittering as the digits change. It is the smallest mover by area at 2.145% and the densest by sampling at intensity 147.3, and each value is read out by one aria-label while the rolling digits stay aria-hidden.

steps(6, end)font-variant-numerictranslateY
$hc-cell: 1.15em;            // one digit cell high — the strip rolls only in whole cells

.hc__strip {
  display: block;
  transform: translateY($hc-cell * -6);   // of the seven digits, the last is the settled value
}

.hc.is-demo .hc__strip {
  animation-name: hcRoll;
  animation-duration: $duration;
  animation-timing-function: steps(6, end);
  animation-iteration-count: infinite;
  animation-fill-mode: backwards;
  animation-delay: var(--d);
}

@keyframes hcRoll {
  0%        { transform: translateY(0); }
  60%       { transform: translateY($hc-cell * -6); }
  100%      { transform: translateY($hc-cell * -6); }
}

07Scroll-folding hero

As you scroll, the hero stays pinned while it shrinks to .86 scale and rounds its corners, a dim layer deepens to .28 opacity, and the body card climbs 20px over the hero to cover it. The whole handover is three animations on one named scroll timeline, scroll-timeline-name: --hfscroll on the scroller and animation-timeline: --hfscroll on each piece, run with linear easing and animation-fill-mode: both so scrolling past the end never snaps anything back. It is the seam between the first screen and the page body, and it moved in 23 of 24 frames for 21.196% cumulative changed area.

scroll-timeline-nameanimation-timelineposition: sticky
.hf__scroller {
  --h: clamp(148px, 58vh, 176px);
  position: relative;
  height: var(--h);
  overflow-y: auto;
  overscroll-behavior: contain;
  border-radius: $r-card;
  background: $hr-ink;
  scroll-timeline-name: --hfscroll;
  scroll-timeline-axis: y;
}
.hf__hero {
  position: sticky;
  top: 0;
  z-index: 0;
  display: grid;
  align-content: center;
  height: calc(var(--h) * .76);
  transform-origin: top center;
  animation-name: hfFold;
  animation-duration: 1s;
  animation-timing-function: $easing;
  animation-fill-mode: both;
  animation-timeline: --hfscroll;
}

08Split slider hero

Three slides sit stacked in a single grid cell, and on each change the text pane wipes in from the left while the picture pane wipes in from the right, opposite clip-path insets on layers sharing grid-area: 1 / 1, with the incoming title riding up 10px. Z-order changes only as steps(1, end) cuts, so a frame holding two half-visible slides never exists, and the dot indicators fill on cuts timed to the wipes rather than on the z-order boundaries. With 15 animations it is the busiest build of the nine at 17.644% over 17 moving frames, and the dots are labeled buttons carrying aria-current.

clip-pathgrid-areaaria-current
.hl__slide {
  grid-area: 1 / 1;
  z-index: 1;
  display: grid;
  grid-template-columns: 1fr 38%;
}

.hl__pane {
  box-sizing: border-box;
  transition: clip-path $dur-enter $easing;
}
.hl__pane--copy { clip-path: inset(0 100% 0 0); }
.hl__pane--art  { clip-path: inset(0 0 0 100%); }

@keyframes hlFill {
  0%,     17.99% { transform: scale(0); }
  18%,    51.32% { transform: scale(1); }
  51.33%, 100%   { transform: scale(0); }
}

09Minimal logo hero

Only a 56px badge, one line of copy, a status note, and a single button remain, while two rings ripple out from the mark half a cycle apart (the second at -1s), spreading to 1.9 times their size before they fade. The ring border is 3px rather than 2px because the thinner stroke all but vanished in the 480×300 grid thumbnail, and the loop finishes its return inside 88% so the last sampled frame never catches the reset. Two animations are all it has, 7.483% cumulative area over 23 moving frames, the fewest moving parts of the set; it is the teaser and coming-soon pattern.

scaleopacity:focus-visible
.hm__ring {
  position: absolute;
  inset: 0;
  border: 3px solid $color;
  border-radius: 50%;
  scale: 1;
  opacity: .34;
}
.hm__ring--b { scale: 1.34; opacity: .18; }

@keyframes hmRing {
  0%             { scale: .58; opacity: .62; }
  88%            { scale: 1.9;  opacity: 0; }
  88.01%, 100%   { scale: .58; opacity: 0; }
}

Where it breaks — the trap

The first wall was the scrollport itself. The fold in 07 was first wired to an anonymous scroll(nearest) timeline, and its progress never left 0, because an overflow: hidden ancestor qualifies as a scrollport and the nearest one never scrolled anywhere. Naming the timeline on the real scroller and pointing every animated piece at it by name fixed the fold outright, which is why the source names --hfscroll instead of trusting nearest. Sticky headroom taught the same lesson: padding-bottom on the scroller sits outside the sticky range and buys nothing, so the body card's own height is what gives the hero room to shrink. Two quieter bugs rode along, both caught in the browser: the dim layer covering the hero swallowed the button's clicks until it got pointer-events: none, and the card came out 40px too tall until box-sizing: border-box stopped its padding from adding to min-height.

The second was arithmetic on the dots in 08. Filling a dot at 0% moved the indicator while the outgoing slide was still wiping out, and the natural cut point, 33.33%, sits exactly on a z-order keyframe boundary where a 24-frame sample spaced 4.1667% apart can catch all three dots empty in one frame. The cuts moved to 18% and 51.33%, both of which fall between sample frames, and the all-empty frame stopped existing.

The third is a fight the highlight bar in 03 can never win. The poster frame is picked as the frame that changed most from the one before it, and a band .78em tall filling behind one word loses that contest to words arriving from 115% below .16s apart no matter how its keyframes are tuned, because during the entrance two words are moving at once and they simply cover more ground. The representative frame therefore shows the completed words, and the band's fill is something you watch in the gif.

The fourth was the static state. A counter strip left at translateY(0) shows its first digit, so the grid-card thumbnail and the reduced-motion rendering of 06 would have displayed the starting digits instead of the settled 98, 12, and 24; pinning the resting position to translateY($hc-cell * -6), the seventh cell, makes every state outside the loop read the final figures. The same discipline shows up in 04 in a milder form: the two bright blobs began near the top left where the subline sits and were draining its contrast, so both were pushed down out of the text area, one to the bottom-right corner and the other across the bottom of the right half, and the mist subline kept its 8.65:1. Every corrected value in this section is already baked into the archive that u6yhwkzq opens, so none of it has to be rediscovered the hard way.

Accessibility

Every pairing below was pushed through the WCAG 2.1 relative-luminance arithmetic before it was allowed into a demo, and three pairings were computed, failed, and then barred from use.

Foreground Background Measured ratio Body-text bar
White #ffffff Ink #17141a 18.24:1 Pass
Yellow #ffd23f Ink #17141a 12.63:1 Pass
Lilac #9d93ff Ink #17141a 6.99:1 Pass
Mist #b6b0c0 Ink #17141a 8.65:1 Pass
Ink #17141a Cream #fdf7ec 17.10:1 Pass
Stone #56505e Cream #fdf7ec 7.28:1 Pass
Violet #5b4bd6 Cream #fdf7ec 5.75:1 Pass
White #ffffff Violet #5b4bd6 6.14:1 Pass
White #ffffff Grass #0f7a52 5.35:1 Pass
Ink #17141a Orange #ff4d1f 5.50:1 Pass
White #ffffff Orange #ff4d1f 3.32:1 Fails — never used
White #ffffff Yellow #ffd23f 1.44:1 Fails — never used
Violet #5b4bd6 Ink #17141a 2.97:1 Fails — never used

That is why every word on the orange stages of 05 and 08 is ink, why the yellow stages of 02 and 06 carry ink text only, and why violet never appears as text on ink; lilac does that job instead at 6.99:1. One pairing sits in between: the violet focus ring on the yellow stage of 02 measures 3.28:1, which clears the 3:1 bar for non-text indicators but would fail as copy, so it is reserved for the ring. The 4.5:1 bar itself and the formula behind these numbers are set out in the W3C's contrast guidance.

With prefers-reduced-motion: reduce, each pattern freezes its loop and keeps its information. In 01 the cards and the cue are pinned to transform: none with the copy, buttons, and stacked art untouched. In 02 the sheen is hidden outright and the tiles park at rest, leaving the badge, labels, and both buttons. In 03 the words stop in place and the band is held at scaleX(1), so the key word keeps its highlight. In 04 the blobs and the beam stop dead while the glass card, title, subline, and pill CTA stay exactly as they were, and the pause switch keeps working. In 05 the zoom, pulses, bursts, and note cuts all stop with the notes locked to full opacity, so the captions remain readable. In 06 the strip and underline stop, and because the resting transform is the final digit, the settled values are what you see. In 07 the hero, dim, and card drop their scroll-driven animations, the hero keeps its rounded top corners, and the card sits at transform: none; Firefox, which still keeps scroll-driven animation behind a flag in its stable build, gets the same picture by fallback. In 08 the wipes and z-cuts stop with the current slide fully revealed and its dot filled through aria-current. In 09 the rings hold at 1x and 1.34x with opacity .34 and .18, so the mark still reads as present. Every one of those overrides needs !important, because the autoplay selectors sit at (0, 3, 0) specificity and an unadorned rule inside a media query cannot outvote them.

The markup underneath is meant to be audited, not just admired. Demos 01 through 07 contain exactly one h1 each, 08 titles its three slides as h2 elements, and 09 carries no heading at all, which suits a teaser. Every interactive element, from buttons to hotspots to dots to the pause switch, draws a visible :focus-visible ring; the figures in 06 expose their values through aria-label while the rolling digits stay hidden; the fold in 07 follows MDN's scroll-timeline-name reference and the glass in 04 follows MDN's backdrop-filter page. More first-screen layouts are collected in the hero category, and the about page documents how each demo here is measured before it ships.

FAQ

Do these heroes need JavaScript?

The motion does not. Every loop above is CSS animation, and the scroll-driven fold in 07 is CSS from end to end. Script appears only where a person acts: the replay buttons in 01, 03, and 06, the pause switch in 04, the hotspot toggles in 05, the slide buttons in 08, the arrow-key pair in 02, the jump button in 07, and the notify button in 09. Even with all script stripped out, each pattern still ships a complete static state, because the static rules are written first and the loop is layered over them.

Which pattern fits a page with no images at all?

04 was built for exactly that case: the background is three radial gradients and the card is glass, so the hero makes zero image requests. Beyond it, 03 needs nothing but type, 06 nothing but numbers, and 09 nothing but a mark, which together cover a text-first service page, a stats-led company screen, and a teaser without touching a single asset.

How do I put one of these at the top of my own site?

Lift the pattern's root element out of the demo's .stage wrapper, make it the first element in the page, and let it span the full viewport width; then give it your own height, since the 300px you see here belongs to the preview canvas rather than to the pattern. The narrow-screen rules are written against max-width: 340px and max-height: 220px, so widen those breakpoints to your real phone size before you judge the small-screen result.

Enter the archive password

The password is inside this article. You will find it as you read.