07Active slide scale up
The centered photo scales up while its neighbors shrink and dim, so the current pick is obvious at a glance — clicking a dot brings that photo to center and enlarges it.
Best forFeatured-product sliders, gallery highlights
:checkedtransform: scaleopacity

<main class="stage">
<div class="as is-demo">
<input type="radio" name="as-tab" id="as-1" class="as__radio" checked>
<input type="radio" name="as-tab" id="as-2" class="as__radio">
<input type="radio" name="as-tab" id="as-3" class="as__radio">
<div class="as__track">
<label class="as__slide as__slide--1" for="as-1" aria-label="Slide 1"></label>
<label class="as__slide as__slide--2" for="as-2" aria-label="Slide 2"></label>
<label class="as__slide as__slide--3" for="as-3" aria-label="Slide 3"></label>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-base;
$easing: $ease-spring;
$color: $stage-orange;
@include stage($stage-ink);
.as { width: min(400px, 92vw); }
.as__radio { position: absolute; opacity: 0; pointer-events: none; }
.as__track {
display: flex;
align-items: center;
justify-content: center;
gap: $sp-3;
}
.as__slide {
flex: 0 0 30%;
aspect-ratio: 3 / 4;
border-radius: $r-card;
position: relative;
overflow: hidden;
cursor: pointer;
transform: scale(.82);
opacity: .5;
transition: transform $duration $easing, opacity $duration $easing;
z-index: 0;
}
.as__slide--1 { background: linear-gradient(160deg, $subject-blue, #d6ddeb); }
.as__slide--2 { background: linear-gradient(160deg, $color, #ebdbd6); }
.as__slide--3 { background: linear-gradient(160deg, #c2ae82, $stage-yellow); }
.as__slide::before {
content: ""; position: absolute; top: $sp-2; right: $sp-2;
width: $sp-6; height: $sp-6; border-radius: 50%;
background: rgba(255, 255, 255, .85);
}
.as__slide::after {
content: ""; position: absolute; left: -10%; right: -10%; bottom: -6%; height: 46%;
background: rgba(23, 20, 26, .26);
clip-path: polygon(0% 100%, 22% 40%, 42% 66%, 60% 26%, 78% 60%, 100% 34%, 100% 100%);
}
// ── 실제 동작: 고른 라디오의 슬라이드만 커지고 진해진다. 나머지는 작고 흐리다 ──
#as-1:checked ~ .as__track .as__slide--1,
#as-2:checked ~ .as__track .as__slide--2,
#as-3:checked ~ .as__track .as__slide--3 {
transform: scale(1.08);
opacity: 1;
z-index: 1;
}
// ── 데모 루프: 클릭 전까지 라디오가 스스로 1→2→3→1 순으로 커진다 (라디오 상태는 그대로 1번) ──
.as.is-demo .as__slide--1 { animation: as-1 $dur-loop $easing infinite; }
.as.is-demo .as__slide--2 { animation: as-2 $dur-loop $easing infinite; }
.as.is-demo .as__slide--3 { animation: as-3 $dur-loop $easing infinite; }
// 1번은 0%에서 작은 상태로 시작해 0→4% 동안 커진다(3번이 96→100%에서 작아지는 것과 이어진다) —
// 루프 경계에서 1번이 순간이동으로 팝 커지던 티를 없앤다 (끝→처음 텔레포트 금지)
@keyframes as-1 {
0% { transform: scale(.82); opacity: .5; }
4%, 29% { transform: scale(1.08); opacity: 1; }
54%, 100% { transform: scale(.82); opacity: .5; }
}
@keyframes as-2 {
0%, 29% { transform: scale(.82); opacity: .5; }
33%, 62% { transform: scale(1.08); opacity: 1; }
66%, 100% { transform: scale(.82); opacity: .5; }
}
@keyframes as-3 {
0%, 62% { transform: scale(.82); opacity: .5; }
66%, 96% { transform: scale(1.08); opacity: 1; }
100% { transform: scale(.82); opacity: .5; }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.as__slide { transition: none; }
.as.is-demo .as__slide--1,
.as.is-demo .as__slide--2,
.as.is-demo .as__slide--3 { animation: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.as {
width: min(400px, 92vw);
}
.as__radio {
position: absolute;
opacity: 0;
pointer-events: none;
}
.as__track {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.as__slide {
flex: 0 0 30%;
aspect-ratio: 3/4;
border-radius: 16px;
position: relative;
overflow: hidden;
cursor: pointer;
transform: scale(0.82);
opacity: 0.5;
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
z-index: 0;
}
.as__slide--1 {
background: linear-gradient(160deg, #2f6df6, #d6ddeb);
}
.as__slide--2 {
background: linear-gradient(160deg, #ff4d1f, #ebdbd6);
}
.as__slide--3 {
background: linear-gradient(160deg, #c2ae82, #ffd23f);
}
.as__slide::before {
content: "";
position: absolute;
top: 8px;
right: 8px;
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.85);
}
.as__slide::after {
content: "";
position: absolute;
left: -10%;
right: -10%;
bottom: -6%;
height: 46%;
background: rgba(23, 20, 26, 0.26);
clip-path: polygon(0% 100%, 22% 40%, 42% 66%, 60% 26%, 78% 60%, 100% 34%, 100% 100%);
}
#as-1:checked ~ .as__track .as__slide--1,
#as-2:checked ~ .as__track .as__slide--2,
#as-3:checked ~ .as__track .as__slide--3 {
transform: scale(1.08);
opacity: 1;
z-index: 1;
}
.as.is-demo .as__slide--1 {
animation: as-1 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.as.is-demo .as__slide--2 {
animation: as-2 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.as.is-demo .as__slide--3 {
animation: as-3 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes as-1 {
0% {
transform: scale(0.82);
opacity: 0.5;
}
4%, 29% {
transform: scale(1.08);
opacity: 1;
}
54%, 100% {
transform: scale(0.82);
opacity: 0.5;
}
}
@keyframes as-2 {
0%, 29% {
transform: scale(0.82);
opacity: 0.5;
}
33%, 62% {
transform: scale(1.08);
opacity: 1;
}
66%, 100% {
transform: scale(0.82);
opacity: 0.5;
}
}
@keyframes as-3 {
0%, 62% {
transform: scale(0.82);
opacity: 0.5;
}
66%, 96% {
transform: scale(1.08);
opacity: 1;
}
100% {
transform: scale(0.82);
opacity: 0.5;
}
}
@media (prefers-reduced-motion: reduce) {
.as__slide {
transition: none;
}
.as.is-demo .as__slide--1,
.as.is-demo .as__slide--2,
.as.is-demo .as__slide--3 {
animation: none !important;
}
}// 실제 동작은 :checked 만으로 끝난다(0줄). 이 3줄은 미리보기 자동 재생만 멈춘다.
document.querySelectorAll('.as__radio').forEach(function (r) {
r.addEventListener('pointerdown', function () { document.querySelector('.as').classList.remove('is-demo'); });
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./ActiveSlideScale.module.scss";
export type ActiveSlideScaleProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function ActiveSlideScale({ duration = 300, easing = "cubic-bezier(.2,.8,.2,1)", color = "#ff4d1f" }: ActiveSlideScaleProps) {
const [active, setActive] = useState(0);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.as} style={vars}>
<div className={styles.track}>
{[0, 1, 2].map((i) => (
<button
key={i}
type="button"
aria-label={`슬라이드 ${i + 1}`}
className={`${styles.slide} ${styles["slide" + (i + 1)]} ${i === active ? styles.isActive : ""}`}
onClick={() => setActive(i)}
/>
))}
</div>
</div>
);
}07 Active slide scale up — Measured area 46.955% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · 9 CSS Only Carousels You Can Copy-Paste
The post this demo lives in
9 CSS Only Carousels You Can Copy-PasteA carousel is a strip of photos that slides across the screen one at a time, usually with dots or arrows to flip through See all demos in this post


