Sometimes a designer’s quirky idea turns into something you actually enjoy building. That’s exactly what happened with this project: columns of content sliding in opposite directions as the user scrolls.
Live demo (CodePen): the effect respects reduced motion settings, so you’ll need to enable motion to see it. As of writing, it works in Chrome and Safari.
Modern CSS makes this surprisingly straightforward — scroll-driven animations are the hero here. Let’s walk through the approach. Maybe you’ll spot a different way to do it.
The HTML Structure
Three nested layers: a parent container (.opposing-columns), child columns (.opposing-column), and items inside each column (.opposing-item).
<div class="opposing-columns">
<div class="opposing-column">
<div class="opposing-item">...</div>
<div class="opposing-item">...</div>
</div>
<div class="opposing-column">
<div class="opposing-item">...</div>
</div>
<div class="opposing-column">
<div class="opposing-item">...</div>
</div>
</div>That’s it. CSS handles the rest.
Responsive Setup
The effect only makes sense on larger screens where there’s enough horizontal space. We wrap everything in a media query:
@media screen and (width >= 50rem) {
.opposing-columns {
display: flex;
gap: 2rem;
max-inline-size: min(90dvi, 50rem);
margin-inline: auto;
}
}Creating the Masking Illusion
Items in the outer columns move upward on scroll; those in the centre column move downward. To make them fade as they cross the container’s boundaries, we use pseudo-elements with gradients — no opacity tricks needed.
First, we set a background colour variable on :root:
:root {
--opposing-bg: lightcyan;
--opposing-mask: 3rem;
background-color: var(--opposing-bg);
}Then we apply :before and :after on the parent with the same background colour and z-index: 1 to sit above the columns:
.opposing-columns {
position: relative;
margin-block: var(--opposing-mask, 3rem);
&:before,
&:after {
content: "";
position: absolute;
inset-inline: 0;
block-size: calc(var(--opposing-mask) * 3);
pointer-events: none;
z-index: 1;
}
}Now the gradients:
- Top pseudo (
:before): solid background to transparent, top-to-bottom. - Bottom pseudo (
:after): transparent to solid background, bottom-to-top.
.opposing-columns {
&:before {
background-image: linear-gradient(
to bottom,
var(--opposing-bg) var(--opposing-mask),
transparent
);
inset-block-start: calc(var(--opposing-mask) * -1);
}
&:after {
background-image: linear-gradient(
to top,
var(--opposing-bg) var(--opposing-mask),
transparent
);
inset-block-end: calc(var(--opposing-mask) * -1);
}
}Items slide under these masks as they scroll, creating a clean fade at the container’s top and bottom edges.
Practical Considerations for Cyprus & EU Businesses
- Performance: scroll-driven animations run on the compositor thread — no JavaScript jank. That matters for mobile users on 4G in Limassol or Nicosia.
- GDPR & Accessibility: the demo respects
prefers-reduced-motion. Always include that check to avoid violating EU accessibility directives. - Multilingual sites (EN/RU/EL): this CSS technique works regardless of language switching or RTL layouts (use
inset-inlineandinset-blockas we did). - Budget estimate: for a custom component like this, expect 2–4 hours of development time. Combined with a full website build (5–10 pages), typical costs from a Cyprus web studio range €1,500–€3,500.