05칩형 필터 바
칩을 누르면 실제로 눌린 상태(색·아이콘)로 바뀌고 오른쪽에 선택 개수가 올라가며, 하나라도 선택되면 '전체 해제' 칩이 나타납니다.
쓰임목록 위에 검색·필터 바 하나만 따로 붙이고 싶을 때
aria-pressedtransformclick

<main class="stage">
<div class="fl">
<div class="fl__row">
<button class="fl__chip is-demo" type="button" aria-pressed="false">진행중</button>
<button class="fl__chip is-demo" type="button" aria-pressed="false">완료</button>
<button class="fl__chip is-demo" type="button" aria-pressed="false">보류</button>
<button class="fl__chip is-demo" type="button" aria-pressed="false">긴급</button>
<button class="fl__clear" type="button" hidden>전체 해제 ✕</button>
</div>
<p class="fl__count"><span class="fl__count-num">0</span>개 선택됨</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-quick;
$easing: $ease-pop;
$color: $subject-blue;
@include stage($stage-orange);
.fl {
box-sizing: border-box;
width: min(400px, 90vw);
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.fl__row { display: flex; flex-wrap: wrap; gap: $sp-2; align-items: center; }
.fl__chip {
box-sizing: border-box;
border: 1px solid rgba(23, 20, 26, .4);
background: transparent;
color: $subject-ink;
padding: $sp-2 $sp-4;
border-radius: $r-pill;
font: inherit; font-weight: 700; font-size: 12px;
cursor: pointer;
transition: background $duration $easing, border-color $duration $easing, transform $duration $easing;
}
.fl__chip[aria-pressed="true"] {
background: $subject-cream;
border-color: $subject-cream;
color: $subject-orange-deep;
} // 크림 면 위 deep 5.04:1
.fl__clear {
border: 0; background: transparent; cursor: pointer;
color: rgba(23, 20, 26, .8);
font: inherit; font-weight: 700; font-size: 11px;
text-decoration: underline;
padding: $sp-2 $sp-1;
}
.fl__count { margin: $sp-3 0 0; font-size: 12px; font-weight: 600; color: $subject-ink; } // 주황 면 위 알파 잉크 .72 는 3.76:1 — 알파를 빼야 5.50:1 (패턴 ①·주황 면 예외)
.fl__count-num { color: $subject-ink; font-weight: 800; }
// ── 자동 루프: 칩이 하나씩 순서대로 눌렸다 풀린다 ──────────────
.fl__chip.is-demo { animation: fl-press 2s $easing infinite; }
.fl__chip.is-demo:nth-child(1) { animation-delay: 0s; }
.fl__chip.is-demo:nth-child(2) { animation-delay: .3s; }
.fl__chip.is-demo:nth-child(3) { animation-delay: .6s; }
.fl__chip.is-demo:nth-child(4) { animation-delay: .9s; }
@keyframes fl-press {
0%, 100% { transform: scale(1); background: transparent; border-color: rgba(23, 20, 26, .4); color: $subject-ink; }
20% { transform: scale(1.06); background: $subject-cream; border-color: $subject-cream; color: $subject-orange-deep; }
40% { transform: scale(1); background: $subject-cream; border-color: $subject-cream; color: $subject-orange-deep; }
}
@media (max-width: 340px) {
.fl__chip { padding: $sp-1 $sp-3; font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.fl__chip.is-demo { animation: none !important; }
.fl__chip { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.fl {
box-sizing: border-box;
width: min(400px, 90vw);
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
}
.fl__row {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.fl__chip {
box-sizing: border-box;
border: 1px solid rgba(23, 20, 26, 0.4);
background: transparent;
color: #17141a;
padding: 8px 16px;
border-radius: 999px;
font: inherit;
font-weight: 700;
font-size: 12px;
cursor: pointer;
transition: background 220ms cubic-bezier(0.2, 1.4, 0.4, 1), border-color 220ms cubic-bezier(0.2, 1.4, 0.4, 1), transform 220ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.fl__chip[aria-pressed=true] {
background: #fff7e6;
border-color: #fff7e6;
color: #c93000;
}
.fl__clear {
border: 0;
background: transparent;
cursor: pointer;
color: rgba(23, 20, 26, 0.8);
font: inherit;
font-weight: 700;
font-size: 11px;
text-decoration: underline;
padding: 8px 4px;
}
.fl__count {
margin: 12px 0 0;
font-size: 12px;
font-weight: 600;
color: #17141a;
}
.fl__count-num {
color: #17141a;
font-weight: 800;
}
.fl__chip.is-demo {
animation: fl-press 2s cubic-bezier(0.2, 1.4, 0.4, 1) infinite;
}
.fl__chip.is-demo:nth-child(1) {
animation-delay: 0s;
}
.fl__chip.is-demo:nth-child(2) {
animation-delay: 0.3s;
}
.fl__chip.is-demo:nth-child(3) {
animation-delay: 0.6s;
}
.fl__chip.is-demo:nth-child(4) {
animation-delay: 0.9s;
}
@keyframes fl-press {
0%, 100% {
transform: scale(1);
background: transparent;
border-color: rgba(23, 20, 26, 0.4);
color: #17141a;
}
20% {
transform: scale(1.06);
background: #fff7e6;
border-color: #fff7e6;
color: #c93000;
}
40% {
transform: scale(1);
background: #fff7e6;
border-color: #fff7e6;
color: #c93000;
}
}
@media (max-width: 340px) {
.fl__chip {
padding: 4px 12px;
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.fl__chip.is-demo {
animation: none !important;
}
.fl__chip {
transition: none;
}
}// 실제 동작: 칩을 누르면 실제로 눌린 상태로 바뀌고 개수가 갱신되며, 하나 이상이면 '전체 해제'가 나타난다.
var chips = document.querySelectorAll('.fl__chip');
var clearBtn = document.querySelector('.fl__clear');
var countEl = document.querySelector('.fl__count-num');
function refresh() {
var n = document.querySelectorAll('.fl__chip[aria-pressed="true"]').length;
countEl.textContent = String(n);
clearBtn.hidden = n === 0;
}
chips.forEach(function (c) {
c.addEventListener('click', function () {
c.classList.remove('is-demo');
var on = c.getAttribute('aria-pressed') === 'true';
c.setAttribute('aria-pressed', String(!on));
refresh();
});
});
clearBtn.addEventListener('click', function () {
chips.forEach(function (c) { c.setAttribute('aria-pressed', 'false'); });
refresh();
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./Filters.module.scss";
export type FiltersProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const CHIPS = ["진행중", "완료", "보류", "긴급"];
export default function Filters({ duration = 220, easing = "cubic-bezier(.2,1.4,.4,1)", color = "#2f6df6" }: FiltersProps) {
const [selected, setSelected] = useState<Set<string>>(new Set());
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
function toggle(chip: string) {
setSelected((s) => {
const next = new Set(s);
next.has(chip) ? next.delete(chip) : next.add(chip);
return next;
});
}
return (
<div className={styles.fl} style={vars}>
<div className={styles.row}>
{CHIPS.map((chip) => (
<button
key={chip}
type="button"
className={styles.chip}
aria-pressed={selected.has(chip)}
onClick={() => toggle(chip)}
>
{chip}
</button>
))}
{selected.size > 0 && (
<button type="button" className={styles.clear} onClick={() => setSelected(new Set())}>
전체 해제 ✕
</button>
)}
</div>
<p className={styles.count}><span className={styles.countNum}>{selected.size}</span>개 선택됨</p>
</div>
);
}05 칩형 필터 바 — 측정 누적면적 5.824% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 대시보드 사이드바·위젯 UI 9종 — 완성 페이지 말고 이 조각 하나만
이 데모가 속한 글
대시보드 사이드바·위젯 UI 9종 — 완성 페이지 말고 이 조각 하나만대시보드 사이드바 하나, 통계 카드 한 줄, 알림 드롭다운 하나처럼 완성된 관리자 페이지 전체가 아니라 화면 한 조각만 그대로 가져다 붙이는 대시보드 위젯 아홉 개를 모았습니다. 아홉 개 중 하나는 숫자가 실제로 세 이 글의 데모 전부 보기


