01토글 레버 스위치
금속 레버가 관절을 축으로 좌우로 기울어 on·off 를 가르고, 손을 떼면 스프링 이징으로 한 번 튕겨 자리를 잡습니다. 켜짐은 LED 점과 ON 글자 둘로 알리고 role="switch" 와 aria-checked 로 상태를 읽어 줍니다.
쓰임오디오 랙의 전원 패널, 기기 설정 화면의 모드 전환
role="switch"aria-checkedcubic-bezier(.2,1.4,.4,1)linear-gradient

<main class="stage">
<div class="tl is-demo" id="tl">
<section class="tl__panel">
<p class="tl__head"><span class="tl__led" aria-hidden="true"></span><span class="tl__name">전원</span></p>
<div class="tl__assembly">
<span class="tl__bracket" aria-hidden="true"></span>
<span class="tl__slot" aria-hidden="true"></span>
<span class="tl__dome" aria-hidden="true"></span>
<button class="tl__hit" id="tlLever" type="button" role="switch" aria-checked="false" aria-label="전원 레버">
<span class="tl__armWrap" aria-hidden="true"><span class="tl__grip"></span></span>
</button>
</div>
<p class="tl__marks" aria-hidden="true"><span class="tl__mark tl__mark--off">OFF</span><span class="tl__mark tl__mark--on">ON</span></p>
<p class="tl__state" role="status"><span class="tl__wordOff">꺼짐</span><span class="tl__wordOn">켜짐</span></p>
</section>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 토글 레버 스위치 — 금속 레버가 관절(돔)을 축으로 좌우로 기울어 on·off 를 가른다.
// 손을 떼는 순간의 스프링 튕김은 $easing(오버슈트 베지에)이 transition 한 번으로 만든다.
// 켜짐은 LED 점과 ON 글자 둘로 알린다(색만으로 상태를 말하지 않는다). role="switch" + aria-checked.
// 미리보기(.is-demo)는 레버가 좌우로 왕복하며 LED·글자가 steps(1,end) 컷으로 따라 바뀐다.
$duration: $dur-quick;
$easing: cubic-bezier(.2, 1.4, .4, 1); // = $ease-pop — 칩 값 그대로 노출
$color: $stage-paper;
@include stage($stage-ink);
.tl { display: grid; place-items: center; width: 100%; }
.tl__panel {
box-sizing: border-box; width: clamp(190px, 60vw, 230px);
display: flex; flex-direction: column; align-items: center; gap: $sp-2;
padding: $sp-3; border-radius: $r-card;
background: rgba($subject-cream, .06);
border: 1px solid rgba($subject-cream, .18);
box-shadow: $shadow-raised;
}
.tl__head { display: flex; align-items: center; gap: $sp-2; margin: 0; height: 16px; }
.tl__led {
width: 10px; height: 10px; border-radius: 50%; flex: 0 0 auto;
background: rgba($stage-yellow, .16);
box-shadow: inset 0 0 0 1px rgba($stage-yellow, .35);
}
.tl__name {
font-weight: 700; font-size: clamp(10px, 2.6vw, 12px); line-height: 1;
letter-spacing: .18em; color: rgba($subject-cream, .78);
}
// ── 레버 어셈블리 ────────────────────────────────────────────
.tl__assembly { position: relative; width: 130px; height: 92px; }
.tl__bracket {
position: absolute; left: 0; bottom: 0; width: 130px; height: 22px;
border-radius: $r-control;
background: linear-gradient(180deg, $color, rgba($subject-ink, .30)), $color;
box-shadow: 0 2px 0 rgba($stage-ink, .8), inset 0 1px 0 rgba($subject-cream, .8);
}
.tl__slot {
position: absolute; left: 50%; top: 72px; width: 72px; height: 6px;
margin-left: -36px; border-radius: $r-pill; background: rgba($stage-ink, .6);
box-shadow: inset 0 2px 3px rgba($stage-ink, .9), 0 1px 0 rgba($subject-cream, .16);
}
.tl__dome {
position: absolute; left: calc(50% - 13px); bottom: 3px; width: 26px; height: 26px;
border-radius: 50%;
background: linear-gradient(155deg, rgba($subject-cream, .95), rgba($subject-cream, .35) 55%, rgba($stage-ink, .65));
box-shadow: 0 1px 2px rgba($stage-ink, .9);
}
.tl__hit {
position: absolute; inset: 0; z-index: 5; margin: 0; padding: 0;
border: 0; background: transparent; cursor: pointer;
}
.tl__hit:focus-visible { outline: 2px solid $stage-yellow; outline-offset: 2px; border-radius: $r-control; }
// 레버 팔 — transform-origin 을 팔 밑(돔 중심)에 두고 좌우로 기울어진다
.tl__armWrap {
position: absolute; left: calc(50% - 5px); bottom: 15px; width: 10px; height: 80px;
border-radius: $r-pill; transform-origin: 50% 100%; transform: rotate(-24deg);
transition: transform $duration $easing;
background: linear-gradient(90deg,
rgba($stage-ink, .55), rgba($subject-cream, .82) 42%,
rgba($subject-cream, .95) 50%, rgba($subject-cream, .70) 62%, rgba($stage-ink, .50));
box-shadow: 0 4px 8px rgba($stage-ink, .55);
}
.tl__grip {
position: absolute; top: -13px; left: calc(50% - 15px); width: 30px; height: 30px;
border-radius: 50%;
background: linear-gradient(150deg, rgba($subject-cream, .96), rgba($subject-cream, .45) 48%, rgba($stage-ink, .55));
box-shadow: 0 6px 10px rgba($stage-ink, .6), inset 0 1px 1px rgba($subject-cream, .9);
}
// ── 상태 ─────────────────────────────────────────────────────
.tl__marks { display: flex; justify-content: space-between; width: 118px; margin: 0; }
.tl__mark {
font-weight: 800; font-size: clamp(9px, 2.4vw, 11px); line-height: 1; letter-spacing: .14em;
}
.tl__mark--off { color: rgba($subject-cream, .78); }
.tl__mark--on { color: rgba($subject-cream, .72); }
.tl__state { position: relative; margin: 0; width: 60px; height: 16px; }
.tl__wordOff, .tl__wordOn {
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
font-weight: 800; font-size: clamp(11px, 2.8vw, 13px); line-height: 1;
}
.tl__wordOff { color: rgba($subject-cream, .85); }
.tl__wordOn { color: $stage-yellow; display: none; }
// 켜짐 — 레버 기울기 · LED · ON 글자 · 상태 단어가 함께 바뀐다
.tl.is-on .tl__armWrap { transform: rotate(24deg); }
.tl.is-on .tl__led {
background: $stage-yellow;
box-shadow: 0 0 10px rgba($stage-yellow, .85), 0 0 0 3px rgba($stage-yellow, .25);
}
.tl.is-on .tl__mark--on { color: $stage-yellow; }
.tl.is-on .tl__mark--off { color: rgba($subject-cream, .72); }
.tl.is-on .tl__wordOn { display: flex; }
.tl.is-on .tl__wordOff { display: none; }
// ── 미리보기 루프(2초 한 바퀴) ────────────────────────────────
// 레버는 오버슈트를 넣었다가 제자리로 돌아온다. LED·글자는 steps(1,end) 컷.
.tl.is-demo .tl__armWrap {
animation-name: tlSwing; animation-duration: $dur-loop;
animation-timing-function: $easing; animation-iteration-count: infinite;
}
@keyframes tlSwing {
0% { transform: rotate(-24deg); }
8% { transform: rotate(-24deg); }
38% { transform: rotate(28deg); }
44% { transform: rotate(23deg); }
48% { transform: rotate(24deg); }
68% { transform: rotate(24deg); }
88% { transform: rotate(-28deg); }
94% { transform: rotate(-23deg); }
97% { transform: rotate(-24deg); }
100% { transform: rotate(-24deg); }
}
.tl.is-demo .tl__led {
animation-name: tlLed; animation-duration: $dur-loop;
animation-timing-function: steps(1, end); animation-iteration-count: infinite;
}
@keyframes tlLed {
0% { background: rgba($stage-yellow, .16); box-shadow: inset 0 0 0 1px rgba($stage-yellow, .35); }
46% { background: $stage-yellow; box-shadow: 0 0 10px rgba($stage-yellow, .85), 0 0 0 3px rgba($stage-yellow, .25); }
70% { background: rgba($stage-yellow, .16); box-shadow: inset 0 0 0 1px rgba($stage-yellow, .35); }
100% { background: rgba($stage-yellow, .16); box-shadow: inset 0 0 0 1px rgba($stage-yellow, .35); }
}
.tl.is-demo .tl__mark--on {
animation-name: tlMarkOn; animation-duration: $dur-loop;
animation-timing-function: steps(1, end); animation-iteration-count: infinite;
}
@keyframes tlMarkOn {
0% { color: rgba($subject-cream, .72); }
46% { color: $stage-yellow; }
70% { color: rgba($subject-cream, .72); }
100% { color: rgba($subject-cream, .72); }
}
.tl.is-demo .tl__mark--off {
animation-name: tlMarkOff; animation-duration: $dur-loop;
animation-timing-function: steps(1, end); animation-iteration-count: infinite;
}
@keyframes tlMarkOff {
0% { color: rgba($subject-cream, .78); }
46% { color: rgba($subject-cream, .72); }
70% { color: rgba($subject-cream, .78); }
100% { color: rgba($subject-cream, .78); }
}
// 상태 단어는 루프에서 둘 다 띄운 뒤 opacity 컷으로 갈아 낀다(정적 규칙의 display 는 건드리지 않는다)
.tl.is-demo .tl__wordOn { display: flex; opacity: 0; }
.tl.is-demo .tl__wordOff { opacity: 1; }
.tl.is-demo .tl__wordOn {
animation-name: tlWordOn; animation-duration: $dur-loop;
animation-timing-function: steps(1, end); animation-iteration-count: infinite;
}
@keyframes tlWordOn {
0% { opacity: 0; }
46% { opacity: 1; }
70% { opacity: 0; }
100% { opacity: 0; }
}
.tl.is-demo .tl__wordOff {
animation-name: tlWordOff; animation-duration: $dur-loop;
animation-timing-function: steps(1, end); animation-iteration-count: infinite;
}
@keyframes tlWordOff {
0% { opacity: 1; }
46% { opacity: 0; }
70% { opacity: 1; }
100% { opacity: 1; }
}
@media (max-width: 340px) {
.tl__panel { gap: $sp-1; padding: $sp-2; }
.tl__assembly { width: 110px; height: 66px; }
.tl__bracket { width: 110px; height: 18px; }
.tl__slot { top: 52px; left: calc(50% - 30px); width: 60px; }
.tl__dome { width: 20px; height: 20px; left: calc(50% - 10px); bottom: 2px; }
.tl__armWrap { height: 56px; bottom: 11px; }
.tl__grip { width: 24px; height: 24px; left: calc(50% - 12px); top: -10px; }
.tl__marks { width: 100px; }
.tl__state { height: 14px; }
}
@media (prefers-reduced-motion: reduce) {
.tl.is-demo .tl__armWrap { animation: none !important; transform: rotate(-24deg) !important; }
.tl.is-demo .tl__led { animation: none !important; }
.tl.is-demo .tl__mark--on { animation: none !important; }
.tl.is-demo .tl__mark--off { animation: none !important; }
.tl.is-demo .tl__wordOn { animation: none !important; opacity: 0 !important; }
.tl.is-demo .tl__wordOff { animation: none !important; opacity: 1 !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);
}
.tl {
display: grid;
place-items: center;
width: 100%;
}
.tl__panel {
box-sizing: border-box;
width: clamp(190px, 60vw, 230px);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 12px;
border-radius: 16px;
background: rgba(255, 247, 230, 0.06);
border: 1px solid rgba(255, 247, 230, 0.18);
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.tl__head {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
height: 16px;
}
.tl__led {
width: 10px;
height: 10px;
border-radius: 50%;
flex: 0 0 auto;
background: rgba(255, 210, 63, 0.16);
box-shadow: inset 0 0 0 1px rgba(255, 210, 63, 0.35);
}
.tl__name {
font-weight: 700;
font-size: clamp(10px, 2.6vw, 12px);
line-height: 1;
letter-spacing: 0.18em;
color: rgba(255, 247, 230, 0.78);
}
.tl__assembly {
position: relative;
width: 130px;
height: 92px;
}
.tl__bracket {
position: absolute;
left: 0;
bottom: 0;
width: 130px;
height: 22px;
border-radius: 12px;
background: linear-gradient(180deg, #ffffff, rgba(23, 20, 26, 0.3)), #ffffff;
box-shadow: 0 2px 0 rgba(23, 20, 26, 0.8), inset 0 1px 0 rgba(255, 247, 230, 0.8);
}
.tl__slot {
position: absolute;
left: 50%;
top: 72px;
width: 72px;
height: 6px;
margin-left: -36px;
border-radius: 999px;
background: rgba(23, 20, 26, 0.6);
box-shadow: inset 0 2px 3px rgba(23, 20, 26, 0.9), 0 1px 0 rgba(255, 247, 230, 0.16);
}
.tl__dome {
position: absolute;
left: calc(50% - 13px);
bottom: 3px;
width: 26px;
height: 26px;
border-radius: 50%;
background: linear-gradient(155deg, rgba(255, 247, 230, 0.95), rgba(255, 247, 230, 0.35) 55%, rgba(23, 20, 26, 0.65));
box-shadow: 0 1px 2px rgba(23, 20, 26, 0.9);
}
.tl__hit {
position: absolute;
inset: 0;
z-index: 5;
margin: 0;
padding: 0;
border: 0;
background: transparent;
cursor: pointer;
}
.tl__hit:focus-visible {
outline: 2px solid #ffd23f;
outline-offset: 2px;
border-radius: 12px;
}
.tl__armWrap {
position: absolute;
left: calc(50% - 5px);
bottom: 15px;
width: 10px;
height: 80px;
border-radius: 999px;
transform-origin: 50% 100%;
transform: rotate(-24deg);
transition: transform 220ms cubic-bezier(0.2, 1.4, 0.4, 1);
background: linear-gradient(90deg, rgba(23, 20, 26, 0.55), rgba(255, 247, 230, 0.82) 42%, rgba(255, 247, 230, 0.95) 50%, rgba(255, 247, 230, 0.7) 62%, rgba(23, 20, 26, 0.5));
box-shadow: 0 4px 8px rgba(23, 20, 26, 0.55);
}
.tl__grip {
position: absolute;
top: -13px;
left: calc(50% - 15px);
width: 30px;
height: 30px;
border-radius: 50%;
background: linear-gradient(150deg, rgba(255, 247, 230, 0.96), rgba(255, 247, 230, 0.45) 48%, rgba(23, 20, 26, 0.55));
box-shadow: 0 6px 10px rgba(23, 20, 26, 0.6), inset 0 1px 1px rgba(255, 247, 230, 0.9);
}
.tl__marks {
display: flex;
justify-content: space-between;
width: 118px;
margin: 0;
}
.tl__mark {
font-weight: 800;
font-size: clamp(9px, 2.4vw, 11px);
line-height: 1;
letter-spacing: 0.14em;
}
.tl__mark--off {
color: rgba(255, 247, 230, 0.78);
}
.tl__mark--on {
color: rgba(255, 247, 230, 0.72);
}
.tl__state {
position: relative;
margin: 0;
width: 60px;
height: 16px;
}
.tl__wordOff, .tl__wordOn {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-weight: 800;
font-size: clamp(11px, 2.8vw, 13px);
line-height: 1;
}
.tl__wordOff {
color: rgba(255, 247, 230, 0.85);
}
.tl__wordOn {
color: #ffd23f;
display: none;
}
.tl.is-on .tl__armWrap {
transform: rotate(24deg);
}
.tl.is-on .tl__led {
background: #ffd23f;
box-shadow: 0 0 10px rgba(255, 210, 63, 0.85), 0 0 0 3px rgba(255, 210, 63, 0.25);
}
.tl.is-on .tl__mark--on {
color: #ffd23f;
}
.tl.is-on .tl__mark--off {
color: rgba(255, 247, 230, 0.72);
}
.tl.is-on .tl__wordOn {
display: flex;
}
.tl.is-on .tl__wordOff {
display: none;
}
.tl.is-demo .tl__armWrap {
animation-name: tlSwing;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 1.4, 0.4, 1);
animation-iteration-count: infinite;
}
@keyframes tlSwing {
0% {
transform: rotate(-24deg);
}
8% {
transform: rotate(-24deg);
}
38% {
transform: rotate(28deg);
}
44% {
transform: rotate(23deg);
}
48% {
transform: rotate(24deg);
}
68% {
transform: rotate(24deg);
}
88% {
transform: rotate(-28deg);
}
94% {
transform: rotate(-23deg);
}
97% {
transform: rotate(-24deg);
}
100% {
transform: rotate(-24deg);
}
}
.tl.is-demo .tl__led {
animation-name: tlLed;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes tlLed {
0% {
background: rgba(255, 210, 63, 0.16);
box-shadow: inset 0 0 0 1px rgba(255, 210, 63, 0.35);
}
46% {
background: #ffd23f;
box-shadow: 0 0 10px rgba(255, 210, 63, 0.85), 0 0 0 3px rgba(255, 210, 63, 0.25);
}
70% {
background: rgba(255, 210, 63, 0.16);
box-shadow: inset 0 0 0 1px rgba(255, 210, 63, 0.35);
}
100% {
background: rgba(255, 210, 63, 0.16);
box-shadow: inset 0 0 0 1px rgba(255, 210, 63, 0.35);
}
}
.tl.is-demo .tl__mark--on {
animation-name: tlMarkOn;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes tlMarkOn {
0% {
color: rgba(255, 247, 230, 0.72);
}
46% {
color: #ffd23f;
}
70% {
color: rgba(255, 247, 230, 0.72);
}
100% {
color: rgba(255, 247, 230, 0.72);
}
}
.tl.is-demo .tl__mark--off {
animation-name: tlMarkOff;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes tlMarkOff {
0% {
color: rgba(255, 247, 230, 0.78);
}
46% {
color: rgba(255, 247, 230, 0.72);
}
70% {
color: rgba(255, 247, 230, 0.78);
}
100% {
color: rgba(255, 247, 230, 0.78);
}
}
.tl.is-demo .tl__wordOn {
display: flex;
opacity: 0;
}
.tl.is-demo .tl__wordOff {
opacity: 1;
}
.tl.is-demo .tl__wordOn {
animation-name: tlWordOn;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes tlWordOn {
0% {
opacity: 0;
}
46% {
opacity: 1;
}
70% {
opacity: 0;
}
100% {
opacity: 0;
}
}
.tl.is-demo .tl__wordOff {
animation-name: tlWordOff;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes tlWordOff {
0% {
opacity: 1;
}
46% {
opacity: 0;
}
70% {
opacity: 1;
}
100% {
opacity: 1;
}
}
@media (max-width: 340px) {
.tl__panel {
gap: 4px;
padding: 8px;
}
.tl__assembly {
width: 110px;
height: 66px;
}
.tl__bracket {
width: 110px;
height: 18px;
}
.tl__slot {
top: 52px;
left: calc(50% - 30px);
width: 60px;
}
.tl__dome {
width: 20px;
height: 20px;
left: calc(50% - 10px);
bottom: 2px;
}
.tl__armWrap {
height: 56px;
bottom: 11px;
}
.tl__grip {
width: 24px;
height: 24px;
left: calc(50% - 12px);
top: -10px;
}
.tl__marks {
width: 100px;
}
.tl__state {
height: 14px;
}
}
@media (prefers-reduced-motion: reduce) {
.tl.is-demo .tl__armWrap {
animation: none !important;
transform: rotate(-24deg) !important;
}
.tl.is-demo .tl__led {
animation: none !important;
}
.tl.is-demo .tl__mark--on {
animation: none !important;
}
.tl.is-demo .tl__mark--off {
animation: none !important;
}
.tl.is-demo .tl__wordOn {
animation: none !important;
opacity: 0 !important;
}
.tl.is-demo .tl__wordOff {
animation: none !important;
opacity: 1 !important;
}
}var root = document.getElementById('tl');
var lever = document.getElementById('tlLever');
function setOn(on) {
lever.setAttribute('aria-checked', on ? 'true' : 'false');
root.classList.toggle('is-on', on);
}
function toggle() {
root.classList.remove('is-demo');
setOn(lever.getAttribute('aria-checked') !== 'true');
}
lever.addEventListener('pointerdown', function () { root.classList.remove('is-demo'); });
lever.addEventListener('click', toggle);// ToggleLever — vanilla 판(run/013/demo/01-skeuo-toggle-lever/index.html) 의 React 포트.
// 금속 레버가 관절을 축으로 좌우로 기울어 on·off 를 가르고, transition 이 오버슈트로 한 번 튕긴다.
// 값·키프레임은 vanilla 와 같다(다시 타이핑하지 않는다). 클릭하면 미리보기 루프를 뗀다.
import { useState } from "react";
import styles from "./ToggleLever.module.scss";
export type ToggleLeverProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = { [k: string]: any };
const NAME_TEXT = "전원";
const STATE_OFF = "꺼짐";
const STATE_ON = "켜짐";
const LEVER_LABEL = "전원 레버";
export default function ToggleLever({
duration = 220,
easing = "cubic-bezier(.2, 1.4, .4, 1)", // = $ease-pop
color = "#ffffff",
}: ToggleLeverProps) {
const [on, setOn] = useState(false);
const [demo, setDemo] = useState(true);
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
};
const cls = [styles.root, demo ? styles.isDemo : "", on ? styles.isOn : ""].filter(Boolean).join(" ");
function toggle() {
setDemo(false);
setOn((prev) => !prev);
}
function wake() {
setDemo(false);
}
return (
<main className={styles.stage}>
<div className={cls} style={vars}>
<section className={styles.panel}>
<p className={styles.head}><span className={styles.led} aria-hidden="true"></span><span className={styles.name}>{NAME_TEXT}</span></p>
<div className={styles.assembly}>
<span className={styles.bracket} aria-hidden="true"></span>
<span className={styles.slot} aria-hidden="true"></span>
<span className={styles.dome} aria-hidden="true"></span>
<button
className={styles.hit}
type="button"
role="switch"
aria-checked={on}
aria-label={LEVER_LABEL}
onPointerDown={wake}
onClick={toggle}
>
<span className={styles.armWrap} aria-hidden="true"><span className={styles.grip}></span></span>
</button>
</div>
<p className={styles.marks} aria-hidden="true">
<span className={`${styles.mark} ${styles.markOff}`}>OFF</span>
<span className={`${styles.mark} ${styles.markOn}`}>ON</span>
</p>
<p className={styles.state} role="status">
<span className={styles.wordOff}>{STATE_OFF}</span>
<span className={styles.wordOn}>{STATE_ON}</span>
</p>
</section>
</div>
</main>
);
}01 토글 레버 스위치 — 측정 누적면적 4.119% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · CSS 스위치 버튼 9종 — 실물처럼 눌리는 레버·다이얼·이젝트
이 데모가 속한 글
CSS 스위치 버튼 9종 — 실물처럼 눌리는 레버·다이얼·이젝트css 스위치 버튼은 화면에서 켜고 끄는 스위치를 실물 레버·로커·다이얼처럼 보이게 만든 버튼입니다. 여기 아홉 개는 라이브러리 없이 SCSS 와 짧은 JS 로만 짰고, 누르면 그 자리에서 기울고 돌고 튀어나오며 화 이 글의 데모 전부 보기


