05Shift·Ctrl 다중 선택
한 줄을 고른 뒤 Shift로 범위를 잡고 Ctrl로 골라 담으면 고른 줄들에 배경이 차례로 쌓이고 개수가 위에 집계됩니다.
쓰임여러 파일을 한 번에 내보내거나 옮기거나 지워야 하는 자료 목록에서 쓰는 선택 방식
shiftKeyctrlKeyaria-multiselectable

<main class="stage">
<div class="ms is-demo" id="ms">
<div class="ms__head">
<span class="ms__title" id="ms-title">자료 6개</span>
<span class="ms__badge" aria-live="polite">
<span class="ms__b ms__b--0">0개 선택</span>
<span class="ms__b ms__b--1">1개 선택</span>
<span class="ms__b ms__b--4">4개 선택</span>
<span class="ms__breal" id="ms-badge" hidden>0개 선택</span>
</span>
</div>
<ul class="ms__list" id="ms-list" role="tree" aria-label="자료" aria-multiselectable="true" tabindex="0" aria-activedescendant="ms-1">
<li class="ms__item" id="ms-1" role="treeitem" aria-level="1" aria-selected="false">
<div class="ms__row" data-hit><span class="ms__ico" aria-hidden="true"><svg viewBox="0 0 256 256" width="13" height="13"><g fill="currentColor"><path d="m104 152l48 72H24l36-56l16.36 25.45Zm48-120v56h56Z" opacity=".2"/><path d="M110.66 147.56a8 8 0 0 0-13.32 0l-20.85 31.29l-9.76-15.18a8 8 0 0 0-13.46 0l-36 56A8 8 0 0 0 24 232h128a8 8 0 0 0 6.66-12.44ZM38.65 216L60 182.79l9.63 15a8 8 0 0 0 13.39.11l21-31.47L137.05 216Zm175-133.66l-56-56A8 8 0 0 0 152 24H56a16 16 0 0 0-16 16v88a8 8 0 0 0 16 0V40h88v48a8 8 0 0 0 8 8h48v120h-8a8 8 0 0 0 0 16h8a16 16 0 0 0 16-16V88a8 8 0 0 0-2.34-5.66ZM160 51.31L188.69 80H160Z"/></g></svg></span><span class="ms__name">hero.jpg</span><span class="ms__ok" aria-hidden="true"><svg viewBox="0 0 256 256" width="12" height="12"><g fill="currentColor"><path d="M232 56v144a16 16 0 0 1-16 16H40a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16" opacity=".2"/><path d="m205.66 85.66l-96 96a8 8 0 0 1-11.32 0l-40-40a8 8 0 0 1 11.32-11.32L104 164.69l90.34-90.35a8 8 0 0 1 11.32 11.32"/></g></svg></span></div>
</li>
<li class="ms__item is-a" id="ms-2" role="treeitem" aria-level="1" aria-selected="false">
<div class="ms__row" data-hit><span class="ms__ico" aria-hidden="true"><svg viewBox="0 0 256 256" width="13" height="13"><g fill="currentColor"><path d="M208 88h-56V32Z" opacity=".2"/><path d="M52 144H36a8 8 0 0 0-8 8v56a8 8 0 0 0 8 8h16a36 36 0 0 0 0-72m0 56h-8v-40h8a20 20 0 0 1 0 40m169.53-4.91a8 8 0 0 1 .25 11.31A30.06 30.06 0 0 1 200 216c-17.65 0-32-16.15-32-36s14.35-36 32-36a30.06 30.06 0 0 1 21.78 9.6a8 8 0 0 1-11.56 11.06A14.24 14.24 0 0 0 200 160c-8.82 0-16 9-16 20s7.18 20 16 20a14.18 14.18 0 0 0 10.22-4.66a8 8 0 0 1 11.31-.25M128 144c-17.64 0-32 16.15-32 36s14.36 36 32 36s32-16.15 32-36s-14.36-36-32-36m0 56c-8.82 0-16-9-16-20s7.18-20 16-20s16 9 16 20s-7.18 20-16 20m-80-80a8 8 0 0 0 8-8V40h88v48a8 8 0 0 0 8 8h48v16a8 8 0 0 0 16 0V88a8 8 0 0 0-2.34-5.66l-56-56A8 8 0 0 0 152 24H56a16 16 0 0 0-16 16v72a8 8 0 0 0 8 8m112-68.69L188.69 80H160Z"/></g></svg></span><span class="ms__name">resume.pdf</span><span class="ms__ok" aria-hidden="true"><svg viewBox="0 0 256 256" width="12" height="12"><g fill="currentColor"><path d="M232 56v144a16 16 0 0 1-16 16H40a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16" opacity=".2"/><path d="m205.66 85.66l-96 96a8 8 0 0 1-11.32 0l-40-40a8 8 0 0 1 11.32-11.32L104 164.69l90.34-90.35a8 8 0 0 1 11.32 11.32"/></g></svg></span></div>
</li>
<li class="ms__item is-b" style="--i: 0" id="ms-3" role="treeitem" aria-level="1" aria-selected="false">
<div class="ms__row" data-hit><span class="ms__ico" aria-hidden="true"><svg viewBox="0 0 256 256" width="13" height="13"><g fill="currentColor"><path d="M208 88h-56V32Z" opacity=".2"/><path d="M184 144h-16a8 8 0 0 0-8 8v56a8 8 0 0 0 16 0v-8h8a28 28 0 0 0 0-56m0 40h-8v-24h8a12 12 0 0 1 0 24m-48-32v56a8 8 0 0 1-16 0v-56a8 8 0 0 1 16 0m-40 56a8 8 0 0 1-8 8H56a8 8 0 0 1-7-12l25.16-44H56a8 8 0 0 1 0-16h32a8 8 0 0 1 7 12l-25.21 44H88a8 8 0 0 1 8 8M213.66 82.34l-56-56A8 8 0 0 0 152 24H56a16 16 0 0 0-16 16v72a8 8 0 0 0 16 0V40h88v48a8 8 0 0 0 8 8h48v16a8 8 0 0 0 16 0V88a8 8 0 0 0-2.34-5.66M160 80V51.31L188.69 80Z"/></g></svg></span><span class="ms__name">bundle.zip</span><span class="ms__ok" aria-hidden="true"><svg viewBox="0 0 256 256" width="12" height="12"><g fill="currentColor"><path d="M232 56v144a16 16 0 0 1-16 16H40a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16" opacity=".2"/><path d="m205.66 85.66l-96 96a8 8 0 0 1-11.32 0l-40-40a8 8 0 0 1 11.32-11.32L104 164.69l90.34-90.35a8 8 0 0 1 11.32 11.32"/></g></svg></span></div>
</li>
<li class="ms__item is-b" style="--i: 1" id="ms-4" role="treeitem" aria-level="1" aria-selected="false">
<div class="ms__row" data-hit><span class="ms__ico" aria-hidden="true"><svg viewBox="0 0 256 256" width="13" height="13"><g fill="currentColor"><path d="M208 88h-56V32Z" opacity=".2"/><path d="m213.66 82.34l-56-56A8 8 0 0 0 152 24H56a16 16 0 0 0-16 16v72a8 8 0 0 0 16 0V40h88v48a8 8 0 0 0 8 8h48v120h-24a8 8 0 0 0 0 16h24a16 16 0 0 0 16-16V88a8 8 0 0 0-2.34-5.66M160 51.31L188.69 80H160Zm-12.19 145a20.82 20.82 0 0 1-9.19 15.23C133.43 215 127 216 121.13 216a61 61 0 0 1-15.13-2a8 8 0 1 1 4.3-15.41c4.38 1.2 15 2.7 19.55-.36c.88-.59 1.83-1.52 2.14-3.93c.35-2.67-.71-4.1-12.78-7.59c-9.35-2.7-25-7.23-23-23.11a20.56 20.56 0 0 1 9-14.95c11.84-8 30.71-3.31 32.83-2.76a8 8 0 0 1-4.07 15.48c-4.49-1.17-15.23-2.56-19.83.56a4.54 4.54 0 0 0-2 3.67c-.12.9-.14 1.09 1.11 1.9c2.31 1.49 6.45 2.68 10.45 3.84c9.79 2.83 26.35 7.66 24.11 24.97M80 152v38a26 26 0 0 1-52 0a8 8 0 0 1 16 0a10 10 0 0 0 20 0v-38a8 8 0 0 1 16 0"/></g></svg></span><span class="ms__name">main.js</span><span class="ms__ok" aria-hidden="true"><svg viewBox="0 0 256 256" width="12" height="12"><g fill="currentColor"><path d="M232 56v144a16 16 0 0 1-16 16H40a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16" opacity=".2"/><path d="m205.66 85.66l-96 96a8 8 0 0 1-11.32 0l-40-40a8 8 0 0 1 11.32-11.32L104 164.69l90.34-90.35a8 8 0 0 1 11.32 11.32"/></g></svg></span></div>
</li>
<li class="ms__item is-b" style="--i: 2" id="ms-5" role="treeitem" aria-level="1" aria-selected="false">
<div class="ms__row" data-hit><span class="ms__ico" aria-hidden="true"><svg viewBox="0 0 256 256" width="13" height="13"><g fill="currentColor"><path d="M208 88h-56V32Z" opacity=".2"/><path d="M48 180c0 11 7.18 20 16 20a14.2 14.2 0 0 0 10.22-4.66a8 8 0 1 1 11.55 11.06A30 30 0 0 1 64 216c-17.65 0-32-16.15-32-36s14.35-36 32-36a30 30 0 0 1 21.77 9.6a8 8 0 1 1-11.55 11.06A14.24 14.24 0 0 0 64 160c-8.82 0-16 9-16 20m79.6-8.69c-4-1.16-8.14-2.35-10.45-3.84c-1.26-.81-1.23-1-1.12-1.9a4.54 4.54 0 0 1 2-3.67c4.6-3.12 15.35-1.73 19.83-.56a8 8 0 0 0 4.07-15.48c-2.12-.55-21-5.22-32.83 2.76a20.55 20.55 0 0 0-9 14.95c-2 15.88 13.64 20.41 23 23.11c12.07 3.49 13.13 4.92 12.78 7.59c-.31 2.41-1.26 3.34-2.14 3.93c-4.6 3.06-15.17 1.56-19.55.36a8 8 0 0 0-4.3 15.41a61.2 61.2 0 0 0 15.18 2c5.83 0 12.3-1 17.49-4.46a20.82 20.82 0 0 0 9.19-15.23c2.25-17.28-14.27-22.11-24.15-24.97m64 0c-4-1.16-8.14-2.35-10.45-3.84c-1.25-.81-1.23-1-1.12-1.9a4.54 4.54 0 0 1 2-3.67c4.6-3.12 15.34-1.73 19.82-.56a8 8 0 0 0 4.07-15.48c-2.11-.55-21-5.22-32.83 2.76a20.58 20.58 0 0 0-8.95 14.95c-2 15.88 13.65 20.41 23 23.11c12.06 3.49 13.12 4.92 12.78 7.59c-.31 2.41-1.26 3.34-2.15 3.93c-4.6 3.06-15.16 1.56-19.54.36a8 8 0 0 0-4.3 15.44a61.3 61.3 0 0 0 15.19 2c5.82 0 12.3-1 17.49-4.46a20.81 20.81 0 0 0 9.18-15.23c2.21-17.31-14.31-22.14-24.2-25ZM40 112V40a16 16 0 0 1 16-16h96a8 8 0 0 1 5.66 2.34l56 56A8 8 0 0 1 216 88v24a8 8 0 1 1-16 0V96h-48a8 8 0 0 1-8-8V40H56v72a8 8 0 0 1-16 0m120-32h28.68L160 51.31Z"/></g></svg></span><span class="ms__name">style.css</span><span class="ms__ok" aria-hidden="true"><svg viewBox="0 0 256 256" width="12" height="12"><g fill="currentColor"><path d="M232 56v144a16 16 0 0 1-16 16H40a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16" opacity=".2"/><path d="m205.66 85.66l-96 96a8 8 0 0 1-11.32 0l-40-40a8 8 0 0 1 11.32-11.32L104 164.69l90.34-90.35a8 8 0 0 1 11.32 11.32"/></g></svg></span></div>
</li>
<li class="ms__item" id="ms-6" role="treeitem" aria-level="1" aria-selected="false">
<div class="ms__row" data-hit><span class="ms__ico" aria-hidden="true"><svg viewBox="0 0 256 256" width="13" height="13"><g fill="currentColor"><path d="M208 88h-56V32Z" opacity=".2"/><path d="m213.66 82.34l-56-56A8 8 0 0 0 152 24H56a16 16 0 0 0-16 16v72a8 8 0 0 0 16 0V40h88v48a8 8 0 0 0 8 8h48v128a8 8 0 0 0 16 0V88a8 8 0 0 0-2.34-5.66M160 51.31L188.69 80H160ZM144 144h-16a8 8 0 0 0-8 8v56a8 8 0 0 0 8 8h16a36 36 0 0 0 0-72m0 56h-8v-40h8a20 20 0 0 1 0 40m-40-48v56a8 8 0 0 1-16 0v-30.62l-13.45 19.21a8 8 0 0 1-13.1 0L48 177.38V208a8 8 0 0 1-16 0v-56a8 8 0 0 1 14.55-4.59L68 178.05l21.45-30.64A8 8 0 0 1 104 152"/></g></svg></span><span class="ms__name">notes.md</span><span class="ms__ok" aria-hidden="true"><svg viewBox="0 0 256 256" width="12" height="12"><g fill="currentColor"><path d="M232 56v144a16 16 0 0 1-16 16H40a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16" opacity=".2"/><path d="m205.66 85.66l-96 96a8 8 0 0 1-11.32 0l-40-40a8 8 0 0 1 11.32-11.32L104 164.69l90.34-90.35a8 8 0 0 1 11.32 11.32"/></g></svg></span></div>
</li>
</ul>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-quick;
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-ink);
.ms {
box-sizing: border-box;
width: min(268px, 88vw);
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: #fff;
}
.ms__head {
display: flex; align-items: center; justify-content: space-between;
margin: 0 0 $sp-2;
}
.ms__title {
font-size: 10px; line-height: 12px; font-weight: 700;
letter-spacing: .04em;
color: rgba(255, 255, 255, .6);
}
.ms__badge {
position: relative;
box-sizing: border-box;
height: 20px;
padding: 0 $sp-2;
border-radius: $r-pill;
background: rgba(255, 255, 255, .1);
display: inline-flex; align-items: center; justify-content: flex-end;
overflow: hidden;
}
.ms__b, .ms__breal {
position: absolute; right: $sp-2;
font-size: 10px; line-height: 20px; font-weight: 800;
white-space: nowrap;
color: rgba(255, 255, 255, .75);
}
.ms__b--1, .ms__b--4 { color: $stage-yellow; }
.ms__b { opacity: 0; }
.ms__b--0 { opacity: 1; }
.ms__list { list-style: none; margin: 0; padding: 0; }
.ms__list:focus-visible { outline: none; }
.ms__row {
box-sizing: border-box;
height: 22px;
display: flex; align-items: center; gap: $sp-2;
padding: 0 $sp-2;
margin-bottom: $sp-1;
border-radius: $r-xs;
background: rgba(255, 255, 255, .05);
font-size: 12px; font-weight: 600;
cursor: pointer;
user-select: none;
transition: background $duration $easing;
}
.ms__row:hover { background: rgba(255, 255, 255, .1); }
.ms__item.is-sel .ms__row { background: rgba(47, 109, 246, .3); }
.ms__ico { display: inline-flex; flex: 0 0 auto; color: rgba(255, 255, 255, .8); }
.ms__name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ms__ok {
margin-left: auto;
display: inline-flex; flex: 0 0 auto;
color: $stage-yellow;
opacity: 0;
transform: scale(.6);
transition: opacity $duration $easing, transform $duration $ease-pop;
}
.ms__item.is-sel .ms__ok { opacity: 1; transform: scale(1); }
// ── 자동 루프: 한 줄 고르고 → Shift 범위로 네 줄 → 풀린다 ──
.ms.is-demo .ms__item.is-a .ms__row {
animation-name: ms-sela;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes ms-sela {
0%, 9.9% { background: rgba(255, 255, 255, .05); }
10%, 82% { background: rgba(47, 109, 246, .3); }
82.01%, 100% { background: rgba(255, 255, 255, .05); }
}
.ms.is-demo .ms__item.is-a .ms__ok {
animation-name: ms-oka;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes ms-oka { 0%, 9.9% { opacity: 0; } 10%, 82% { opacity: 1; } 82.01%, 100% { opacity: 0; } }
.ms.is-demo .ms__item.is-b .ms__row {
animation-name: ms-selb;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-delay: calc(var(--i) * 90ms);
}
@keyframes ms-selb {
0%, 21.9% { background: rgba(255, 255, 255, .05); }
22%, 82% { background: rgba(47, 109, 246, .3); }
82.01%, 100% { background: rgba(255, 255, 255, .05); }
}
.ms.is-demo .ms__item.is-b .ms__ok {
animation-name: ms-okb;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-delay: calc(var(--i) * 90ms);
}
@keyframes ms-okb { 0%, 21.9% { opacity: 0; } 22%, 82% { opacity: 1; } 82.01%, 100% { opacity: 0; } }
.ms.is-demo .ms__b--0 {
animation-name: ms-b0;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes ms-b0 { 0% { opacity: 1; } 13% { opacity: 0; } 89% { opacity: 1; } 100% { opacity: 1; } }
.ms.is-demo .ms__b--1 {
animation-name: ms-b1;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes ms-b1 { 0% { opacity: 0; } 13% { opacity: 1; } 37% { opacity: 0; } 100% { opacity: 0; } }
.ms.is-demo .ms__b--4 {
animation-name: ms-b4;
animation-duration: $dur-loop;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes ms-b4 { 0% { opacity: 0; } 37% { opacity: 1; } 89% { opacity: 0; } 100% { opacity: 0; } }
@media (max-width: 340px) {
.ms__row { height: 20px; font-size: 11px; }
}
@media (prefers-reduced-motion: reduce) {
.ms.is-demo .ms__row, .ms.is-demo .ms__ok, .ms.is-demo .ms__b { animation: none !important; }
.ms__row, .ms__ok { transition: 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);
}
.ms {
box-sizing: border-box;
width: min(268px, 88vw);
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: #fff;
}
.ms__head {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 0 8px;
}
.ms__title {
font-size: 10px;
line-height: 12px;
font-weight: 700;
letter-spacing: 0.04em;
color: rgba(255, 255, 255, 0.6);
}
.ms__badge {
position: relative;
box-sizing: border-box;
height: 20px;
padding: 0 8px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.1);
display: inline-flex;
align-items: center;
justify-content: flex-end;
overflow: hidden;
}
.ms__b, .ms__breal {
position: absolute;
right: 8px;
font-size: 10px;
line-height: 20px;
font-weight: 800;
white-space: nowrap;
color: rgba(255, 255, 255, 0.75);
}
.ms__b--1, .ms__b--4 {
color: #ffd23f;
}
.ms__b {
opacity: 0;
}
.ms__b--0 {
opacity: 1;
}
.ms__list {
list-style: none;
margin: 0;
padding: 0;
}
.ms__list:focus-visible {
outline: none;
}
.ms__row {
box-sizing: border-box;
height: 22px;
display: flex;
align-items: center;
gap: 8px;
padding: 0 8px;
margin-bottom: 4px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.05);
font-size: 12px;
font-weight: 600;
cursor: pointer;
user-select: none;
transition: background 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.ms__row:hover {
background: rgba(255, 255, 255, 0.1);
}
.ms__item.is-sel .ms__row {
background: rgba(47, 109, 246, 0.3);
}
.ms__ico {
display: inline-flex;
flex: 0 0 auto;
color: rgba(255, 255, 255, 0.8);
}
.ms__name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ms__ok {
margin-left: auto;
display: inline-flex;
flex: 0 0 auto;
color: #ffd23f;
opacity: 0;
transform: scale(0.6);
transition: opacity 220ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 220ms cubic-bezier(0.2, 1.4, 0.4, 1);
}
.ms__item.is-sel .ms__ok {
opacity: 1;
transform: scale(1);
}
.ms.is-demo .ms__item.is-a .ms__row {
animation-name: ms-sela;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes ms-sela {
0%, 9.9% {
background: rgba(255, 255, 255, 0.05);
}
10%, 82% {
background: rgba(47, 109, 246, 0.3);
}
82.01%, 100% {
background: rgba(255, 255, 255, 0.05);
}
}
.ms.is-demo .ms__item.is-a .ms__ok {
animation-name: ms-oka;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes ms-oka {
0%, 9.9% {
opacity: 0;
}
10%, 82% {
opacity: 1;
}
82.01%, 100% {
opacity: 0;
}
}
.ms.is-demo .ms__item.is-b .ms__row {
animation-name: ms-selb;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-delay: calc(var(--i) * 90ms);
}
@keyframes ms-selb {
0%, 21.9% {
background: rgba(255, 255, 255, 0.05);
}
22%, 82% {
background: rgba(47, 109, 246, 0.3);
}
82.01%, 100% {
background: rgba(255, 255, 255, 0.05);
}
}
.ms.is-demo .ms__item.is-b .ms__ok {
animation-name: ms-okb;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-delay: calc(var(--i) * 90ms);
}
@keyframes ms-okb {
0%, 21.9% {
opacity: 0;
}
22%, 82% {
opacity: 1;
}
82.01%, 100% {
opacity: 0;
}
}
.ms.is-demo .ms__b--0 {
animation-name: ms-b0;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes ms-b0 {
0% {
opacity: 1;
}
13% {
opacity: 0;
}
89% {
opacity: 1;
}
100% {
opacity: 1;
}
}
.ms.is-demo .ms__b--1 {
animation-name: ms-b1;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes ms-b1 {
0% {
opacity: 0;
}
13% {
opacity: 1;
}
37% {
opacity: 0;
}
100% {
opacity: 0;
}
}
.ms.is-demo .ms__b--4 {
animation-name: ms-b4;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes ms-b4 {
0% {
opacity: 0;
}
37% {
opacity: 1;
}
89% {
opacity: 0;
}
100% {
opacity: 0;
}
}
@media (max-width: 340px) {
.ms__row {
height: 20px;
font-size: 11px;
}
}
@media (prefers-reduced-motion: reduce) {
.ms.is-demo .ms__row, .ms.is-demo .ms__ok, .ms.is-demo .ms__b {
animation: none !important;
}
.ms__row, .ms__ok {
transition: none;
}
}// 실제 동작: 클릭 한 개, Ctrl+클릭 토글, Shift+클릭 범위.
var root = document.querySelector('.ms');
var list = document.getElementById('ms-list');
var badge = document.getElementById('ms-badge');
var items = [].slice.call(list.querySelectorAll('.ms__item'));
var anchor = 0;
function sync() {
var n = 0;
items.forEach(function (li, i) {
var on = li.classList.contains('is-sel');
li.setAttribute('aria-selected', String(on));
if (on) { n++; list.setAttribute('aria-activedescendant', li.id); }
});
badge.textContent = n + '개 선택';
}
items.forEach(function (li, i) {
li.querySelector('[data-hit]').addEventListener('click', function (e) {
root.classList.remove('is-demo');
document.querySelector('.ms__badge').querySelectorAll('.ms__b').forEach(function (b) { b.hidden = true; });
badge.hidden = false;
if (e.shiftKey) {
var a = Math.min(anchor, i), z = Math.max(anchor, i);
items.forEach(function (l, k) { l.classList.toggle('is-sel', k >= a && k <= z); });
} else if (e.ctrlKey || e.metaKey) {
li.classList.toggle('is-sel');
anchor = i;
} else {
items.forEach(function (l) { l.classList.remove('is-sel'); });
li.classList.add('is-sel');
anchor = i;
}
sync();
});
});
list.addEventListener('keydown', function (e) {
var at = items.indexOf(document.getElementById(list.getAttribute('aria-activedescendant')));
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
var li = items[Math.max(0, Math.min(at + (e.key === 'ArrowDown' ? 1 : -1), items.length - 1))];
list.setAttribute('aria-activedescendant', li.id);
e.preventDefault();
} else if (e.key === ' ' && at >= 0) {
root.classList.remove('is-demo');
items[at].classList.toggle('is-sel');
anchor = at;
sync();
e.preventDefault();
}
});import { useRef, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./MultiSelectTree.module.scss";
export type FileKind = "img" | "pdf" | "zip" | "js" | "css" | "md";
export type FileItem = { name: string; kind: FileKind };
export type MultiSelectTreeProps = {
label?: string;
items?: FileItem[];
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type KeyEvt = { key: string; preventDefault: () => void };
type ClickEvt = { shiftKey: boolean; ctrlKey: boolean; metaKey: boolean };
type Glyph = { dim: string; solid: string };
const PAGE = "M208 88h-56V32Z";
const GLYPH: Record<FileKind, Glyph> = {
img: {
dim: "m104 152l48 72H24l36-56l16.36 25.45Zm48-120v56h56Z",
solid: "M110.66 147.56a8 8 0 0 0-13.32 0l-20.85 31.29l-9.76-15.18a8 8 0 0 0-13.46 0l-36 56A8 8 0 0 0 24 232h128a8 8 0 0 0 6.66-12.44ZM38.65 216L60 182.79l9.63 15a8 8 0 0 0 13.39.11l21-31.47L137.05 216Zm175-133.66l-56-56A8 8 0 0 0 152 24H56a16 16 0 0 0-16 16v88a8 8 0 0 0 16 0V40h88v48a8 8 0 0 0 8 8h48v120h-8a8 8 0 0 0 0 16h8a16 16 0 0 0 16-16V88a8 8 0 0 0-2.34-5.66ZM160 51.31L188.69 80H160Z",
},
pdf: {
dim: PAGE,
solid: "M52 144H36a8 8 0 0 0-8 8v56a8 8 0 0 0 8 8h16a36 36 0 0 0 0-72m0 56h-8v-40h8a20 20 0 0 1 0 40m169.53-4.91a8 8 0 0 1 .25 11.31A30.06 30.06 0 0 1 200 216c-17.65 0-32-16.15-32-36s14.35-36 32-36a30.06 30.06 0 0 1 21.78 9.6a8 8 0 0 1-11.56 11.06A14.24 14.24 0 0 0 200 160c-8.82 0-16 9-16 20s7.18 20 16 20a14.18 14.18 0 0 0 10.22-4.66a8 8 0 0 1 11.31-.25M128 144c-17.64 0-32 16.15-32 36s14.36 36 32 36s32-16.15 32-36s-14.36-36-32-36m0 56c-8.82 0-16-9-16-20s7.18-20 16-20s16 9 16 20s-7.18 20-16 20m-80-80a8 8 0 0 0 8-8V40h88v48a8 8 0 0 0 8 8h48v16a8 8 0 0 0 16 0V88a8 8 0 0 0-2.34-5.66l-56-56A8 8 0 0 0 152 24H56a16 16 0 0 0-16 16v72a8 8 0 0 0 8 8m112-68.69L188.69 80H160Z",
},
zip: {
dim: PAGE,
solid: "M184 144h-16a8 8 0 0 0-8 8v56a8 8 0 0 0 16 0v-8h8a28 28 0 0 0 0-56m0 40h-8v-24h8a12 12 0 0 1 0 24m-48-32v56a8 8 0 0 1-16 0v-56a8 8 0 0 1 16 0m-40 56a8 8 0 0 1-8 8H56a8 8 0 0 1-7-12l25.16-44H56a8 8 0 0 1 0-16h32a8 8 0 0 1 7 12l-25.21 44H88a8 8 0 0 1 8 8M213.66 82.34l-56-56A8 8 0 0 0 152 24H56a16 16 0 0 0-16 16v72a8 8 0 0 0 16 0V40h88v48a8 8 0 0 0 8 8h48v16a8 8 0 0 0 16 0V88a8 8 0 0 0-2.34-5.66M160 80V51.31L188.69 80Z",
},
js: {
dim: PAGE,
solid: "m213.66 82.34l-56-56A8 8 0 0 0 152 24H56a16 16 0 0 0-16 16v72a8 8 0 0 0 16 0V40h88v48a8 8 0 0 0 8 8h48v120h-24a8 8 0 0 0 0 16h24a16 16 0 0 0 16-16V88a8 8 0 0 0-2.34-5.66M160 51.31L188.69 80H160Zm-12.19 145a20.82 20.82 0 0 1-9.19 15.23C133.43 215 127 216 121.13 216a61 61 0 0 1-15.13-2a8 8 0 1 1 4.3-15.41c4.38 1.2 15 2.7 19.55-.36c.88-.59 1.83-1.52 2.14-3.93c.35-2.67-.71-4.1-12.78-7.59c-9.35-2.7-25-7.23-23-23.11a20.56 20.56 0 0 1 9-14.95c11.84-8 30.71-3.31 32.83-2.76a8 8 0 0 1-4.07 15.48c-4.49-1.17-15.23-2.56-19.83.56a4.54 4.54 0 0 0-2 3.67c-.12.9-.14 1.09 1.11 1.9c2.31 1.49 6.45 2.68 10.45 3.84c9.79 2.83 26.35 7.66 24.11 24.97M80 152v38a26 26 0 0 1-52 0a8 8 0 0 1 16 0a10 10 0 0 0 20 0v-38a8 8 0 0 1 16 0",
},
css: {
dim: PAGE,
solid: "M48 180c0 11 7.18 20 16 20a14.2 14.2 0 0 0 10.22-4.66a8 8 0 1 1 11.55 11.06A30 30 0 0 1 64 216c-17.65 0-32-16.15-32-36s14.35-36 32-36a30 30 0 0 1 21.77 9.6a8 8 0 1 1-11.55 11.06A14.24 14.24 0 0 0 64 160c-8.82 0-16 9-16 20m79.6-8.69c-4-1.16-8.14-2.35-10.45-3.84c-1.26-.81-1.23-1-1.12-1.9a4.54 4.54 0 0 1 2-3.67c4.6-3.12 15.35-1.73 19.83-.56a8 8 0 0 0 4.07-15.48c-2.12-.55-21-5.22-32.83 2.76a20.55 20.55 0 0 0-9 14.95c-2 15.88 13.64 20.41 23 23.11c12.07 3.49 13.13 4.92 12.78 7.59c-.31 2.41-1.26 3.34-2.14 3.93c-4.6 3.06-15.17 1.56-19.55.36a8 8 0 0 0-4.3 15.41a61.2 61.2 0 0 0 15.18 2c5.83 0 12.3-1 17.49-4.46a20.82 20.82 0 0 0 9.19-15.23c2.25-17.28-14.27-22.11-24.15-24.97m64 0c-4-1.16-8.14-2.35-10.45-3.84c-1.25-.81-1.23-1-1.12-1.9a4.54 4.54 0 0 1 2-3.67c4.6-3.12 15.34-1.73 19.82-.56a8 8 0 0 0 4.07-15.48c-2.11-.55-21-5.22-32.83 2.76a20.58 20.58 0 0 0-8.95 14.95c-2 15.88 13.65 20.41 23 23.11c12.06 3.49 13.12 4.92 12.78 7.59c-.31 2.41-1.26 3.34-2.15 3.93c-4.6 3.06-15.16 1.56-19.54.36a8 8 0 0 0-4.3 15.44a61.3 61.3 0 0 0 15.19 2c5.82 0 12.3-1 17.49-4.46a20.81 20.81 0 0 0 9.18-15.23c2.21-17.31-14.31-22.14-24.2-25ZM40 112V40a16 16 0 0 1 16-16h96a8 8 0 0 1 5.66 2.34l56 56A8 8 0 0 1 216 88v24a8 8 0 1 1-16 0V96h-48a8 8 0 0 1-8-8V40H56v72a8 8 0 0 1-16 0m120-32h28.68L160 51.31Z",
},
md: {
dim: PAGE,
solid: "m213.66 82.34l-56-56A8 8 0 0 0 152 24H56a16 16 0 0 0-16 16v72a8 8 0 0 0 16 0V40h88v48a8 8 0 0 0 8 8h48v128a8 8 0 0 0 16 0V88a8 8 0 0 0-2.34-5.66M160 51.31L188.69 80H160ZM144 144h-16a8 8 0 0 0-8 8v56a8 8 0 0 0 8 8h16a36 36 0 0 0 0-72m0 56h-8v-40h8a20 20 0 0 1 0 40m-40-48v56a8 8 0 0 1-16 0v-30.62l-13.45 19.21a8 8 0 0 1-13.1 0L48 177.38V208a8 8 0 0 1-16 0v-56a8 8 0 0 1 14.55-4.59L68 178.05l21.45-30.64A8 8 0 0 1 104 152",
},
};
const CHECK: Glyph = {
dim: "M232 56v144a16 16 0 0 1-16 16H40a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16",
solid: "m205.66 85.66l-96 96a8 8 0 0 1-11.32 0l-40-40a8 8 0 0 1 11.32-11.32L104 164.69l90.34-90.35a8 8 0 0 1 11.32 11.32",
};
function Icon({ glyph, size }: { glyph: Glyph; size: number }) {
return (
<svg viewBox="0 0 256 256" width={size} height={size}>
<g fill="currentColor">
<path d={glyph.dim} opacity=".2" />
<path d={glyph.solid} />
</g>
</svg>
);
}
const DEFAULT_ITEMS: FileItem[] = [
{ name: "hero.jpg", kind: "img" },
{ name: "resume.pdf", kind: "pdf" },
{ name: "bundle.zip", kind: "zip" },
{ name: "main.js", kind: "js" },
{ name: "style.css", kind: "css" },
{ name: "notes.md", kind: "md" },
];
export default function MultiSelectTree({
label = "자료",
items = DEFAULT_ITEMS,
duration = 220,
easing = "cubic-bezier(.2, .8, .2, 1)",
color = "#2f6df6",
}: MultiSelectTreeProps) {
const [sel, setSel] = useState<boolean[]>(items.map(() => false));
const [anchor, setAnchor] = useState(0);
const [active, setActive] = useState(0);
const listRef = useRef<HTMLUListElement>(null);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const count = sel.filter(Boolean).length;
const last = items.length - 1;
function hit(i: number, e: ClickEvt) {
if (e.shiftKey) {
const a = Math.min(anchor, i);
const z = Math.max(anchor, i);
setSel(items.map((_, k) => k >= a && k <= z));
} else if (e.ctrlKey || e.metaKey) {
setSel((prev) => prev.map((v, k) => (k === i ? !v : v)));
setAnchor(i);
} else {
setSel(items.map((_, k) => k === i));
setAnchor(i);
}
setActive(i);
listRef.current?.focus({ preventScroll: true });
}
function onKey(e: KeyEvt) {
if (e.key === "ArrowDown") {
setActive((a) => Math.min(a + 1, last));
e.preventDefault();
} else if (e.key === "ArrowUp") {
setActive((a) => Math.max(a - 1, 0));
e.preventDefault();
} else if (e.key === " ") {
setSel((prev) => prev.map((v, k) => (k === active ? !v : v)));
setAnchor(active);
e.preventDefault();
}
}
return (
<div className={styles.ms} style={vars}>
<div className={styles.head}>
<span className={styles.title} id="ms-title">{`${label} ${items.length}개`}</span>
<span className={`${styles.badge} ${count > 0 ? styles.badgeOn : ""}`} aria-live="polite">
{`${count}개 선택`}
</span>
</div>
<ul
ref={listRef}
className={styles.list}
role="tree"
aria-labelledby="ms-title"
aria-multiselectable="true"
tabIndex={0}
aria-activedescendant={`ms-o${active}`}
onKeyDown={onKey}
>
{items.map((it, i) => (
<li
key={it.name}
id={`ms-o${i}`}
className={`${styles.item} ${sel[i] ? styles.isSel : ""} ${i === active ? styles.isActive : ""}`}
role="treeitem"
aria-level={1}
aria-selected={sel[i]}
>
<div className={styles.row} onClick={(e: ClickEvt) => hit(i, e)}>
<span className={styles.ico} aria-hidden="true">
<Icon glyph={GLYPH[it.kind]} size={13} />
</span>
<span className={styles.name}>{it.name}</span>
<span className={styles.ok} aria-hidden="true">
<Icon glyph={CHECK} size={12} />
</span>
</div>
</li>
))}
</ul>
</div>
);
}05 Shift·Ctrl 다중 선택 — 측정 누적면적 16.023% · 움직인 프레임 8/23GODRICH 원작 · zip 에 SCSS·React 포함 · 파일 트리 만들기 9종 — 폴더가 진짜로 열리는 사이드바
이 데모가 속한 글
파일 트리 만들기 9종 — 폴더가 진짜로 열리는 사이드바파일 트리 만들기는 폴더와 문서가 겹겹이 들어간 목록을, 누르면 실제로 펼쳐지고 접히는 사이드바로 직접 짜는 일입니다. 여기 아홉 개는 모양만 비슷한 게 아니라 끌어 넣기, 이름 고치기, 검색으로 좁히기, 용량 보기 이 글의 데모 전부 보기


