<section class="section">
<div class="container">
<div class="slider" id="scroller">
<div class="card card-1">
<div class="visual">
<img
class="img"
width="400"
height="400"
src="https://raw.githubusercontent.com/mobalti/open-props-interfaces/main/sticky-slider-cta-cards/assets/img-1.avif"
alt="A 3D rendering of a futuristic hotel created by AI"
/>
</div>
<div class="content">
<div class="meta">
<h2 class="title">Search for anything</h2>
<div class="desc">
Ask any question — short or long, specific or vague. Then
follow-up in chat.
</div>
</div>
<div class="card-actions">
<a class="btn common-btn primary" href="#">Try Open Props</a>
<a class="btn common-btn" href="#">Learn tips & tricks</a>
</div>
</div>
</div>
<div class="card card-2">
<div class="visual">
<img
class="img"
width="400"
height="400"
src="https://raw.githubusercontent.com/mobalti/open-props-interfaces/main/sticky-slider-cta-cards/assets/img-2.avif"
alt="A 3D rendering of a futuristic hotel created by AI"
/>
</div>
<div class="content">
<div class="meta">
<h2 class="title">Expand your world</h2>
<div class="desc">
Visualize your ideas. Generate custom images with ease.
</div>
</div>
<div class="card-actions">
<a class="btn common-btn primary" href="#">Try Open Props</a>
<a class="btn common-btn" href="#">Learn tips & tricks</a>
</div>
</div>
</div>
<div class="card card-3">
<div class="visual">
<img
class="img"
width="400"
height="400"
src="https://raw.githubusercontent.com/mobalti/open-props-interfaces/main/sticky-slider-cta-cards/assets/img-3.avif"
alt="A 3D rendering of a futuristic hotel created by AI"
/>
</div>
<div class="content">
<div class="meta">
<h2 class="title">Keep exploring</h2>
<div class="desc">
Perfect your vision. Fine-tune images to match your style.
</div>
</div>
<div class="card-actions">
<a class="btn common-btn primary" href="#">Try Open Props</a>
<a class="btn common-btn" href="#">Learn tips & tricks</a>
</div>
</div>
</div>
</div>
<div class="slider-controls">
<div class="slider-controls-wrapper">
<button
class="control-button prev"
aria-label="Go to previous"
onclick="scroller.scrollBy(-100, 0)"
>
<svg
xmlns="http://www.w3.org/2000/svg"
height="40px"
viewBox="0 -960 960 960"
width="40px"
fill="#5f6368"
>
<path
d="m414.67-480.67 170 170q9.66 9.67 9.33 23.34-.33 13.66-10 23.33-9.67 9.67-23.67 9.67-14 0-23.66-9.67L343.33-457.33q-5.33-5.34-7.5-11-2.16-5.67-2.16-12.34 0-6.66 2.16-12.33 2.17-5.67 7.5-11l194-194q9.67-9.67 23.67-9.67 14 0 23.67 9.67 9.66 9.67 9.66 23.67 0 14-9.66 23.66l-170 170Z"
/>
</svg>
</button>
<button
class="control-button next"
aria-label="Go to next"
onclick="scroller.scrollBy(100, 0)"
>
<svg
xmlns="http://www.w3.org/2000/svg"
height="40px"
viewBox="0 -960 960 960"
width="40px"
fill="#5f6368"
>
<path
d="m521.33-480.67-170-170q-9.66-9.66-9.33-23.33.33-13.67 10-23.33 9.67-9.67 23.67-9.67 14 0 23.66 9.67L592.67-504q5.33 5.33 7.5 11 2.16 5.67 2.16 12.33 0 6.67-2.16 12.34-2.17 5.66-7.5 11l-194 194q-9.67 9.66-23.34 9.33-13.66-.33-23.33-10-9.67-9.67-9.67-23.67 0-14 9.67-23.66l169.33-169.34Z"
/>
</svg>
</button>
</div>
</div>
<div class="pagination">
<div class="pagination-wrapper">
<span class="marker marker-1"></span>
<span class="marker marker-2"></span>
<span class="marker marker-3"></span>
</div>
</div>
</div>
</section>
@layer library, reset, base, utilities, components, layout, override;
@import url("https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400..600&display=swap")
layer(library.font);
@import "https://unpkg.com/open-props" layer(library.design-system);
@import "https://unpkg.com/open-props/normalize.light.min.css"
layer(library.normalize);
@import "https://unpkg.com/open-props/buttons.light.min.css"
layer(library.buttons);
@layer demo {
.section {
background-color: white;
display: grid;
min-block-size: 800px;
padding-block: var(--size-px-7);
padding-inline: var(--size-px-7);
place-items: center;
}
.container {
container-type: inline-size;
position: relative;
display: grid;
inline-size: min(100%, 1064px);
> * {
grid-area: 1/1;
}
}
.slider {
-ms-overflow-style: none;
border-radius: var(--radius-3);
display: grid;
grid-auto-flow: column;
inline-size: 100%;
overflow-x: auto;
position: relative;
scroll-behavior: smooth;
scroll-snap-type: x mandatory;
scroll-timeline: --section-wrapper inline;
scrollbar-width: none;
/* Firefox */
&::-webkit-scrollbar {
display: none;
}
}
.card {
background-image: linear-gradient(122deg, lavenderblush, lavender);
border-radius: var(--radius-3);
display: grid;
gap: var(--size-px-5);
inline-size: 100cqi;
inset-inline-start: 0;
padding-block: var(--size-px-10);
padding-inline: var(--size-px-6);
place-items: start;
scroll-snap-align: start;
scroll-snap-stop: always;
@supports not (-moz-appearance: none) {
position: sticky;
}
@container (width >=860px) {
gap: var(--size-px-9);
grid-template-columns: 1fr 1fr;
padding-inline: var(--size-px-10);
place-items: center;
}
}
.visual {
aspect-ratio: var(--ratio-square);
border-radius: var(--radius-3);
box-shadow: var(--shadow-6);
overflow: clip;
@container (width >=860px) {
max-block-size: 400px;
}
}
.img {
block-size: 100%;
inline-size: 100%;
object-fit: cover;
background-image: var(--gradient-6);
}
.content {
display: grid;
gap: var(--size-px-8);
@container (width >=860px) {
gap: var(--size-px-5);
place-items: center start;
}
}
.meta {
display: grid;
gap: var(--size-px-2);
}
.title {
font-family: var(--font-neo-grotesque);
font-size: 1.75rem;
font-weight: var(--font-weight-4);
text-wrap: balance;
@container (width >=860px) {
font-size: var(--font-size-5);
}
}
.desc {
font-family: var(--font-neo-grotesque);
font-size: var(--font-size-2);
max-inline-size: var(--size-content-2);
text-wrap: pretty;
}
.card-actions {
display: grid;
gap: var(--size-px-2);
place-items: start;
@container (width >=860px) {
gap: var(--size-px-3);
grid-auto-flow: column;
}
}
.common-btn {
--_bg: linear-gradient(aliceblue, aliceblue),
linear-gradient(to right, deepskyblue, royalblue);
--_border: transparent;
--_ink-shadow: none;
background-clip: padding-box, border-box;
background-origin: border-box;
border-radius: var(--radius-4);
font-family: var(--font-neo-grotesque);
font-size: 0.875rem;
font-weight: var(--font-weight-5);
inline-size: max-content;
min-block-size: 40px;
text-decoration: none;
}
.primary {
--_bg: linear-gradient(deepskyblue, royalblue);
--_border: deepskyblue;
--_text: white;
border-width: 0;
}
.slider-controls {
display: grid;
place-items: center;
padding-block: var(--size-px-9);
}
.slider-controls-wrapper {
display: grid;
grid-auto-flow: column;
inline-size: calc(100% + 2rem);
justify-content: space-between;
place-items: center;
@container (width >=860px) {
inline-size: 100%;
padding: var(--size-px-2);
}
/* Remove this block to show slider controls on mobile */
@container (width < 860px) {
display: none;
}
}
.control-button {
block-size: var(--size-px-8);
border-radius: var(--radius-round);
display: inline-grid;
inline-size: var(--size-px-8);
padding: var(--size-px-1);
place-items: center;
z-index: var(--layer-4);
& svg {
inline-size: 100%;
block-size: 100%;
}
}
@supports (animation-timeline: view()) {
/*
The markers are highlighted based on the scroll position of the slider
using scroll-driven animations.
To determine the animation range for each marker, we rely on container
queries to get the inline size of the nearest container, which in this case
is the `slider container`.
Since we have 3 cards, each card occupies 100% of the container's inline size,
which translates to 100cqi. This is why we increment the animation range for
each marker by 100cqi:
- `.marker-1` highlights when the scroll position is within the first 100cqi
of the slider's width.
- `.marker-2` highlights when the scroll position is between 100cqi and
200cqi of the slider's width.
- `.marker-3` highlights when the scroll position is between 200cqi and
300cqi of the slider's width.
For more examples and information on scroll-driven animations, check out
https://scroll-driven-animations.style/ by @bramus.
*/
body {
timeline-scope: --slider;
}
.pagination {
display: grid;
inset-block-end: 0;
inset-inline: 0;
padding-block: var(--size-px-7);
place-items: center;
position: absolute;
}
.pagination-wrapper {
display: grid;
gap: var(--size-px-3);
grid-auto-flow: column;
z-index: var(--layer-important);
}
.marker {
background-color: black;
block-size: 12px;
border-radius: var(--radius-round);
display: block;
inline-size: 12px;
z-index: var(--layer-important);
}
.slider {
scroll-timeline-axis: --inline;
scroll-timeline-name: --slider;
}
.marker {
animation-name: highlight-dot;
animation-timeline: --slider;
background-color: black;
opacity: 0.3;
}
.marker-1 {
animation-range-end: 100cqi;
}
.marker-2 {
animation-range: 100cqi 200cqi;
}
.marker-3 {
animation-range: 200cqi 300cqi;
}
@keyframes highlight-dot {
0%,
100% {
opacity: 0.9;
}
}
/* Handle control button visibility depend on the slider position using Scroll-Driven Animations */
.control-button {
animation-fill-mode: forwards;
animation-timeline: --slider;
}
.next {
animation-name: hideOnScrollEnd;
}
.prev {
animation-name: hideOnScrollStart;
}
@keyframes hideOnScrollStart {
from {
visibility: hidden;
}
}
@keyframes hideOnScrollEnd {
to {
visibility: hidden;
}
}
}
}
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.