06칩을 눌러 문장이 되는 반복 규칙
요일 칩을 켜고 끄는 동안 아래 문장이 매주 월·수·금처럼 다시 쓰이고, 매월로 바꾸면 같은 칩이 몇째 주 무슨 요일이라는 뜻으로 읽힙니다.
쓰임반복 일정·정기 알림처럼 규칙을 말로 확인시켜야 하는 설정 창
aria-checkedaria-livesteps(1, end)

<main class="stage">
<div class="rc is-demo">
<div class="rc__seg" id="rc-seg" data-m="w" role="radiogroup" aria-label="반복 주기">
<span class="rc__ind" aria-hidden="true"></span>
<button class="rc__opt" type="button" role="radio" aria-checked="true" data-m="w">매주</button>
<button class="rc__opt" type="button" role="radio" aria-checked="false" data-m="m">매월</button>
</div>
<div class="rc__days" role="group" aria-label="반복 요일">
<button class="rc__chip" type="button" role="checkbox" aria-checked="false" aria-label="일요일" data-w="0">일</button>
<button class="rc__chip" type="button" role="checkbox" aria-checked="true" aria-label="월요일" data-w="1">월</button>
<button class="rc__chip" type="button" role="checkbox" aria-checked="false" aria-label="화요일" data-w="2">화</button>
<button class="rc__chip" type="button" role="checkbox" aria-checked="true" aria-label="수요일" data-w="3">수</button>
<button class="rc__chip" type="button" role="checkbox" aria-checked="false" aria-label="목요일" data-w="4">목</button>
<button class="rc__chip" type="button" role="checkbox" aria-checked="true" aria-label="금요일" data-w="5">금</button>
<button class="rc__chip" type="button" role="checkbox" aria-checked="false" aria-label="토요일" data-w="6">토</button>
</div>
<p class="rc__sum" aria-live="polite">
<span class="rc__cut" data-k="1">매주 월·수·금 오전 9시</span>
<span class="rc__cut" data-k="2" aria-hidden="true">매주 월·수·목·금 오전 9시</span>
<span class="rc__cut" data-k="3" aria-hidden="true">매주 월·화·수·목·금 오전 9시</span>
<span class="rc__cut" data-k="4" aria-hidden="true">매월 둘째 주 월요일 오전 9시</span>
</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop;
$easing: $ease-spring;
$color: $subject-ink;
@include stage($stage-yellow);
.rc {
--seg-w: 72px; // 세그먼트 한 칸 폭 = 인디케이터가 옮겨가는 거리 (BD19: 칸은 flex: 0 0 auto)
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-3;
width: min(300px, 90vw);
color: $color;
}
// ── 매주 / 매월 세그먼트 두 칸 ──
.rc__seg {
position: relative;
display: flex;
gap: 0;
padding: $sp-1;
border-radius: $r-pill;
background-color: rgba($subject-ink, .16);
}
.rc__ind {
position: absolute;
left: $sp-1;
top: $sp-1;
width: var(--seg-w);
height: 28px;
border-radius: $r-pill;
background-color: $color;
transform: translateX(0);
transition: transform $dur-base $easing;
}
.rc__seg[data-m="m"] .rc__ind { transform: translateX(var(--seg-w)); }
.rc__opt {
position: relative;
flex: 0 0 auto;
width: var(--seg-w);
height: 28px;
margin: 0;
padding: 0;
border: 0;
border-radius: $r-pill;
background-color: transparent;
color: $color;
font-size: clamp(12px, 3.6vw, 13px);
line-height: 28px;
font-weight: 700;
cursor: pointer;
}
.rc__opt[aria-checked="true"] { color: $stage-yellow; }
.rc__opt:focus-visible { outline: 3px solid $color; outline-offset: 2px; }
// ── 요일 칩 일곱 개 ──
.rc__days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: $sp-1;
width: 100%;
}
.rc__chip {
margin: 0;
padding: 0;
height: 30px;
border: 2px solid $color;
border-radius: $r-pill;
background-color: transparent;
color: $color;
font-size: clamp(11px, 3.4vw, 13px);
line-height: 26px;
font-weight: 700;
cursor: pointer;
}
.rc__chip[aria-checked="true"] { background-color: $color; color: $stage-yellow; }
.rc__chip:focus-visible { outline: 3px solid $color; outline-offset: 2px; }
// ── 라이브 요약 문장 ──
.rc__sum {
position: relative;
margin: 0;
width: 100%;
height: 38px;
}
.rc__cut {
position: absolute;
left: 0;
right: 0;
top: 0;
text-align: center;
font-size: clamp(12px, 3.8vw, 14px);
line-height: 19px;
font-weight: 700;
opacity: 0;
}
.rc__cut[data-k="1"] { opacity: 1; }
// ── 자동 루프: 칩 두 개가 차례로 켜지고 문장이 컷으로 갈리며 세그먼트가 한 번 옮겨간다 ──
.rc.is-demo .rc__ind {
animation-name: rc-ind;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
// 인디케이터가 매월로 옮겨 앉는 66% 가 이 데모의 주인공 장면이다 — 요약 문장도 그 프레임에 함께 갈린다
@keyframes rc-ind {
0%, 62% { transform: translateX(0); }
70%, 76% { transform: translateX(var(--seg-w)); }
86%, 100% { transform: translateX(0); }
}
.rc.is-demo .rc__opt {
animation-duration: $duration;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.rc.is-demo .rc__opt[data-m="w"] { animation-name: rc-ow; }
.rc.is-demo .rc__opt[data-m="m"] { animation-name: rc-om; }
// 라벨 색은 인디케이터가 그 칸을 덮는 순간에 맞춰 갈린다 — 어긋나면 먹 글자가 먹 알약 위에 얹혀 사라진다
@keyframes rc-ow { 0% { color: $stage-yellow; } 64% { color: $color; } 78% { color: $stage-yellow; } 100% { color: $stage-yellow; } }
@keyframes rc-om { 0% { color: $color; } 64% { color: $stage-yellow; } 78% { color: $color; } 100% { color: $color; } }
// 켜짐은 한 프레임에 컷 — 그 옆에 눌림 반동(scale)을 붙여 프레임을 채운다
.rc.is-demo .rc__chip[data-w="4"],
.rc.is-demo .rc__chip[data-w="2"] {
animation-duration: $duration, $duration;
animation-timing-function: steps(1, end), $ease-pop;
animation-iteration-count: infinite, infinite;
animation-fill-mode: backwards, backwards;
}
.rc.is-demo .rc__chip[data-w="4"] { animation-name: rc-on4, rc-pop4; }
.rc.is-demo .rc__chip[data-w="2"] { animation-name: rc-on2, rc-pop2; }
// 끄기는 한 프레임에 몰지 않고 74% · 82% 로 나눈다 (되돌림이 최대 변화가 되면 poster 가 빈 장면이 된다)
@keyframes rc-on4 {
0% { background-color: transparent; color: $color; }
20% { background-color: $color; color: $stage-yellow; }
74% { background-color: transparent; color: $color; }
100% { background-color: transparent; color: $color; }
}
@keyframes rc-on2 {
0% { background-color: transparent; color: $color; }
40% { background-color: $color; color: $stage-yellow; }
82% { background-color: transparent; color: $color; }
100% { background-color: transparent; color: $color; }
}
@keyframes rc-pop4 {
0%, 19% { transform: scale(1); }
22% { transform: scale(.86); }
29% { transform: scale(1.12); }
36%, 100% { transform: scale(1); }
}
@keyframes rc-pop2 {
0%, 39% { transform: scale(1); }
42% { transform: scale(.86); }
49% { transform: scale(1.12); }
56%, 100% { transform: scale(1); }
}
.rc.is-demo .rc__cut {
animation-duration: $duration;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.rc.is-demo .rc__cut[data-k="1"] { animation-name: rc-k1; }
.rc.is-demo .rc__cut[data-k="2"] { animation-name: rc-k2; }
.rc.is-demo .rc__cut[data-k="3"] { animation-name: rc-k3; }
.rc.is-demo .rc__cut[data-k="4"] { animation-name: rc-k4; }
@keyframes rc-k1 { 0% { opacity: 1; } 20% { opacity: 0; } 86% { opacity: 1; } 100% { opacity: 1; } }
@keyframes rc-k2 { 0% { opacity: 0; } 20% { opacity: 1; } 40% { opacity: 0; } 100% { opacity: 0; } }
@keyframes rc-k3 { 0% { opacity: 0; } 40% { opacity: 1; } 66% { opacity: 0; } 100% { opacity: 0; } }
@keyframes rc-k4 { 0% { opacity: 0; } 66% { opacity: 1; } 86% { opacity: 0; } 100% { opacity: 0; } }
.rc.is-demo .rc__sum {
animation-name: rc-drop;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
@keyframes rc-drop {
0%, 19% { transform: translateY(0); }
22% { transform: translateY(6px); }
29% { transform: translateY(0); }
39% { transform: translateY(0); }
42% { transform: translateY(6px); }
49% { transform: translateY(0); }
65% { transform: translateY(0); }
68% { transform: translateY(6px); }
75%, 100% { transform: translateY(0); }
}
@media (max-width: 340px) {
.rc { --seg-w: 60px; gap: $sp-2; }
.rc__chip { height: 28px; line-height: 24px; }
.rc__sum { height: 36px; }
.rc__cut { line-height: 18px; }
}
@media (prefers-reduced-motion: reduce) {
.rc.is-demo .rc__ind,
.rc.is-demo .rc__opt,
.rc.is-demo .rc__chip,
.rc.is-demo .rc__cut,
.rc.is-demo .rc__sum { animation: none !important; }
.rc__ind { transition: none !important; }
.rc.is-demo .rc__cut[data-k="1"] { opacity: 1; }
}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);
}
.rc {
--seg-w: 72px;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
width: min(300px, 90vw);
color: #17141a;
}
.rc__seg {
position: relative;
display: flex;
gap: 0;
padding: 4px;
border-radius: 999px;
background-color: rgba(23, 20, 26, 0.16);
}
.rc__ind {
position: absolute;
left: 4px;
top: 4px;
width: var(--seg-w);
height: 28px;
border-radius: 999px;
background-color: #17141a;
transform: translateX(0);
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.rc__seg[data-m=m] .rc__ind {
transform: translateX(var(--seg-w));
}
.rc__opt {
position: relative;
flex: 0 0 auto;
width: var(--seg-w);
height: 28px;
margin: 0;
padding: 0;
border: 0;
border-radius: 999px;
background-color: transparent;
color: #17141a;
font-size: clamp(12px, 3.6vw, 13px);
line-height: 28px;
font-weight: 700;
cursor: pointer;
}
.rc__opt[aria-checked=true] {
color: #ffd23f;
}
.rc__opt:focus-visible {
outline: 3px solid #17141a;
outline-offset: 2px;
}
.rc__days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
width: 100%;
}
.rc__chip {
margin: 0;
padding: 0;
height: 30px;
border: 2px solid #17141a;
border-radius: 999px;
background-color: transparent;
color: #17141a;
font-size: clamp(11px, 3.4vw, 13px);
line-height: 26px;
font-weight: 700;
cursor: pointer;
}
.rc__chip[aria-checked=true] {
background-color: #17141a;
color: #ffd23f;
}
.rc__chip:focus-visible {
outline: 3px solid #17141a;
outline-offset: 2px;
}
.rc__sum {
position: relative;
margin: 0;
width: 100%;
height: 38px;
}
.rc__cut {
position: absolute;
left: 0;
right: 0;
top: 0;
text-align: center;
font-size: clamp(12px, 3.8vw, 14px);
line-height: 19px;
font-weight: 700;
opacity: 0;
}
.rc__cut[data-k="1"] {
opacity: 1;
}
.rc.is-demo .rc__ind {
animation-name: rc-ind;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
@keyframes rc-ind {
0%, 62% {
transform: translateX(0);
}
70%, 76% {
transform: translateX(var(--seg-w));
}
86%, 100% {
transform: translateX(0);
}
}
.rc.is-demo .rc__opt {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.rc.is-demo .rc__opt[data-m=w] {
animation-name: rc-ow;
}
.rc.is-demo .rc__opt[data-m=m] {
animation-name: rc-om;
}
@keyframes rc-ow {
0% {
color: #ffd23f;
}
64% {
color: #17141a;
}
78% {
color: #ffd23f;
}
100% {
color: #ffd23f;
}
}
@keyframes rc-om {
0% {
color: #17141a;
}
64% {
color: #ffd23f;
}
78% {
color: #17141a;
}
100% {
color: #17141a;
}
}
.rc.is-demo .rc__chip[data-w="4"],
.rc.is-demo .rc__chip[data-w="2"] {
animation-duration: 2s, 2s;
animation-timing-function: steps(1, end), cubic-bezier(0.2, 1.4, 0.4, 1);
animation-iteration-count: infinite, infinite;
animation-fill-mode: backwards, backwards;
}
.rc.is-demo .rc__chip[data-w="4"] {
animation-name: rc-on4, rc-pop4;
}
.rc.is-demo .rc__chip[data-w="2"] {
animation-name: rc-on2, rc-pop2;
}
@keyframes rc-on4 {
0% {
background-color: transparent;
color: #17141a;
}
20% {
background-color: #17141a;
color: #ffd23f;
}
74% {
background-color: transparent;
color: #17141a;
}
100% {
background-color: transparent;
color: #17141a;
}
}
@keyframes rc-on2 {
0% {
background-color: transparent;
color: #17141a;
}
40% {
background-color: #17141a;
color: #ffd23f;
}
82% {
background-color: transparent;
color: #17141a;
}
100% {
background-color: transparent;
color: #17141a;
}
}
@keyframes rc-pop4 {
0%, 19% {
transform: scale(1);
}
22% {
transform: scale(0.86);
}
29% {
transform: scale(1.12);
}
36%, 100% {
transform: scale(1);
}
}
@keyframes rc-pop2 {
0%, 39% {
transform: scale(1);
}
42% {
transform: scale(0.86);
}
49% {
transform: scale(1.12);
}
56%, 100% {
transform: scale(1);
}
}
.rc.is-demo .rc__cut {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.rc.is-demo .rc__cut[data-k="1"] {
animation-name: rc-k1;
}
.rc.is-demo .rc__cut[data-k="2"] {
animation-name: rc-k2;
}
.rc.is-demo .rc__cut[data-k="3"] {
animation-name: rc-k3;
}
.rc.is-demo .rc__cut[data-k="4"] {
animation-name: rc-k4;
}
@keyframes rc-k1 {
0% {
opacity: 1;
}
20% {
opacity: 0;
}
86% {
opacity: 1;
}
100% {
opacity: 1;
}
}
@keyframes rc-k2 {
0% {
opacity: 0;
}
20% {
opacity: 1;
}
40% {
opacity: 0;
}
100% {
opacity: 0;
}
}
@keyframes rc-k3 {
0% {
opacity: 0;
}
40% {
opacity: 1;
}
66% {
opacity: 0;
}
100% {
opacity: 0;
}
}
@keyframes rc-k4 {
0% {
opacity: 0;
}
66% {
opacity: 1;
}
86% {
opacity: 0;
}
100% {
opacity: 0;
}
}
.rc.is-demo .rc__sum {
animation-name: rc-drop;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
@keyframes rc-drop {
0%, 19% {
transform: translateY(0);
}
22% {
transform: translateY(6px);
}
29% {
transform: translateY(0);
}
39% {
transform: translateY(0);
}
42% {
transform: translateY(6px);
}
49% {
transform: translateY(0);
}
65% {
transform: translateY(0);
}
68% {
transform: translateY(6px);
}
75%, 100% {
transform: translateY(0);
}
}
@media (max-width: 340px) {
.rc {
--seg-w: 60px;
gap: 8px;
}
.rc__chip {
height: 28px;
line-height: 24px;
}
.rc__sum {
height: 36px;
}
.rc__cut {
line-height: 18px;
}
}
@media (prefers-reduced-motion: reduce) {
.rc.is-demo .rc__ind,
.rc.is-demo .rc__opt,
.rc.is-demo .rc__chip,
.rc.is-demo .rc__cut,
.rc.is-demo .rc__sum {
animation: none !important;
}
.rc__ind {
transition: none !important;
}
.rc.is-demo .rc__cut[data-k="1"] {
opacity: 1;
}
}(function () {
var root = document.querySelector('.rc'), seg = document.getElementById('rc-seg');
var sum = root.querySelector('.rc__cut[data-k="1"]');
var opts = [].slice.call(root.querySelectorAll('.rc__opt'));
var chips = [].slice.call(root.querySelectorAll('.rc__chip'));
var SH = ['일', '월', '화', '수', '목', '금', '토'];
var OR = ['첫째', '둘째', '셋째', '넷째', '다섯째'];
function on(c) { return c.getAttribute('aria-checked') === 'true'; }
// 매월 모드의 몇째 주는 실제로 센다 — 기준일에서 그 요일이 처음 오는 날짜로 주차를 구한다
function ord(w) {
var d = new Date(2026, 8, 12);
d.setDate(d.getDate() + ((w - d.getDay() + 7) % 7));
return OR[Math.ceil(d.getDate() / 7) - 1];
}
function render() {
var m = seg.dataset.m, p = chips.filter(on).map(function (c) { return +c.dataset.w; });
if (!p.length) { sum.textContent = m === 'm' ? '매월 반복 없음' : '매주 반복 없음'; return; }
sum.textContent = m === 'm'
? '매월 ' + ord(p[0]) + ' 주 ' + SH[p[0]] + '요일 오전 9시'
: p.length === 1
? '매주 ' + SH[p[0]] + '요일 오전 9시'
: '매주 ' + p.map(function (w) { return SH[w]; }).join('·') + ' 오전 9시';
}
function wake() { root.classList.remove('is-demo'); }
chips.forEach(function (c) {
c.addEventListener('click', function () { wake(); c.setAttribute('aria-checked', String(!on(c))); render(); });
});
opts.forEach(function (o) {
o.addEventListener('click', function () {
wake();
opts.forEach(function (x) { x.setAttribute('aria-checked', String(x === o)); });
seg.dataset.m = o.dataset.m; render();
});
});
seg.addEventListener('keydown', function (e) {
var d = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
if (!d) return;
e.preventDefault(); wake();
var i = (opts.indexOf(document.activeElement) + d + 2) % 2;
opts[i].focus({ preventScroll: true }); opts[i].click();
});
root.querySelector('.rc__days').addEventListener('keydown', function (e) {
var d = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
if (!d) return;
e.preventDefault();
var i = (chips.indexOf(document.activeElement) + d + 7) % 7;
chips[i].focus({ preventScroll: true });
});
render();
})();import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./RecurrenceWeekdayChips.module.scss";
export type RecurrenceWeekdayChipsProps = {
labels?: {
cycle: string; weekly: string; monthly: string;
days: string; abbr: string[]; full: string[]; ordinals: string[];
at: string; none: string;
};
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type KeyEvt = { key: string; preventDefault(): void };
const DEMO_CUTS = [
[1, 3, 5],
[1, 3, 4, 5],
[1, 2, 3, 4, 5],
];
export default function RecurrenceWeekdayChips({
labels = {
cycle: "Repeat cycle",
weekly: "Weekly",
monthly: "Monthly",
days: "Repeat days",
abbr: ["S", "M", "T", "W", "T", "F", "S"],
full: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
ordinals: ["1st", "2nd", "3rd", "4th", "5th"],
at: "at 9:00 AM",
none: "no days selected",
},
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#17141a",
}: RecurrenceWeekdayChipsProps) {
const [demo, setDemo] = useState(true);
const [monthly, setMonthly] = useState(false);
const [picked, setPicked] = useState<number[]>([1, 3, 5]);
const vars: Vars = { "--duration": duration + "ms", "--easing": easing, "--color": color };
// 매월 모드의 몇째 주는 실제로 센다 — 기준일에서 그 요일이 처음 오는 날짜로 주차를 구한다
function ordinal(w: number): string {
const d = new Date(2026, 8, 12);
d.setDate(d.getDate() + ((w - d.getDay() + 7) % 7));
return labels.ordinals[Math.ceil(d.getDate() / 7) - 1];
}
function sentence(mode: boolean, p: number[]): string {
const head = mode ? labels.monthly : labels.weekly;
if (!p.length) return head + " — " + labels.none;
if (mode) return head + " on the " + ordinal(p[0]) + " " + labels.full[p[0]] + " " + labels.at;
if (p.length === 1) return head + " on " + labels.full[p[0]] + " " + labels.at;
return head + " on " + p.map((w) => labels.full[w].slice(0, 3)).join(", ") + " " + labels.at;
}
function wake() { setDemo(false); }
function toggle(w: number) {
wake();
setPicked((p) => (p.indexOf(w) < 0 ? p.concat([w]).sort((x, y) => x - y) : p.filter((x) => x !== w)));
}
function segKey(e: KeyEvt) {
const d = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
if (!d) return;
e.preventDefault();
wake();
setMonthly((m) => !m);
}
const cuts = [
sentence(false, DEMO_CUTS[0]),
sentence(false, DEMO_CUTS[1]),
sentence(false, DEMO_CUTS[2]),
sentence(true, [1]),
];
return (
<div className={styles.rc + (demo ? " " + styles.isDemo : "")} style={vars}>
<div className={styles.seg} data-m={monthly ? "m" : "w"} role="radiogroup"
aria-label={labels.cycle} onKeyDown={segKey}>
<span className={styles.ind} aria-hidden="true" />
<button className={styles.opt} type="button" role="radio" data-m="w"
aria-checked={!monthly} onClick={() => { wake(); setMonthly(false); }}>
{labels.weekly}
</button>
<button className={styles.opt} type="button" role="radio" data-m="m"
aria-checked={monthly} onClick={() => { wake(); setMonthly(true); }}>
{labels.monthly}
</button>
</div>
<div className={styles.days} role="group" aria-label={labels.days}>
{labels.abbr.map((a, w) => (
<button key={w} className={styles.chip} type="button" role="checkbox" data-w={w}
aria-checked={picked.indexOf(w) >= 0} aria-label={labels.full[w]}
onClick={() => toggle(w)}>
{a}
</button>
))}
</div>
<p className={styles.sum} aria-live="polite">
<span className={styles.cut} data-k="1">{demo ? cuts[0] : sentence(monthly, picked)}</span>
<span className={styles.cut} data-k="2" aria-hidden="true">{cuts[1]}</span>
<span className={styles.cut} data-k="3" aria-hidden="true">{cuts[2]}</span>
<span className={styles.cut} data-k="4" aria-hidden="true">{cuts[3]}</span>
</p>
</div>
);
}06 칩을 눌러 문장이 되는 반복 규칙 — 측정 누적면적 6.613% · 움직인 프레임 18/23GODRICH 원작 · zip 에 SCSS·React 포함 · 시간 선택 ui 9종 — 시각 하나가 아니라 구간까지
이 데모가 속한 글
시간 선택 ui 9종 — 시각 하나가 아니라 구간까지시간 선택 ui 는 사용자가 몇 시 몇 분을 고르게 하는 화면 부품입니다. 여기 모은 아홉 개는 시각 한 점을 찍는 데서 그치지 않고, 언제부터 언제까지인지, 얼마나 걸리는지까지 손가락과 키보드로 정하게 합니다. 이 글의 데모 전부 보기


