31 CSS Hero Sections09 / 31
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
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><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;
}
}.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();
})();(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();
})();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();
})();
```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 least44 × 44pxto 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#2c4a4fto#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 setstyle={{ '--pos': `${pos}%` }}on the stage. Do NOT useonChange— it fires only on release, breaking the drag interaction. For Vue use@input, for Svelte useon:input.
Gotchas — read before shipping
- The a11y story is subtle: hiding the range input with
opacity: 0keeps it keyboard-focusable — arrow keys still drive the comparison. But you MUST addaria-label="Drag to compare before and after"ANDaria-valuetextthat updates with the value (the demo's JS does updatearia-valuetextto "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 towidthor the deprecatedclip: rect(), every drag frame triggers layout and INP spikes to 300-500ms on mid-tier mobile. Always useclip-path, neverclip. - 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
inputlistener attaches twice on dev — the drag feels laggy because two handlers fire per event. Wrap the attach in auseEffectwith proper cleanup (return () => input.removeEventListener('input', handler)) or use aref-based approach. Production builds don't double-mount, but you'll ship a broken dev experience without the cleanup.