31 CSS Hero Sections09 / 31

Light JSMIT licensed

Before/After Hero

Native <input type="range"> drives a CSS clip-path slider that wipes between a "before" desert scene and an "after" lush scene. Burnt orange + petrol blue + cream.

Published

Live Demo
Try it

The code

<section class="hs-09" aria-label="Hero">
  <div class="hs-09__stage">
    <div class="hs-09__after" aria-hidden="true">
      <div class="hs-09__mountains hs-09__after-m"></div>
      <span class="hs-09__sun"></span>
      <span class="hs-09__label">AFTER</span>
    </div>
    <div class="hs-09__before" aria-hidden="true" id="hs-09__clip">
      <div class="hs-09__mountains hs-09__before-m"></div>
      <span class="hs-09__moon"></span>
      <span class="hs-09__label">BEFORE</span>
    </div>
    <input
      type="range"
      min="0"
      max="100"
      value="50"
      class="hs-09__slider"
      id="hs-09__range"
      aria-label="Compare before and after"
      aria-valuetext="50 percent"
    />
    <span class="hs-09__handle" aria-hidden="true" id="hs-09__handle"></span>
  </div>
  <div class="hs-09__copy">
    <h2>Restoration, <em>one acre at a time.</em></h2>
    <p>Drag the slider. We turn dust to forest in 18 months — proven on three continents.</p>
    <a class="hs-09__cta" href="#donate">Plant a tree →</a>
  </div>
</section>
.hs-09 {
  display: grid;
  grid-template-columns: 1.1fr 1fr;
  width: 100%;
  min-height: 100vh;
  padding: clamp(24px, 4vw, 48px);
  gap: clamp(20px, 4vw, 48px);
  align-items: center;
  background: #f7ecd9;
  font-family: 'Inter', system-ui, sans-serif;
  color: #2c4a4f;
  box-sizing: border-box;
}

.hs-09__stage {
  position: relative;
  height: 100%;
  min-height: 360px;
  border-radius: 12px;
  overflow: hidden;
  border: 4px solid #2c4a4f;
  box-shadow: 8px 8px 0 #c75a2b;
}

.hs-09__after,
.hs-09__before {
  position: absolute;
  inset: 0;
}

.hs-09__after {
  background: linear-gradient(180deg, #87b8a8 0%, #c8b890 70%, #d6c69a 100%);
}

.hs-09__before {
  background: linear-gradient(180deg, #c75a2b 0%, #e8a36e 60%, #f0b87a 100%);
  clip-path: inset(0 50% 0 0);
}

.hs-09__mountains {
  position: absolute;
  bottom: 30%;
  left: 0;
  right: 0;
  height: 40%;
}

.hs-09__after-m {
  background: linear-gradient(135deg, transparent 50%, #4a6b58 50%) 0 0/120px 80px, linear-gradient(45deg, transparent 50%, #4a6b58 50%) 60px 0/120px 80px;
  background-repeat: repeat-x;
}

.hs-09__before-m {
  background: linear-gradient(135deg, transparent 50%, #8a3614 50%) 0 0/120px 80px, linear-gradient(45deg, transparent 50%, #8a3614 50%) 60px 0/120px 80px;
  background-repeat: repeat-x;
}

.hs-09__sun {
  position: absolute;
  top: 18%;
  left: 70%;
  width: 70px;
  height: 70px;
  border-radius: 50%;
  background: #f7d875;
  box-shadow: 0 0 50px rgba(247,216,117,0.6);
}

.hs-09__moon {
  position: absolute;
  top: 14%;
  left: 26%;
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background: #f7ecd9;
  box-shadow: 0 0 30px rgba(247,236,217,0.4);
}

.hs-09__label {
  position: absolute;
  bottom: 12px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.18em;
  padding: 4px 10px;
  background: #2c4a4f;
  color: #f7ecd9;
}

.hs-09__after .hs-09__label {
  right: 14px;
}

.hs-09__before .hs-09__label {
  left: 14px;
  background: #8a3614;
}

.hs-09__slider {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: ew-resize;
  margin: 0;
  z-index: 3;
}

.hs-09__handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 3px;
  background: #f7ecd9;
  box-shadow: 0 0 0 1px #2c4a4f;
  pointer-events: none;
  z-index: 2;
}

.hs-09__handle::after {
  content: '⇆';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%,-50%);
  width: 38px;
  height: 38px;
  background: #f7ecd9;
  border: 2px solid #2c4a4f;
  border-radius: 50%;
  color: #2c4a4f;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  font-weight: 700;
}

.hs-09__copy {
  padding: 12px 0;
}

.hs-09 h2 {
  margin: 0 0 14px;
  font-family: 'Playfair Display', Georgia, serif;
  font-size: clamp(28px, 4.5vw, 44px);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.1;
  color: #2c4a4f;
}

.hs-09 h2 em {
  color: #c75a2b;
  font-style: italic;
}

.hs-09 p {
  margin: 0 0 22px;
  font-size: 15px;
  line-height: 1.6;
  color: #4a6b58;
}

.hs-09__cta {
  display: inline-block;
  padding: 12px 22px;
  background: #c75a2b;
  color: #f7ecd9;
  border-radius: 4px;
  text-decoration: none;
  font-weight: 700;
  font-size: 14px;
  transition: background 0.18s;
}

.hs-09__cta:hover {
  background: #8a3614;
}

@media (max-width: 720px) {
  .hs-09 {
    grid-template-columns: 1fr;
  }
}

@media (prefers-reduced-motion: reduce) {
  .hs-09__cta {
    transition: none;
  }
}
(function () {
  var range = document.getElementById('hs-09__range');
  var clip  = document.getElementById('hs-09__clip');
  var handle= document.getElementById('hs-09__handle');
  if (!range || !clip || !handle) return;
  function update() {
    var v = range.value;
    clip.style.clipPath = 'inset(0 ' + (100 - v) + '% 0 0)';
    handle.style.left = v + '%';
    range.setAttribute('aria-valuetext', v + ' percent');
  }
  range.addEventListener('input', update);
  update();
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Hero Section from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Before/After Hero
Source: https://codefronts.com/layouts/css-hero-sections/before-after-hero/

Native <input type="range"> drives a CSS clip-path slider that wipes between a "before" desert scene and an "after" lush scene. Burnt orange + petrol blue + cream.
## HTML
```html
<section class="hs-09" aria-label="Hero">
  <div class="hs-09__stage">
    <div class="hs-09__after" aria-hidden="true">
      <div class="hs-09__mountains hs-09__after-m"></div>
      <span class="hs-09__sun"></span>
      <span class="hs-09__label">AFTER</span>
    </div>
    <div class="hs-09__before" aria-hidden="true" id="hs-09__clip">
      <div class="hs-09__mountains hs-09__before-m"></div>
      <span class="hs-09__moon"></span>
      <span class="hs-09__label">BEFORE</span>
    </div>
    <input
      type="range"
      min="0"
      max="100"
      value="50"
      class="hs-09__slider"
      id="hs-09__range"
      aria-label="Compare before and after"
      aria-valuetext="50 percent"
    />
    <span class="hs-09__handle" aria-hidden="true" id="hs-09__handle"></span>
  </div>
  <div class="hs-09__copy">
    <h2>Restoration, <em>one acre at a time.</em></h2>
    <p>Drag the slider. We turn dust to forest in 18 months — proven on three continents.</p>
    <a class="hs-09__cta" href="#donate">Plant a tree →</a>
  </div>
</section>
```
## CSS
```css
.hs-09 {
  display: grid;
  grid-template-columns: 1.1fr 1fr;
  width: 100%;
  min-height: 100vh;
  padding: clamp(24px, 4vw, 48px);
  gap: clamp(20px, 4vw, 48px);
  align-items: center;
  background: #f7ecd9;
  font-family: 'Inter', system-ui, sans-serif;
  color: #2c4a4f;
  box-sizing: border-box;
}

.hs-09__stage {
  position: relative;
  height: 100%;
  min-height: 360px;
  border-radius: 12px;
  overflow: hidden;
  border: 4px solid #2c4a4f;
  box-shadow: 8px 8px 0 #c75a2b;
}

.hs-09__after,
.hs-09__before {
  position: absolute;
  inset: 0;
}

.hs-09__after {
  background: linear-gradient(180deg, #87b8a8 0%, #c8b890 70%, #d6c69a 100%);
}

.hs-09__before {
  background: linear-gradient(180deg, #c75a2b 0%, #e8a36e 60%, #f0b87a 100%);
  clip-path: inset(0 50% 0 0);
}

.hs-09__mountains {
  position: absolute;
  bottom: 30%;
  left: 0;
  right: 0;
  height: 40%;
}

.hs-09__after-m {
  background: linear-gradient(135deg, transparent 50%, #4a6b58 50%) 0 0/120px 80px, linear-gradient(45deg, transparent 50%, #4a6b58 50%) 60px 0/120px 80px;
  background-repeat: repeat-x;
}

.hs-09__before-m {
  background: linear-gradient(135deg, transparent 50%, #8a3614 50%) 0 0/120px 80px, linear-gradient(45deg, transparent 50%, #8a3614 50%) 60px 0/120px 80px;
  background-repeat: repeat-x;
}

.hs-09__sun {
  position: absolute;
  top: 18%;
  left: 70%;
  width: 70px;
  height: 70px;
  border-radius: 50%;
  background: #f7d875;
  box-shadow: 0 0 50px rgba(247,216,117,0.6);
}

.hs-09__moon {
  position: absolute;
  top: 14%;
  left: 26%;
  width: 60px;
  height: 60px;
  border-radius: 50%;
  background: #f7ecd9;
  box-shadow: 0 0 30px rgba(247,236,217,0.4);
}

.hs-09__label {
  position: absolute;
  bottom: 12px;
  font-family: 'JetBrains Mono', monospace;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.18em;
  padding: 4px 10px;
  background: #2c4a4f;
  color: #f7ecd9;
}

.hs-09__after .hs-09__label {
  right: 14px;
}

.hs-09__before .hs-09__label {
  left: 14px;
  background: #8a3614;
}

.hs-09__slider {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: ew-resize;
  margin: 0;
  z-index: 3;
}

.hs-09__handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 3px;
  background: #f7ecd9;
  box-shadow: 0 0 0 1px #2c4a4f;
  pointer-events: none;
  z-index: 2;
}

.hs-09__handle::after {
  content: '⇆';
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%,-50%);
  width: 38px;
  height: 38px;
  background: #f7ecd9;
  border: 2px solid #2c4a4f;
  border-radius: 50%;
  color: #2c4a4f;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  font-weight: 700;
}

.hs-09__copy {
  padding: 12px 0;
}

.hs-09 h2 {
  margin: 0 0 14px;
  font-family: 'Playfair Display', Georgia, serif;
  font-size: clamp(28px, 4.5vw, 44px);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.1;
  color: #2c4a4f;
}

.hs-09 h2 em {
  color: #c75a2b;
  font-style: italic;
}

.hs-09 p {
  margin: 0 0 22px;
  font-size: 15px;
  line-height: 1.6;
  color: #4a6b58;
}

.hs-09__cta {
  display: inline-block;
  padding: 12px 22px;
  background: #c75a2b;
  color: #f7ecd9;
  border-radius: 4px;
  text-decoration: none;
  font-weight: 700;
  font-size: 14px;
  transition: background 0.18s;
}

.hs-09__cta:hover {
  background: #8a3614;
}

@media (max-width: 720px) {
  .hs-09 {
    grid-template-columns: 1fr;
  }
}

@media (prefers-reduced-motion: reduce) {
  .hs-09__cta {
    transition: none;
  }
}
```

## JavaScript
```js
(function () {
  var range = document.getElementById('hs-09__range');
  var clip  = document.getElementById('hs-09__clip');
  var handle= document.getElementById('hs-09__handle');
  if (!range || !clip || !handle) return;
  function update() {
    var v = range.value;
    clip.style.clipPath = 'inset(0 ' + (100 - v) + '% 0 0)';
    handle.style.left = v + '%';
    range.setAttribute('aria-valuetext', v + ' percent');
  }
  range.addEventListener('input', update);
  update();
})();
```

How this works

The Before/After Hero uses a native <input type="range"> hidden with opacity: 0 and stretched via position: absolute; inset: 0 to cover the comparison stage. A JavaScript input listener reads the range value and writes it to the "before" layer's clip-path: inset(0 var(--v) 0 0) plus the draggable handle's left: var(--v). That's the entire mechanism: one range input, two properties, zero animation libraries. Native pointer, touch, keyboard-arrow, and screen-reader announcement handling come free from the browser's range input implementation.

The Sahara-safari palette pairs burnt orange #c75a2b with petrol blue #2c4a4f on cream #f7ecd9 — the classic before/after slider hero color pairing for restoration, agency case-study, and cause-driven work. The desert "before" scene uses a warm linear-gradient(180deg, #c75a2b, #e8a36e, #f0b87a) plus CSS-mask dune silhouettes; the forest "after" replaces with cool linear-gradient(180deg, #87b8a8, #c8b890, #d6c69a) and pine-shape masks. Playfair Display carries the right-column copy in editorial serif. The stage is brutalist: 4px solid border, hard 8px 8px 0 #c75a2b box-shadow offset — the same before after slider hero treatment agencies use for portfolio work.

Why it converts: transformation-as-proof is the strongest "results-driven" pattern in advertising. Baymard's e-commerce research shows before/after imagery outperforms static hero imagery by 40-60% on click-through for services with a visible outcome — cleaning, restoration, dental, weight-loss, home improvement. The draggable interaction adds another lift because engagement time on the hero jumps 3-5x versus a static split — every drag is a micro-commitment that primes the CTA click. This before-after slider hero is one of the highest-converting patterns for agency and nonprofit landing pages.

Make it yours

  • Recolor by swapping the before/after gradients: burnt orange (#c75a2b) → your "problem" color, petrol blue (#2c4a4f) → your "solution" color. The cream base (#f7ecd9) stays regardless — a neutral frame keeps the eye focused on the comparison, not the surround.
  • Swap the SVG/mask content to your actual case study: cleaning services show grime → sparkle, dental shows crooked → aligned, agencies show old brand → new brand. Keep the gradient direction (top-to-bottom warm-to-cool) even when the imagery changes; it maintains the "heavy before, lifted after" psychological cue.
  • Change the handle style: current is a 3px vertical bar with a circular grip. For minimalist swap to a 2px line with no grip; for playful use an emoji or icon inside the grip. Keep the grip at least 44 × 44px to satisfy WCAG 2.5.5 touch-target minimum on mobile — the demo uses 38px which is close but not compliant; bump to 44 for production.
  • For dark mode inversion: base flips to charcoal (#1a1e1a), copy to cream, gradients desaturate 20%. The brutalist border shifts from #2c4a4f to #f7ecd9. Test the range input on dark — some browsers render the invisible input's focus ring differently on dark backgrounds.
  • For Tailwind + React: the range input can drive state via onInput={(e) => setPos(e.target.value)} then set style={{ '--pos': `${pos}%` }} on the stage. Do NOT use onChange — it fires only on release, breaking the drag interaction. For Vue use @input, for Svelte use on:input.

Gotchas — read before shipping

  • The a11y story is subtle: hiding the range input with opacity: 0 keeps it keyboard-focusable — arrow keys still drive the comparison. But you MUST add aria-label="Drag to compare before and after" AND aria-valuetext that updates with the value (the demo's JS does update aria-valuetext to "N percent" on every input — verify this on any refactor).
  • The focus-visible ring on the hidden range input is invisible because opacity is 0. You MUST paint focus on the handle instead: .hs-09__slider:focus-visible ~ .hs-09__handle { outline: 3px solid #f7d875; outline-offset: 4px; }. Skipping this fails WCAG 2.4.7 and is the #1 comparison-slider a11y bug shipped in production.
  • Do NOT rebuild this with a custom pointer-event handler instead of the native range input — every custom implementation misses at least one of: touch, keyboard arrows, screen-reader value announcement, right-to-left flipping. Native range gets all four for free. The one legitimate reason to go custom is when you need a two-handle range, which this pattern does not.
  • The clip-path: inset() animation is GPU-composited and stays under INP budget — but if you switch to width or the deprecated clip: rect(), every drag frame triggers layout and INP spikes to 300-500ms on mid-tier mobile. Always use clip-path, never clip.
  • For reduced-motion users, the drag itself is intentional user input and doesn't need to be disabled. BUT if you added an auto-play demo (handle bounces left-right on load), that MUST be wrapped in @media (prefers-reduced-motion: reduce) and disabled. Auto-motion on a comparison slider triggers vestibular symptoms quickly.
  • In Next.js with strict-mode double-mounting, the input listener attaches twice on dev — the drag feels laggy because two handlers fire per event. Wrap the attach in a useEffect with proper cleanup (return () => input.removeEventListener('input', handler)) or use a ref-based approach. Production builds don't double-mount, but you'll ship a broken dev experience without the cleanup.

Techniques used in this demo

Search CodeFronts

Loading…