05필터 칩 슬롯 채움
고른 필터 칩이 제자리에서 위쪽 빈 슬롯 테두리로 날아가 딱 맞게 채워지고, 아래 칩 줄은 한 칸씩 밀려 자리를 다시 갖춘다.
쓰임상품 필터 요약 바, 예약 조건 선택
translate 비행체크박스 :checked칩 잔물결

<main class="stage">
<section class="pick is-demo" aria-label="고른 필터 칩이 위 빈 슬롯으로 날아가 채워진다">
<input class="pick__check" type="checkbox" id="pick-1">
<input class="pick__check" type="checkbox" id="pick-2">
<input class="pick__check" type="checkbox" id="pick-3">
<p class="pick__slot"><span class="pick__slot-text">색상을 고르세요</span></p>
<div class="pick__row">
<label class="pick__chip pick__chip--1" for="pick-1">블랙</label>
<label class="pick__chip pick__chip--2" for="pick-2">크림</label>
<label class="pick__chip pick__chip--3" for="pick-3">민트</label>
</div>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: 620ms; // 칩이 슬롯으로 날아가는 시간
$easing: $ease-spring;
$color: $subject-blue; // 포인트색 (칩 보더)
@include stage($stage-ink);
.pick {
position: relative;
width: min(300px, 90vw);
padding-top: $sp-4;
}
.pick__check { position: absolute; opacity: 0; pointer-events: none; }
// ── 위쪽 슬롯: 비어 있을 땐 점선, 칩이 착지하면 덮인다
.pick__slot {
margin: 0 0 $sp-8;
height: 44px;
border: 2px dashed rgba(255, 255, 255, .34);
border-radius: $r-pill;
transition: border-color $duration $easing;
}
.pick__slot-text {
display: grid; place-items: center;
height: 100%;
font-weight: 500;
font-size: clamp(11px, 3vw, 13px);
font-family: inherit;
color: rgba(255, 255, 255, .5);
}
// ── 아래 칩 줄: 크기 고정, transform 으로만 움직인다
.pick__row {
position: relative;
display: flex;
gap: $sp-2;
padding-left: 4px;
}
.pick__chip {
display: grid; place-items: center;
width: 84px;
height: 44px;
box-sizing: border-box;
border-radius: $r-pill;
font-weight: 800;
font-size: clamp(11px, 3vw, 13px);
font-family: inherit;
cursor: pointer;
transition: transform $duration $easing;
z-index: 1;
}
.pick__chip--1 { background: $subject-ink; color: $stage-paper; border: 2px solid $color; }
.pick__chip--2 { background: $subject-cream; color: $stage-ink; }
.pick__chip--3 { background: #d5eae5; color: $stage-ink; }
// 칩마다 슬롯까지의 거리가 다르다 (칩1 x4 → x0, 칩2 x96, 칩3 x188 / 낙차 76px)
#pick-1:checked ~ .pick__row .pick__chip--1 { transform: translate(-4px, -76px); z-index: 2; }
#pick-2:checked ~ .pick__row .pick__chip--2 { transform: translate(-96px, -76px); z-index: 2; }
#pick-3:checked ~ .pick__row .pick__chip--3 { transform: translate(-188px, -76px); z-index: 2; }
#pick-1:checked ~ .pick__slot,
#pick-2:checked ~ .pick__slot,
#pick-3:checked ~ .pick__slot { border-color: rgba(255, 255, 255, .8); }
// 렌더·갤러리용 자동 루프: 칩2 가 날아가 채워졌다 돌아오고, 이어 칩3 도 같은 길을 밟는다
.pick.is-demo .pick__chip--2 { animation: chip2-loop $dur-loop $easing infinite; }
.pick.is-demo .pick__chip--3 { animation: chip3-loop $dur-loop $easing infinite; }
.pick.is-demo .pick__slot { animation: slot-loop $dur-loop $easing infinite; }
@keyframes chip2-loop {
0%, 6% { transform: translate(0, 0); }
26%, 40% { transform: translate(-96px, -76px); }
50%, 100% { transform: translate(0, 0); }
}
@keyframes chip3-loop {
0%, 52% { transform: translate(0, 0); }
72%, 86% { transform: translate(-188px, -76px); }
94%, 100% { transform: translate(0, 0); }
}
@keyframes slot-loop {
0%, 12% { border-color: rgba(255, 255, 255, .34); }
30%, 42% { border-color: rgba(255, 255, 255, .8); }
58%, 68% { border-color: rgba(255, 255, 255, .34); }
76%, 88% { border-color: rgba(255, 255, 255, .8); }
96%, 100% { border-color: rgba(255, 255, 255, .34); }
}
@media (prefers-reduced-motion: reduce) {
.pick__chip, .pick__slot { transition: none; }
.pick.is-demo .pick__chip,
.pick.is-demo .pick__slot { animation: none; } // 칩이 즉시 슬롯에 얹힌다
}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);
}
.pick {
position: relative;
width: min(300px, 90vw);
padding-top: 16px;
}
.pick__check {
position: absolute;
opacity: 0;
pointer-events: none;
}
.pick__slot {
margin: 0 0 32px;
height: 44px;
border: 2px dashed rgba(255, 255, 255, 0.34);
border-radius: 999px;
transition: border-color 620ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.pick__slot-text {
display: grid;
place-items: center;
height: 100%;
font-weight: 500;
font-size: clamp(11px, 3vw, 13px);
font-family: inherit;
color: rgba(255, 255, 255, 0.5);
}
.pick__row {
position: relative;
display: flex;
gap: 8px;
padding-left: 4px;
}
.pick__chip {
display: grid;
place-items: center;
width: 84px;
height: 44px;
box-sizing: border-box;
border-radius: 999px;
font-weight: 800;
font-size: clamp(11px, 3vw, 13px);
font-family: inherit;
cursor: pointer;
transition: transform 620ms cubic-bezier(0.2, 0.8, 0.2, 1);
z-index: 1;
}
.pick__chip--1 {
background: #17141a;
color: #ffffff;
border: 2px solid #2f6df6;
}
.pick__chip--2 {
background: #fff7e6;
color: #17141a;
}
.pick__chip--3 {
background: #d5eae5;
color: #17141a;
}
#pick-1:checked ~ .pick__row .pick__chip--1 {
transform: translate(-4px, -76px);
z-index: 2;
}
#pick-2:checked ~ .pick__row .pick__chip--2 {
transform: translate(-96px, -76px);
z-index: 2;
}
#pick-3:checked ~ .pick__row .pick__chip--3 {
transform: translate(-188px, -76px);
z-index: 2;
}
#pick-1:checked ~ .pick__slot,
#pick-2:checked ~ .pick__slot,
#pick-3:checked ~ .pick__slot {
border-color: rgba(255, 255, 255, 0.8);
}
.pick.is-demo .pick__chip--2 {
animation: chip2-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.pick.is-demo .pick__chip--3 {
animation: chip3-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
.pick.is-demo .pick__slot {
animation: slot-loop 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes chip2-loop {
0%, 6% {
transform: translate(0, 0);
}
26%, 40% {
transform: translate(-96px, -76px);
}
50%, 100% {
transform: translate(0, 0);
}
}
@keyframes chip3-loop {
0%, 52% {
transform: translate(0, 0);
}
72%, 86% {
transform: translate(-188px, -76px);
}
94%, 100% {
transform: translate(0, 0);
}
}
@keyframes slot-loop {
0%, 12% {
border-color: rgba(255, 255, 255, 0.34);
}
30%, 42% {
border-color: rgba(255, 255, 255, 0.8);
}
58%, 68% {
border-color: rgba(255, 255, 255, 0.34);
}
76%, 88% {
border-color: rgba(255, 255, 255, 0.8);
}
96%, 100% {
border-color: rgba(255, 255, 255, 0.34);
}
}
@media (prefers-reduced-motion: reduce) {
.pick__chip, .pick__slot {
transition: none;
}
.pick.is-demo .pick__chip,
.pick.is-demo .pick__slot {
animation: none;
}
}import type { CSSProperties } from "react";
import { useState } from "react";
import styles from "./ChipSlotFill.module.scss";
export type ChipSlotFillProps = {
chips: string[];
placeholder: string;
/** 칩이 슬롯으로 날아가는 시간 (기본 620ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function ChipSlotFill({
chips,
placeholder,
duration = 620,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
}: ChipSlotFillProps) {
const [picked, setPicked] = useState<number | null>(null);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section className={styles.pick} style={vars} aria-label={placeholder}>
<p className={`${styles.slot} ${picked !== null ? styles.slotOn : ""}`}>
{picked === null ? <span className={styles.slotText}>{placeholder}</span> : <span className={styles.slotText}>{chips[picked]}</span>}
</p>
<div className={styles.row}>
{chips.map((name, i) => (
<button
key={name}
type="button"
className={`${styles.chip} ${styles["chip" + (i + 1)]}`}
aria-pressed={picked === i}
data-picked={picked === i}
onClick={() => setPicked(picked === i ? null : i)}
>
{name}
</button>
))}
</div>
</section>
);
}05 필터 칩 슬롯 채움 — 측정 누적면적 12.475% · 움직인 프레임 17/23GODRICH 원작 · zip 에 SCSS·React 포함 · CSS 화면 전환 효과 9종 — 화면이 바뀌는 순간 모음
이 데모가 속한 글
CSS 화면 전환 효과 9종 — 화면이 바뀌는 순간 모음CSS 화면 전환 효과는 페이지를 새로 불러오지 않고 한 화면 안에서 무엇이 다른 모습으로 바뀔 때, 그 바뀜을 부드럽게 눈에 보여 주는 장치입니다. 장이 바뀌듯 화면이 바뀌는 순간마다 붙일 수 있는 아홉 가지를 전 이 글의 데모 전부 보기


