02제거 버튼 달린 태그 칩
칩 오른쪽 × 버튼을 실제로 클릭하면 transform: scale(.6)과 opacity 0으로 줄어들며 DOM에서 사라지고, 데모에서는 세 번째 칩이 같은 축소·소멸 애니메이션을 scale·opacity만으로 스스로 반복해 동작 방식을 보여준다.
쓰임필터·검색에 적용된 선택 카테고리 표시, 멀티 셀렉트 입력
scale+opacity실제 클릭 제거transitionend

<main class="stage is-demo">
<ul class="chips" aria-label="선택된 카테고리">
<li class="chip" data-chip>
<span>Design</span>
<button class="chip__remove" type="button" aria-label="Design 태그 제거">
<svg viewBox="0 0 256 256" width="12" height="12" aria-hidden="true"><g fill="currentColor"><path d="M216,56V200a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V56A16,16,0,0,1,56,40H200A16,16,0,0,1,216,56Z" opacity=".2"/><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"/></g></svg>
</button>
</li>
<li class="chip" data-chip>
<span>Marketing</span>
<button class="chip__remove" type="button" aria-label="Marketing 태그 제거">
<svg viewBox="0 0 256 256" width="12" height="12" aria-hidden="true"><g fill="currentColor"><path d="M216,56V200a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V56A16,16,0,0,1,56,40H200A16,16,0,0,1,216,56Z" opacity=".2"/><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"/></g></svg>
</button>
</li>
<li class="chip chip--demo" data-chip>
<span>Remote</span>
<button class="chip__remove" type="button" aria-label="Remote 태그 제거">
<svg viewBox="0 0 256 256" width="12" height="12" aria-hidden="true"><g fill="currentColor"><path d="M216,56V200a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V56A16,16,0,0,1,56,40H200A16,16,0,0,1,216,56Z" opacity=".2"/><path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"/></g></svg>
</button>
</li>
</ul>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 2s — 제거 루프 한 주기
$easing: $ease-spring;
$color: $subject-pink;
@include stage($stage-yellow);
.chips {
display: flex; flex-wrap: wrap; gap: $sp-2;
margin: 0; padding: 0; list-style: none;
max-width: 260px;
}
.chip {
display: inline-flex; align-items: center; gap: $sp-1;
padding: $sp-1 $sp-1 $sp-1 $sp-3;
border-radius: $r-pill;
background: #fff;
box-shadow: $shadow-press;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 600;
font-size: 13px;
color: $subject-ink;
transform-origin: center;
}
.chip:nth-child(2) { background: mix(#fff, $subject-mint, 55%); }
.chip:nth-child(3) { background: mix(#fff, $subject-lilac, 55%); }
.chip__remove {
display: grid; place-items: center;
width: 20px; height: 20px;
border: 0; border-radius: 50%;
background: rgba($subject-ink, .08);
color: $subject-ink;
padding: 0;
cursor: pointer;
}
.chip__remove:hover { background: rgba($subject-ink, .16); }
// 진짜 클릭 제거 — transform·opacity 만
.chip--gone {
transition: transform $dur-quick $easing, opacity $dur-quick $easing;
transform: scale(.6);
opacity: 0;
}
// 데모 루프 — 세 번째 칩이 지워졌다 다시 나타나길 반복한다 (자동 재생용)
.stage.is-demo .chip--demo {
animation: chip-cycle $duration $easing infinite;
}
@keyframes chip-cycle {
0%, 38% { transform: scale(1); opacity: 1; }
55% { transform: scale(.55); opacity: 0; }
78% { transform: scale(.55); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
// ── 한 겹: 제거 순간 테두리 링 확산 (chip-cycle 55% 소멸 지점에 동기화) ──
.chip--demo { position: relative; }
.chip--demo::after {
content: "";
position: absolute; inset: 0;
border-radius: $r-pill;
border: 2px solid $color;
opacity: 0;
pointer-events: none;
}
.stage.is-demo .chip--demo::after {
animation: chip-ripple $duration $easing infinite;
}
@keyframes chip-ripple {
0%, 38% { opacity: 0; transform: scale(1); }
44% { opacity: .55; transform: scale(1.35); } // 칩이 줄며 사라질 때 링은 바깥으로
56%, 100% { opacity: 0; transform: scale(1.9); }
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .chip--demo { animation: none; }
.stage.is-demo .chip--demo::after { animation: none; opacity: 0; }
.chip--gone { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 0;
padding: 0;
list-style: none;
max-width: 260px;
}
.chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 4px 4px 12px;
border-radius: 999px;
background: #fff;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
font-weight: 600;
font-size: 13px;
color: #17141a;
transform-origin: center;
}
.chip:nth-child(2) {
background: rgb(68.4117647059%, 92.4117647059%, 84.2941176471%);
}
.chip:nth-child(3) {
background: rgb(87.6470588235%, 83.2352941176%, 100%);
}
.chip__remove {
display: grid;
place-items: center;
width: 20px;
height: 20px;
border: 0;
border-radius: 50%;
background: rgba(23, 20, 26, 0.08);
color: #17141a;
padding: 0;
cursor: pointer;
}
.chip__remove:hover {
background: rgba(23, 20, 26, 0.16);
}
.chip--gone {
transition: transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1), opacity 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
transform: scale(0.6);
opacity: 0;
}
.stage.is-demo .chip--demo {
animation: chip-cycle 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes chip-cycle {
0%, 38% {
transform: scale(1);
opacity: 1;
}
55% {
transform: scale(0.55);
opacity: 0;
}
78% {
transform: scale(0.55);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.chip--demo {
position: relative;
}
.chip--demo::after {
content: "";
position: absolute;
inset: 0;
border-radius: 999px;
border: 2px solid #ff9ebb;
opacity: 0;
pointer-events: none;
}
.stage.is-demo .chip--demo::after {
animation: chip-ripple 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes chip-ripple {
0%, 38% {
opacity: 0;
transform: scale(1);
}
44% {
opacity: 0.55;
transform: scale(1.35);
}
56%, 100% {
opacity: 0;
transform: scale(1.9);
}
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .chip--demo {
animation: none;
}
.stage.is-demo .chip--demo::after {
animation: none;
opacity: 0;
}
.chip--gone {
transition: none;
}
}// 실제 클릭 제거 — 데모 밖에서도 그대로 동작한다
document.querySelectorAll('.chip__remove').forEach(function (btn) {
btn.addEventListener('click', function () {
var chip = btn.closest('.chip');
if (!chip || chip.classList.contains('chip--gone')) return;
chip.classList.remove('chip--demo');
chip.classList.add('chip--gone');
chip.addEventListener('transitionend', function () { chip.remove(); }, { once: true });
});
});import { useState, type CSSProperties } from "react";
import styles from "./TagChipRemove.module.scss";
export type TagChipRemoveProps = {
duration?: number;
easing?: string;
color?: string;
};
const INITIAL = ["Design", "Marketing", "Remote"];
export default function TagChipRemove({
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#ff9ebb",
}: TagChipRemoveProps) {
const [tags, setTags] = useState(INITIAL);
const [leaving, setLeaving] = useState<string | null>(null);
const vars: CSSProperties = { "--duration": `${duration}ms`, "--easing": easing, "--color": color } as CSSProperties;
function remove(tag: string) {
setLeaving(tag);
window.setTimeout(() => {
setTags((t) => t.filter((x) => x !== tag));
setLeaving(null);
}, 220);
}
return (
<ul className={styles.chips} style={vars} aria-label="selected categories">
{tags.map((tag, i) => (
<li
key={tag}
className={[styles.chip, i === 2 ? styles.demo : "", leaving === tag ? styles.gone : ""].join(" ")}
>
<span>{tag}</span>
<button
type="button"
className={styles.remove}
aria-label={`remove ${tag}`}
onClick={() => remove(tag)}
>
<svg viewBox="0 0 256 256" width={12} height={12} aria-hidden="true">
<g fill="currentColor">
<path d="M216,56V200a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V56A16,16,0,0,1,56,40H200A16,16,0,0,1,216,56Z" opacity=".2" />
<path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z" />
</g>
</svg>
</button>
</li>
))}
</ul>
);
}02 제거 버튼 달린 태그 칩 — 측정 누적면적 2.362% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 태그 UI 디자인 9종 — 상태·우선순위·인증 배지 코드
이 데모가 속한 글
태그 UI 디자인 9종 — 상태·우선순위·인증 배지 코드태그 UI 디자인은 상태·카테고리·우선순위 같은 정보를 작은 알약이나 칩 모양으로 압축해 보여주는 장식이다. 온라인 점 하나, 우선순위 색 하나만 봐도 상태를 바로 알 수 있고, 움직임을 더하면 그대로 붙여서 훨씬 이 글의 데모 전부 보기


