GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 CSS Hero Section Animations You Can Copy-Paste

A hero section is the big banner filling a landing page's first screen — headline, text, button.

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

These nine are ordered by what a visitor's eye actually meets on a fresh load, not by popularity. 01 through 03 own the very first screen — the headline, the background drifting behind it, and the CTA that has to keep holding attention. 04 and 05 build trust with a product mockup and a badge, 06 is the partner-logo strip most hero sections tuck just under the fold, and the last three (07-09) react to something happening — scrolling past, or a toast loading in. Rotating that first screen through several slides is framed by the pure CSS carousels, and showing the order of the story that follows is filled in by the timeline animations.

01Headline stagger in

What you see

The three lines of a hero headline don't land together — each one fades and lifts into place a beat after the one before it, like someone typing out the pitch line by line.

Headline stagger in
Open in a new tab

01 · Headline stagger in — Measured area 13.299% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · See the password

@keyframesopacity · translateYstagger delay

How it works

Every line has its own @keyframes, and only the entrance point (10%, 20%, 30%) shifts between them, so the whole stagger runs on nothing but opacity and translateY.

.hero__line { opacity: 0; transform: translateY($sp-6); }

.hero__line--1 { animation: line-in-1 $duration $easing infinite; }

@keyframes line-in-1 {
  0%   { opacity: 0; transform: translateY($sp-6); }
  10%  { opacity: 1; transform: translateY(0); }
  85%  { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY($sp-6); }
}

02Gradient background drift

Where a css hero background animation fits

Behind the card sits a conic-gradient layer more than twice the card's own size, held in place only by overflow: hidden on the card that clips it.

Gradient background drift
Open in a new tab

02 · Gradient background drift — Measured area 23.185% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

::beforeconic-gradienttranslate · rotate

How to wire it up

Just translate and rotate move it — never background-position — so the gradient never has to be recalculated as it drifts.

.hero::before {
  content: '';
  position: absolute;
  inset: -60% -60%;
  background: conic-gradient(from 0deg, #ff477e, #7b2ff7, #4cc9f0, #ff477e);
  opacity: .55;
}
.stage.is-demo .hero::before { animation: drift $duration $easing infinite; }

@keyframes drift {
  0%   { transform: translate(-6%, -4%) rotate(0deg); }
  50%  { transform: translate(6%, 5%) rotate(180deg); }
  100% { transform: translate(-6%, -4%) rotate(360deg); }
}

03CTA pulse glow

What moves

The button breathes: scale and the box-shadow glow's blur radius and opacity grow and shrink together on the same loop.

CTA pulse glow
Open in a new tab

03 · CTA pulse glow — Measured area 8.864% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

box-shadowscale:hover · :focus-visible

The core of it

That loop is decoration only — the button still lifts on a genuine :hover and outlines on :focus-visible, each running its own, much quicker transition underneath.

.cta {
  box-shadow: 0 0 0 0 rgba(255, 224, 102, 0);
  transition: transform $dur-quick $ease-out;
}
.cta:hover { transform: translateY(-2px); }
.cta:focus-visible { outline: 2px solid $subject-ink; outline-offset: 2px; }

.stage.is-demo .cta { animation: pulse $duration $easing infinite; }

@keyframes pulse {
  0%   { transform: scale(1);     box-shadow: 0 0 0 0 rgba(255, 224, 102, .55); }
  50%  { transform: scale(1.045); box-shadow: 0 0 $sp-8 $sp-2 rgba(255, 224, 102, .55); }
  100% { transform: scale(1);     box-shadow: 0 0 0 0 rgba(255, 224, 102, 0); }
}

04Mockup float

The moment it fires

The whole mockup card — browser-chrome dots and all — drifts up and tilts a fraction of a degree, then settles back, reading as floating instead of sitting flat on the page.

Mockup float
Open in a new tab

04 · Mockup float — Measured area 16.142% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

transform: translateYrotate@keyframes

The idea behind it

Both values live entirely inside transform, so the float never forces a layout recalculation.

.mockup { overflow: hidden; }

.stage.is-demo .mockup { animation: float $duration $easing infinite; }

@keyframes float {
  0%   { transform: translateY(0) rotate(0deg); }
  50%  { transform: translateY(-$sp-4) rotate(-1.2deg); }
  100% { transform: translateY(0) rotate(0deg); }
}

05Badge slide in

What the user notices

A NEW badge slides in from the side, holds still long enough to actually be read, then slides back out and repeats.

Badge slide in
Open in a new tab

05 · Badge slide in — Measured area 1.14% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

transform: translateXopacitykeyframe hold

Why it is done this way

That hold comes from a flat stretch in the keyframes — 30% to 65% sit at the same value — rather than any pause added to the animation itself.

.badge {
  opacity: 0;
  transform: translateX($sp-10);
}
.stage.is-demo .badge { animation: slide-in $duration $easing infinite; }

@keyframes slide-in {
  0%   { opacity: 0; transform: translateX($sp-10); }
  30%  { opacity: 1; transform: translateX(0); }
  65%  { opacity: 1; transform: translateX(0); }
  100% { opacity: 0; transform: translateX($sp-10); }
}

06Marquee strip

Where this shows up

Below the hero, a row of partner-logo pills scrolls sideways without a visible end, because the row is actually two identical sets of pills glued together and moved exactly half their combined width.

Marquee strip
Open in a new tab

06 · Marquee strip — Measured area 7.556% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

translateX(-50%)margin-right gapdoubled track

Worth noting

Spacing between pills comes from margin-right, not flex gap — gap would only widen the seam between the two sets, and the loop would visibly jump once a lap.

.marquee__track {
  display: flex;
  width: max-content;
  animation: pill-scroll $duration $easing infinite;
}
.marquee__pill { margin-right: $sp-4; flex-shrink: 0; }

@keyframes pill-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

07Text fades behind on scroll

What happens on screen

In production, a real scroll-timeline would tie this headline's scale, opacity, and position directly to how far the page has scrolled.

Text fades behind on scroll
Open in a new tab

07 · Text fades behind on scroll — Measured area 13.056% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

scale · opacitytranslateYtransform-origin: center top

The key property

Browser support for that still varies enough that this demo swaps in a plain, looping @keyframes animation instead, so the capture looks the same no matter which browser opens it.

.stage.is-demo .hero {
  animation: recede $duration $easing infinite;
  transform-origin: center top;
}

@keyframes recede {
  0%   { opacity: 1;   transform: scale(1)   translateY(0); }
  55%  { opacity: .15; transform: scale(.85) translateY(-$sp-10); }
  100% { opacity: 1;   transform: scale(1)   translateY(0); }
}

08Split text mask reveal

What it looks like

The headline looks like it's being wiped into view left to right, with an underline growing on the exact same beat underneath it.

Split text mask reveal
Open in a new tab

08 · Split text mask reveal — Measured area 2.719% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · See the password

clip-path: inset()scaleX underlinesweep reveal

What to read in the code

clip-path: inset() does the wipe by narrowing its right-hand value from 100% down to 0%, while the underline grows alongside it with a plain scaleX.

.headline__mask { clip-path: inset(0 100% 0 0); }
.headline__underline { transform: scaleX(0); transform-origin: left center; }

.stage.is-demo .headline__mask { animation: reveal $duration $easing infinite; }
.stage.is-demo .headline__underline { animation: underline-grow $duration $easing infinite; }

@keyframes reveal {
  0%   { clip-path: inset(0 100% 0 0); }
  40%  { clip-path: inset(0 0% 0 0); }
  85%  { clip-path: inset(0 0% 0 0); }
  100% { clip-path: inset(0 100% 0 0); }
}

09@starting-style load-in

When to use it

A toast can go from display: none to fully visible with a genuine entrance transition — no @keyframes, no animation logic in JavaScript — just @starting-style paired with transition-behavior: allow-discrete set on the display property itself. The only JavaScript anywhere in this set of nine lives here, and it exists purely to replay that entrance every 350ms for the demo capture, not to make a real toast's first appearance work.

@starting-style load-in
Open in a new tab

09 · @starting-style load-in — Measured area 2.515% · moving frames 8/23Original by GODRICH · SCSS and React ship in the zip · See the password

@starting-styletransition-behavior: allow-discretedisplay transition

How it is built

In the actual file this loop sits inside a small function that waits 300ms before its first replay and then repeats.

@supports (transition-behavior: allow-discrete) {
  .toast {
    transition: display $duration allow-discrete,
                opacity $duration $easing, transform $duration $easing;
  }
  .toast.is-hidden {
    display: none;
    opacity: 0;
    transform: translateY($sp-3) scale(.92);
  }
  @starting-style {
    .toast { opacity: 0; transform: translateY($sp-3) scale(.92); }
  }
}
function loop() {
  t.classList.add('is-hidden');
  requestAnimationFrame(function () {
    requestAnimationFrame(function () { t.classList.remove('is-hidden'); });
  });
}
setInterval(loop, 350);

Where does @starting-style break on a page that was never freshly loaded?

What goes wrong

When item 09 of this css hero animation set was first built, the assumption was that @starting-style only fires for elements added to the page after it has already rendered. The real render disagreed: the toast played its entrance once on its own, before the replay script had run a single time, because @starting-style also applies to whatever was already sitting in the document at that very first style calculation — not only to later insertions. The replay loop that follows isn't what creates the entrance at all; it just re-triggers something CSS had already done by itself.

How to avoid it

The re-run problem that appears once an opening sequence moves inside a component is sorted out, mount by mount, in CSS animations in React without a library.

Two other items in this set pay a related, quieter cost for stepping outside transform/opacity: 03's box-shadow glow and 08's clip-path wipe both force a repaint on every frame instead of a cheap composite — a trade each effect makes for a look transform alone can't produce. All nine files — the SCSS above and the React versions — sit behind one zip that opens with 5p7qvp7z, typed exactly as it appears in this sentence.

Variants worth trying

Variant Changed value Feel
Tighter stagger Item 01's entrance points (10% / 20% / 30%) tightened to (5% / 10% / 15%) Lines arrive almost together instead of one at a time
Bigger float Item 04's translateY(-$sp-4) (16px) raised to translateY(-32px) The mockup bobs like it's on water instead of breathing gently
Wider glow Item 03's box-shadow spread $sp-8 (32px) widened to 56px The CTA reads as a more urgent pulse instead of a calm one

Accessibility

What reduced motion leaves

With prefers-reduced-motion: reduce, all nine turn their animation off, and most simply land on an end state: 01 and 05 show every headline line and the badge already in place, 04 and 07 hold their resting transform, and 08 leaves the headline fully unmasked with the underline already grown. 02's gradient just stops turning wherever it happened to be mid-loop; 03 swaps its pulsing glow for one fixed, softer shadow rather than turning the glow off outright, so the button still reads as interactive without the motion. 06's marquee track stops at translateX(0), holding on the first set of pills — nothing is lost, since the row was only ever duplicated to loop, not to show two different sets of logos.

What else to check

09 is the odd one out: reduced motion strips its transition, not its animation, so the demo's own replay script still toggles the toast every 350ms, just as an instant flash instead of a slide — the one item here that doesn't fully go still. More CSS animation sets that reuse this project's building blocks live in the marquee post item 06 borrows its scroll structure from, and the rest of what's published sits in the CSS category hub.

FAQ

Do I need JavaScript for these hero section animation ideas?

No — eight of the nine run on CSS alone. The only JavaScript in the set is a short replay loop behind item 09, and it exists only to repeat the entrance for this demo's capture, not to make a real toast's first appearance work.

Why does my scroll-linked hero effect play even when I'm not scrolling?

Item 07 shows what a real scroll-timeline effect would look like, but browser support for animation-timeline: view() and scroll() still varies enough that this demo substitutes a timed @keyframes loop instead. Wire the same scale, opacity, and translateY values to animation-timeline: view() directly once the target browsers support it — see MDN's scroll-driven animations guide.

Will box-shadow and clip-path animations run as smoothly as the rest?

Not quite — transform and opacity are the only two properties a browser can animate by compositing existing layers, so items 03 (box-shadow) and 08 (clip-path) force a repaint on every frame instead. Both still earn their keep here, since neither a glow nor a wipe reveal can be built from transform or opacity alone. For a first screen that also has to carry a date and a signup button, the event landing page templates hold the finished sections.

Enter the archive password

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