05Working-hours bars stretched one row per weekday
Five weekday rows each carry one horizontal bar, dragging an end snaps it to half-hour steps, and the figures on the bar keep recomputing while you drag.
Best forAdmin settings where shop hours or office hours are registered per weekday
setPointerCaptureclamptranslateX

<main class="stage">
<div class="wh is-demo">
<div class="wh__scale" aria-hidden="true">
<span class="wh__pad"></span>
<span class="wh__marks"><span>0</span><span>6</span><span>12</span><span>18</span><span>24</span></span>
<span class="wh__pad"></span>
</div>
<div class="wh__row" data-d="mon" data-s="18" data-e="36" style="--s:.375;--e:.75">
<span class="wh__day"><span class="wh__full" id="wh-d1">Monday</span><span class="wh__ab" aria-hidden="true">Mo</span></span>
<div class="wh__axis" role="group" aria-labelledby="wh-d1">
<span class="wh__rail" aria-hidden="true"></span>
<span class="wh__pos" aria-hidden="true"><span class="wh__bar"></span></span>
<span class="wh__h wh__h--s"><span class="wh__grip" role="slider" tabindex="0" aria-label="Start" aria-valuemin="0" aria-valuemax="1440" aria-valuenow="540" aria-valuetext="09:00"></span></span>
<span class="wh__h wh__h--e"><span class="wh__grip" role="slider" tabindex="0" aria-label="End" aria-valuemin="0" aria-valuemax="1440" aria-valuenow="1080" aria-valuetext="18:00"></span></span>
</div>
<span class="wh__read"><span data-k="1">09:00–18:00</span></span>
</div>
<div class="wh__row" data-d="tue" data-s="18" data-e="36" style="--s:.375;--e:.75">
<span class="wh__day"><span class="wh__full" id="wh-d2">Tuesday</span><span class="wh__ab" aria-hidden="true">Tu</span></span>
<div class="wh__axis" role="group" aria-labelledby="wh-d2">
<span class="wh__rail" aria-hidden="true"></span>
<span class="wh__pos" aria-hidden="true"><span class="wh__bar"></span></span>
<span class="wh__h wh__h--s"><span class="wh__grip" role="slider" tabindex="0" aria-label="Start" aria-valuemin="0" aria-valuemax="1440" aria-valuenow="540" aria-valuetext="09:00"></span></span>
<span class="wh__h wh__h--e"><span class="wh__grip" role="slider" tabindex="0" aria-label="End" aria-valuemin="0" aria-valuemax="1440" aria-valuenow="1080" aria-valuetext="18:00"></span></span>
</div>
<span class="wh__read"><span data-k="1">09:00–18:00</span></span>
</div>
<div class="wh__row" data-d="wed" data-s="20" data-e="38" style="--s:.41667;--e:.79167">
<span class="wh__day"><span class="wh__full" id="wh-d3">Wednesday</span><span class="wh__ab" aria-hidden="true">We</span></span>
<div class="wh__axis" role="group" aria-labelledby="wh-d3">
<span class="wh__rail" aria-hidden="true"></span>
<span class="wh__pos" aria-hidden="true"><span class="wh__bar"></span></span>
<span class="wh__h wh__h--s"><span class="wh__grip" role="slider" tabindex="0" aria-label="Start" aria-valuemin="0" aria-valuemax="1440" aria-valuenow="600" aria-valuetext="10:00"></span></span>
<span class="wh__h wh__h--e"><span class="wh__grip" role="slider" tabindex="0" aria-label="End" aria-valuemin="0" aria-valuemax="1440" aria-valuenow="1140" aria-valuetext="19:00"></span></span>
</div>
<span class="wh__read"><span data-k="1">10:00–19:00</span><span data-k="2" aria-hidden="true">10:00–22:00</span></span>
</div>
<div class="wh__row" data-d="thu" data-s="18" data-e="35" style="--s:.375;--e:.72917">
<span class="wh__day"><span class="wh__full" id="wh-d4">Thursday</span><span class="wh__ab" aria-hidden="true">Th</span></span>
<div class="wh__axis" role="group" aria-labelledby="wh-d4">
<span class="wh__rail" aria-hidden="true"></span>
<span class="wh__pos" aria-hidden="true"><span class="wh__bar"></span></span>
<span class="wh__h wh__h--s"><span class="wh__grip" role="slider" tabindex="0" aria-label="Start" aria-valuemin="0" aria-valuemax="1440" aria-valuenow="540" aria-valuetext="09:00"></span></span>
<span class="wh__h wh__h--e"><span class="wh__grip" role="slider" tabindex="0" aria-label="End" aria-valuemin="0" aria-valuemax="1440" aria-valuenow="1050" aria-valuetext="17:30"></span></span>
</div>
<span class="wh__read"><span data-k="1">09:00–17:30</span></span>
</div>
<div class="wh__row" data-d="fri" data-s="18" data-e="32" style="--s:.375;--e:.66667">
<span class="wh__day"><span class="wh__full" id="wh-d5">Friday</span><span class="wh__ab" aria-hidden="true">Fr</span></span>
<div class="wh__axis" role="group" aria-labelledby="wh-d5">
<span class="wh__rail" aria-hidden="true"></span>
<span class="wh__pos" aria-hidden="true"><span class="wh__bar"></span></span>
<span class="wh__h wh__h--s"><span class="wh__grip" role="slider" tabindex="0" aria-label="Start" aria-valuemin="0" aria-valuemax="1440" aria-valuenow="540" aria-valuetext="09:00"></span></span>
<span class="wh__h wh__h--e"><span class="wh__grip" role="slider" tabindex="0" aria-label="End" aria-valuemin="0" aria-valuemax="1440" aria-valuenow="960" aria-valuetext="16:00"></span></span>
</div>
<span class="wh__read"><span data-k="1">09:00–16:00</span></span>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop;
$easing: $ease-spring;
$color: $subject-mint;
@include stage($stage-ink);
.wh {
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: $sp-2;
width: min(420px, 90vw);
overflow: hidden; // 손잡이 레이어가 축 폭만큼 밀려도 문서가 안 넘치게
color: $subject-cream;
}
.wh__scale,
.wh__row {
display: grid;
grid-template-columns: clamp(16px, 6vw, 40px) minmax(0, 1fr) clamp(62px, 22vw, 84px);
gap: $sp-2;
align-items: center;
}
.wh__marks {
display: flex;
justify-content: space-between;
}
.wh__marks span {
font-size: clamp(8px, 2.4vw, 10px);
line-height: 12px;
font-weight: 500;
color: rgba($subject-cream, .55);
}
.wh__day {
font-size: clamp(9px, 3vw, 12px);
line-height: 14px;
font-weight: 700;
white-space: nowrap;
}
.wh__ab { display: none; }
.wh__axis {
position: relative;
height: clamp(20px, 6.5vw, 26px);
}
.wh__rail {
position: absolute;
left: 0;
right: 0;
top: 50%;
height: clamp(10px, 3.4vw, 14px);
translate: 0 -50%;
border-radius: $r-pill;
background-color: rgba($subject-cream, .14);
background-image: repeating-linear-gradient(90deg, rgba($subject-cream, .34) 0 1px, rgba($subject-cream, 0) 1px 25%);
}
// 막대는 자리를 translateX 로, 길이를 폭으로 — 늘여도 알약 모서리가 찌그러지지 않는다
.wh__pos {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 100%;
transform: translateX(calc(var(--s) * 100%));
}
.wh__bar {
position: absolute;
left: 0;
top: 50%;
width: calc((var(--e) - var(--s)) * 100%);
height: clamp(10px, 3.4vw, 14px);
translate: 0 -50%;
border-radius: $r-pill;
background-color: $color;
}
.wh__h {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 100%;
pointer-events: none;
}
.wh__h--s { transform: translateX(calc(var(--s) * 100%)); }
.wh__h--e { transform: translateX(calc(var(--e) * 100%)); }
.wh__grip {
position: absolute;
left: 0;
top: 50%;
width: clamp(12px, 4vw, 16px);
height: clamp(12px, 4vw, 16px);
translate: -50% -50%;
border-radius: 50%;
background-color: $subject-cream;
box-shadow: $shadow-press;
cursor: ew-resize;
pointer-events: auto;
touch-action: none;
}
.wh__grip:focus-visible { outline: 3px solid $color; outline-offset: 2px; }
.wh__read {
position: relative;
height: 14px;
}
.wh__read span {
position: absolute;
right: 0;
top: 0;
font-size: clamp(8px, 2.6vw, 11px);
line-height: 14px;
font-weight: 700;
white-space: nowrap;
color: rgba($subject-cream, .88);
}
.wh__read span[data-k="2"] { opacity: 0; }
// ── 자동 루프: 수요일 끝 손잡이가 늘어났다 줄고, 나머지 줄은 시차로 밝아진다 ──
.wh.is-demo .wh__row[data-d="wed"] .wh__bar,
.wh.is-demo .wh__row[data-d="wed"] .wh__h--e {
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.wh.is-demo .wh__row[data-d="wed"] .wh__bar { animation-name: wh-grow; }
.wh.is-demo .wh__row[data-d="wed"] .wh__h--e { animation-name: wh-end; }
@keyframes wh-grow {
0%, 10% { width: 37.5%; }
36%, 52% { width: 50%; }
78%, 100% { width: 37.5%; }
}
@keyframes wh-end {
0%, 10% { transform: translateX(79.167%) scale(1); }
36% { transform: translateX(91.667%) scale(1.18); }
42%, 52% { transform: translateX(91.667%) scale(1); }
78%, 100% { transform: translateX(79.167%) scale(1); }
}
// 끄는 동안 다시 계산된 숫자는 한 프레임에 컷으로 갈린다
.wh.is-demo .wh__row[data-d="wed"] .wh__read span {
animation-duration: $duration;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.wh.is-demo .wh__row[data-d="wed"] .wh__read span[data-k="1"] { animation-name: wh-t1; }
.wh.is-demo .wh__row[data-d="wed"] .wh__read span[data-k="2"] { animation-name: wh-t2; }
@keyframes wh-t1 { 0% { opacity: 1; } 36% { opacity: 0; } 78% { opacity: 1; } 100% { opacity: 1; } }
@keyframes wh-t2 { 0% { opacity: 0; } 36% { opacity: 1; } 78% { opacity: 0; } 100% { opacity: 0; } }
.wh.is-demo .wh__row:not([data-d="wed"]) {
animation-name: wh-glow;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.wh.is-demo .wh__row[data-d="tue"] { animation-delay: 120ms; }
.wh.is-demo .wh__row[data-d="thu"] { animation-delay: 240ms; }
.wh.is-demo .wh__row[data-d="fri"] { animation-delay: 360ms; }
@keyframes wh-glow {
0% { opacity: .55; }
22% { opacity: 1; }
48%, 100% { opacity: .55; }
}
@media (max-width: 340px) {
.wh { gap: $sp-1; }
.wh__scale,
.wh__row { gap: $sp-1; }
.wh__full {
display: inline-block;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.wh__ab { display: inline; }
.wh__read { height: 12px; }
.wh__read span { line-height: 12px; }
.wh__marks span { line-height: 10px; }
}
@media (prefers-reduced-motion: reduce) {
.wh.is-demo .wh__row,
.wh.is-demo .wh__bar,
.wh.is-demo .wh__h--e,
.wh.is-demo .wh__read span { animation: none !important; }
.wh.is-demo .wh__read span[data-k="2"] { opacity: 0; }
}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);
}
.wh {
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 8px;
width: min(420px, 90vw);
overflow: hidden;
color: #fff7e6;
}
.wh__scale,
.wh__row {
display: grid;
grid-template-columns: clamp(16px, 6vw, 40px) minmax(0, 1fr) clamp(62px, 22vw, 84px);
gap: 8px;
align-items: center;
}
.wh__marks {
display: flex;
justify-content: space-between;
}
.wh__marks span {
font-size: clamp(8px, 2.4vw, 10px);
line-height: 12px;
font-weight: 500;
color: rgba(255, 247, 230, 0.55);
}
.wh__day {
font-size: clamp(9px, 3vw, 12px);
line-height: 14px;
font-weight: 700;
white-space: nowrap;
}
.wh__ab {
display: none;
}
.wh__axis {
position: relative;
height: clamp(20px, 6.5vw, 26px);
}
.wh__rail {
position: absolute;
left: 0;
right: 0;
top: 50%;
height: clamp(10px, 3.4vw, 14px);
translate: 0 -50%;
border-radius: 999px;
background-color: rgba(255, 247, 230, 0.14);
background-image: repeating-linear-gradient(90deg, rgba(255, 247, 230, 0.34) 0 1px, rgba(255, 247, 230, 0) 1px 25%);
}
.wh__pos {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 100%;
transform: translateX(calc(var(--s) * 100%));
}
.wh__bar {
position: absolute;
left: 0;
top: 50%;
width: calc((var(--e) - var(--s)) * 100%);
height: clamp(10px, 3.4vw, 14px);
translate: 0 -50%;
border-radius: 999px;
background-color: #4cd4a6;
}
.wh__h {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 100%;
pointer-events: none;
}
.wh__h--s {
transform: translateX(calc(var(--s) * 100%));
}
.wh__h--e {
transform: translateX(calc(var(--e) * 100%));
}
.wh__grip {
position: absolute;
left: 0;
top: 50%;
width: clamp(12px, 4vw, 16px);
height: clamp(12px, 4vw, 16px);
translate: -50% -50%;
border-radius: 50%;
background-color: #fff7e6;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
cursor: ew-resize;
pointer-events: auto;
touch-action: none;
}
.wh__grip:focus-visible {
outline: 3px solid #4cd4a6;
outline-offset: 2px;
}
.wh__read {
position: relative;
height: 14px;
}
.wh__read span {
position: absolute;
right: 0;
top: 0;
font-size: clamp(8px, 2.6vw, 11px);
line-height: 14px;
font-weight: 700;
white-space: nowrap;
color: rgba(255, 247, 230, 0.88);
}
.wh__read span[data-k="2"] {
opacity: 0;
}
.wh.is-demo .wh__row[data-d=wed] .wh__bar,
.wh.is-demo .wh__row[data-d=wed] .wh__h--e {
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.wh.is-demo .wh__row[data-d=wed] .wh__bar {
animation-name: wh-grow;
}
.wh.is-demo .wh__row[data-d=wed] .wh__h--e {
animation-name: wh-end;
}
@keyframes wh-grow {
0%, 10% {
width: 37.5%;
}
36%, 52% {
width: 50%;
}
78%, 100% {
width: 37.5%;
}
}
@keyframes wh-end {
0%, 10% {
transform: translateX(79.167%) scale(1);
}
36% {
transform: translateX(91.667%) scale(1.18);
}
42%, 52% {
transform: translateX(91.667%) scale(1);
}
78%, 100% {
transform: translateX(79.167%) scale(1);
}
}
.wh.is-demo .wh__row[data-d=wed] .wh__read span {
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.wh.is-demo .wh__row[data-d=wed] .wh__read span[data-k="1"] {
animation-name: wh-t1;
}
.wh.is-demo .wh__row[data-d=wed] .wh__read span[data-k="2"] {
animation-name: wh-t2;
}
@keyframes wh-t1 {
0% {
opacity: 1;
}
36% {
opacity: 0;
}
78% {
opacity: 1;
}
100% {
opacity: 1;
}
}
@keyframes wh-t2 {
0% {
opacity: 0;
}
36% {
opacity: 1;
}
78% {
opacity: 0;
}
100% {
opacity: 0;
}
}
.wh.is-demo .wh__row:not([data-d=wed]) {
animation-name: wh-glow;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.wh.is-demo .wh__row[data-d=tue] {
animation-delay: 120ms;
}
.wh.is-demo .wh__row[data-d=thu] {
animation-delay: 240ms;
}
.wh.is-demo .wh__row[data-d=fri] {
animation-delay: 360ms;
}
@keyframes wh-glow {
0% {
opacity: 0.55;
}
22% {
opacity: 1;
}
48%, 100% {
opacity: 0.55;
}
}
@media (max-width: 340px) {
.wh {
gap: 4px;
}
.wh__scale,
.wh__row {
gap: 4px;
}
.wh__full {
display: inline-block;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
.wh__ab {
display: inline;
}
.wh__read {
height: 12px;
}
.wh__read span {
line-height: 12px;
}
.wh__marks span {
line-height: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.wh.is-demo .wh__row,
.wh.is-demo .wh__bar,
.wh.is-demo .wh__h--e,
.wh.is-demo .wh__read span {
animation: none !important;
}
.wh.is-demo .wh__read span[data-k="2"] {
opacity: 0;
}
}(function () {
var root = document.querySelector('.wh');
var rows = [].slice.call(root.querySelectorAll('.wh__row'));
var drag = null;
function clamp(x, lo, hi) { return Math.min(Math.max(x, lo), hi); } // 축 밖으로 못 나간다
function fmt(t) { var h = t / 2 | 0; return (h < 10 ? '0' : '') + h + ':' + (t % 2 ? '30' : '00'); }
function draw(r) {
var s = +r.dataset.s, e = +r.dataset.e, g = r.querySelectorAll('.wh__grip');
r.style.setProperty('--s', s / 48); r.style.setProperty('--e', e / 48);
g[0].setAttribute('aria-valuenow', s * 30); g[0].setAttribute('aria-valuetext', fmt(s));
g[1].setAttribute('aria-valuenow', e * 30); g[1].setAttribute('aria-valuetext', fmt(e));
r.querySelector('.wh__read [data-k="1"]').textContent = fmt(s) + '–' + fmt(e);
}
function set(r, i, t) { // 30 minutes 한 칸
if (i) r.dataset.e = clamp(t, +r.dataset.s + 1, 48);
else r.dataset.s = clamp(t, 0, +r.dataset.e - 1);
draw(r);
}
function at(r, e) {
var b = r.querySelector('.wh__axis').getBoundingClientRect();
return Math.round((e.clientX - b.left) / b.width * 48);
}
function grip(e) { return e.target.closest ? e.target.closest('.wh__grip') : null; }
function side(g) { return g.parentNode.classList.contains('wh__h--e') ? 1 : 0; }
root.addEventListener('pointerdown', function (e) {
var g = grip(e); if (!g) return;
root.classList.remove('is-demo');
drag = { r: g.closest('.wh__row'), i: side(g) };
g.setPointerCapture(e.pointerId); g.focus({ preventScroll: true }); e.preventDefault();
});
root.addEventListener('pointermove', function (e) { if (drag) set(drag.r, drag.i, at(drag.r, e)); });
root.addEventListener('pointerup', function () { drag = null; });
root.addEventListener('keydown', function (e) {
var g = grip(e); if (!g) return;
var d = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
if (!d && e.key !== 'Home' && e.key !== 'End') return;
root.classList.remove('is-demo'); e.preventDefault();
var r = g.closest('.wh__row'), i = side(g);
set(r, i, e.key === 'Home' ? 0 : e.key === 'End' ? 48 : (i ? +r.dataset.e : +r.dataset.s) + d);
});
rows.forEach(draw);
})();import { useRef, useState } from "react";
import type { CSSProperties } from "react";
import styles from "./WorkingHoursDayBars.module.scss";
export type WorkingHoursDayBarsProps = {
labels?: { full: string[]; abbr: string[]; start: string; end: string; marks: string[] };
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type RowVars = CSSProperties & { "--s"?: string; "--e"?: string };
type PtrEvt = {
clientX: number; pointerId: number; preventDefault(): void;
currentTarget: { setPointerCapture(id: number): void };
};
type KeyEvt = { key: string; preventDefault(): void };
type Row = { key: string; s: number; e: number };
const START: Row[] = [
{ key: "mon", s: 18, e: 36 },
{ key: "tue", s: 18, e: 36 },
{ key: "wed", s: 20, e: 38 },
{ key: "thu", s: 18, e: 35 },
{ key: "fri", s: 18, e: 32 },
];
function clamp(x: number, lo: number, hi: number): number { return Math.min(Math.max(x, lo), hi); }
function fmt(t: number): string { // 30분 한 칸
const h = Math.floor(t / 2);
return (h < 10 ? "0" : "") + h + ":" + (t % 2 ? "30" : "00");
}
export default function WorkingHoursDayBars({
labels = {
full: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"],
abbr: ["Mo", "Tu", "We", "Th", "Fr"],
start: "Start",
end: "End",
marks: ["0", "6", "12", "18", "24"],
},
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#4cd4a6",
}: WorkingHoursDayBarsProps) {
const [demo, setDemo] = useState(true);
const [rows, setRows] = useState<Row[]>(START);
const axes = useRef<(HTMLDivElement | null)[]>([]);
const drag = useRef<{ r: number; i: number } | null>(null);
const vars: Vars = { "--duration": duration + "ms", "--easing": easing, "--color": color };
function set(r: number, i: number, t: number) {
setRows((p) => p.map((row, n) => {
if (n !== r) return row;
return i ? { ...row, e: clamp(t, row.s + 1, 48) } : { ...row, s: clamp(t, 0, row.e - 1) };
}));
}
function at(r: number, e: PtrEvt): number {
const list = axes.current;
const el = list ? list[r] : null;
if (!el) return 0;
const b = el.getBoundingClientRect();
return Math.round(((e.clientX - b.left) / b.width) * 48);
}
function down(r: number, i: number) {
return (e: PtrEvt) => {
setDemo(false);
drag.current = { r, i };
e.currentTarget.setPointerCapture(e.pointerId); // 손가락이 축을 벗어나도 계속 따라온다
e.preventDefault();
};
}
function move(e: PtrEvt) {
const d = drag.current;
if (d) set(d.r, d.i, at(d.r, e));
}
function up() { drag.current = null; }
function key(r: number, i: number) {
return (e: KeyEvt) => {
const d = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0;
if (!d && e.key !== "Home" && e.key !== "End") return;
setDemo(false);
e.preventDefault();
const row = rows[r];
const t = e.key === "Home" ? 0 : e.key === "End" ? 48 : (i ? row.e : row.s) + d;
set(r, i, t);
};
}
return (
<div className={styles.wh + (demo ? " " + styles.isDemo : "")} style={vars}>
<div className={styles.scale} aria-hidden="true">
<span />
<span className={styles.marks}>{labels.marks.map((m) => <span key={m}>{m}</span>)}</span>
<span />
</div>
{rows.map((row, r) => {
const rv: RowVars = { "--s": String(row.s / 48), "--e": String(row.e / 48) };
return (
<div className={styles.row} data-d={row.key} key={row.key} style={rv}>
<span className={styles.day}>
<span className={styles.full} id={"wh-d" + r}>{labels.full[r]}</span>
<span className={styles.ab} aria-hidden="true">{labels.abbr[r]}</span>
</span>
<div className={styles.axis} role="group" aria-labelledby={"wh-d" + r}
ref={(el: HTMLDivElement | null) => { if (axes.current) axes.current[r] = el; }}>
<span className={styles.rail} aria-hidden="true" />
<span className={styles.pos} aria-hidden="true"><span className={styles.bar} /></span>
{[0, 1].map((i) => (
<span key={i} className={styles.h + " " + (i ? styles.hE : styles.hS)}>
<span
className={styles.grip}
role="slider"
tabIndex={0}
aria-label={i ? labels.end : labels.start}
aria-valuemin={0}
aria-valuemax={1440}
aria-valuenow={(i ? row.e : row.s) * 30}
aria-valuetext={fmt(i ? row.e : row.s)}
onPointerDown={down(r, i)}
onPointerMove={move}
onPointerUp={up}
onKeyDown={key(r, i)}
/>
</span>
))}
</div>
<span className={styles.read}>
<span data-k="1">{fmt(row.s) + "–" + fmt(row.e)}</span>
{row.key === "wed" ? <span data-k="2" aria-hidden="true">{fmt(row.s) + "–" + fmt(44)}</span> : null}
</span>
</div>
);
})}
</div>
);
}05 Working-hours bars stretched one row per weekday — Measured area 13.674% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · 9 time picker ui patterns, from a point to a span
The post this demo lives in
9 time picker ui patterns, from a point to a spanA time picker ui is the part of a screen where someone sets a time without typing it. These nine go past a single time o See all demos in this post


