02Month-year unit value toggle — when the display-unit value switches from month to year, several years can be skipped at once
Each time the center label value is engaged, the display-unit value toggles between month and year, and jumping while in year mode covers a larger span at once.
Best forA birth-date input, a period picker for reaching far-back records
AUTO_SEQCELL_TOTALmode

<main class="stage">
<div class="mj is-demo" id="mj">
<div class="mj__bar">
<button type="button" class="mj__nav" id="mjPrev" aria-label="Previous month">‹</button>
<button type="button" class="mj__head" id="mjHead">March 2026</button>
<button type="button" class="mj__nav" id="mjNext" aria-label="Next month">›</button>
</div>
<div class="mj__weekday" aria-hidden="true">
<span>Sun</span><span>Mon</span><span>Tue</span><span>Wed</span><span>Thu</span><span>Fri</span><span>Sat</span>
</div>
<div class="mj__viewport" id="mjViewport">
<div class="mj__grid" id="mjMonthGrid"></div>
</div>
<div class="mj__yearGrid" id="mjYearGrid"></div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: slideTo() 가 뷰포트 실측 폭(getBoundingClientRect)을 슬라이드 거리로,
// 목표-현재 월 오프셋의 부호를 슬라이드 방향으로 써서 translateX 를 계산한다.
@include stage($stage-yellow);
.mj {
width: 100%;
max-width: 230px;
background: $stage-paper;
border-radius: $r-card;
padding: $sp-2;
box-shadow: $shadow-raised;
box-sizing: border-box;
}
.mj__bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $sp-1;
}
.mj__nav {
width: 20px;
height: 20px;
display: grid;
place-items: center;
border: none;
border-radius: $r-xs;
background: transparent;
color: $subject-ink;
font-size: 14px;
cursor: pointer;
}
.mj__head {
border: none;
background: transparent;
color: $subject-ink;
font-size: 12px;
font-weight: 700;
cursor: pointer;
padding: $sp-1 $sp-2;
border-radius: $r-xs;
}
.mj__weekday {
display: grid;
grid-template-columns: repeat(7, 1fr);
margin-bottom: $sp-1;
}
.mj__weekday span {
font-size: 9px;
text-align: center;
color: rgba($subject-ink, .72);
}
.mj__viewport {
overflow: hidden;
}
.mj__grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-auto-rows: 21px;
transition: transform $dur-base $ease-spring;
}
.mj__cell {
display: grid;
place-items: center;
font-size: 11px;
color: $subject-ink;
border-radius: $r-xs;
}
.mj__cell--out {
visibility: hidden;
}
.mj__yearGrid {
display: none;
grid-template-columns: repeat(4, 1fr);
gap: $sp-1;
}
.mj--year .mj__weekday,
.mj--year .mj__viewport {
display: none;
}
.mj--year .mj__yearGrid {
display: grid;
}
.mj__ycell {
height: 26px;
display: grid;
place-items: center;
border: none;
border-radius: $r-xs;
background: $subject-blue;
color: $subject-cream;
font-size: 10px;
cursor: pointer;
}
.mj__ycell--now {
background: $subject-blue-deep;
}
@media (max-width: 340px) {
.mj { max-width: 190px; }
.mj__grid { grid-auto-rows: 17px; }
.mj__cell { font-size: 10px; }
.mj__ycell { height: 21px; font-size: 9px; }
.mj__weekday span { font-size: 8px; }
}
@media (prefers-reduced-motion: reduce) {
.mj__grid { 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);
}
.mj {
width: 100%;
max-width: 230px;
background: #ffffff;
border-radius: 16px;
padding: 8px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
box-sizing: border-box;
}
.mj__bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 4px;
}
.mj__nav {
width: 20px;
height: 20px;
display: grid;
place-items: center;
border: none;
border-radius: 4px;
background: transparent;
color: #17141a;
font-size: 14px;
cursor: pointer;
}
.mj__head {
border: none;
background: transparent;
color: #17141a;
font-size: 12px;
font-weight: 700;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
}
.mj__weekday {
display: grid;
grid-template-columns: repeat(7, 1fr);
margin-bottom: 4px;
}
.mj__weekday span {
font-size: 9px;
text-align: center;
color: rgba(23, 20, 26, 0.72);
}
.mj__viewport {
overflow: hidden;
}
.mj__grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-auto-rows: 21px;
transition: transform 300ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.mj__cell {
display: grid;
place-items: center;
font-size: 11px;
color: #17141a;
border-radius: 4px;
}
.mj__cell--out {
visibility: hidden;
}
.mj__yearGrid {
display: none;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
}
.mj--year .mj__weekday,
.mj--year .mj__viewport {
display: none;
}
.mj--year .mj__yearGrid {
display: grid;
}
.mj__ycell {
height: 26px;
display: grid;
place-items: center;
border: none;
border-radius: 4px;
background: #2f6df6;
color: #fff7e6;
font-size: 10px;
cursor: pointer;
}
.mj__ycell--now {
background: #0a4fe8;
}
@media (max-width: 340px) {
.mj {
max-width: 190px;
}
.mj__grid {
grid-auto-rows: 17px;
}
.mj__cell {
font-size: 10px;
}
.mj__ycell {
height: 21px;
font-size: 9px;
}
.mj__weekday span {
font-size: 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.mj__grid {
transition: none;
}
}(function () {
var YEAR = 2026; // 계산 겹 상수: 연도 고정, new Date() 미사용
var MONTH_LABEL = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
var HEADER_LABEL = [
'January 2026', 'February 2026', 'March 2026', 'April 2026', 'May 2026', 'June 2026',
'July 2026', 'August 2026', 'September 2026', 'October 2026', 'November 2026', 'December 2026'
];
var AUTO_SEQ = [3, 7, 11, 3]; // 계산 겹: 자동재생이 도는 목표 Mon 순서, 오프셋 부호로 슬라이드 방향을 정한다
var CELL_TOTAL = 42; // 계산 겹: 6행 고정 — 월마다 칸 수가 달라도 뷰포트 높이가 흔들리지 않는다
function isLeap(y) { return (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0; }
function daysInMonth(y, m) {
var t = [31, isLeap(y) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
return t[m - 1];
}
function dow(y, m, d) { // Zeller 합동식 — 0=Sun ~ 6=Sat
var yy = y, mm = m;
if (mm < 3) { yy -= 1; mm += 12; }
var K = yy % 100, J = Math.floor(yy / 100);
var h = (d + Math.floor(13 * (mm + 1) / 5) + K + Math.floor(K / 4) + Math.floor(J / 4) - 2 * J) % 7;
h = ((h % 7) + 7) % 7;
return (h + 6) % 7;
}
var root = document.getElementById('mj');
var head = document.getElementById('mjHead');
var prevBtn = document.getElementById('mjPrev');
var nextBtn = document.getElementById('mjNext');
var viewport = document.getElementById('mjViewport');
var monthGrid = document.getElementById('mjMonthGrid');
var yearGrid = document.getElementById('mjYearGrid');
var current = 3;
var mode = 'month';
function renderMonth(m) {
monthGrid.innerHTML = '';
var n = daysInMonth(YEAR, m);
var lead = dow(YEAR, m, 1);
var trail = CELL_TOTAL - lead - n;
var i, cell;
for (i = 0; i < lead; i++) {
cell = document.createElement('span');
cell.className = 'mj__cell mj__cell--out';
monthGrid.appendChild(cell);
}
for (var d = 1; d <= n; d++) {
cell = document.createElement('span');
cell.className = 'mj__cell';
cell.textContent = String(d);
monthGrid.appendChild(cell);
}
for (i = 0; i < trail; i++) {
cell = document.createElement('span');
cell.className = 'mj__cell mj__cell--out';
monthGrid.appendChild(cell);
}
}
function renderYear(highlight) {
yearGrid.innerHTML = '';
for (var i = 0; i < 12; i++) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'mj__ycell' + (i + 1 === highlight ? ' mj__ycell--now' : '');
btn.textContent = MONTH_LABEL[i];
btn.dataset.m = String(i + 1);
btn.addEventListener('click', function (e) {
human();
slideTo(parseInt(e.currentTarget.dataset.m, 10));
});
yearGrid.appendChild(btn);
}
}
function setHead(m) { head.textContent = HEADER_LABEL[m - 1]; }
function slideTo(target) {
// 계산 겹: 뷰포트 실측 폭을 슬라이드 거리로, 목표-현재 오프셋 부호를 슬라이드 방향으로 쓴다
var dist = viewport.getBoundingClientRect().width;
var dir = target >= current ? 1 : -1;
current = target;
renderMonth(current);
setHead(current);
root.classList.remove('mj--year');
mode = 'month';
monthGrid.style.transition = 'none';
monthGrid.style.transform = 'translateX(' + (dir * dist) + 'px)';
void monthGrid.offsetWidth;
monthGrid.style.transition = '';
monthGrid.style.transform = 'translateX(0)';
}
function stepMonth(delta) {
var target = current + delta;
if (target < 1) target = 12;
if (target > 12) target = 1;
slideTo(target);
}
function toggleYear() {
mode = mode === 'year' ? 'month' : 'year';
root.classList.toggle('mj--year', mode === 'year');
if (mode === 'year') renderYear(current);
}
function human() {
root.classList.remove('is-demo');
stopAutoplay();
}
head.addEventListener('click', function () { human(); toggleYear(); });
prevBtn.addEventListener('click', function () { human(); stepMonth(-1); });
nextBtn.addEventListener('click', function () { human(); stepMonth(1); });
document.addEventListener('keydown', function (e) {
if (e.key === 'ArrowLeft') { human(); stepMonth(-1); }
if (e.key === 'ArrowRight') { human(); stepMonth(1); }
});
var seqIdx = 0;
var autoTimer = null;
function stopAutoplay() { if (autoTimer) { clearInterval(autoTimer); autoTimer = null; } }
function tick() {
// 자동재생은 슬라이드(transform 트랜지션)만 반복한다 — 연·Mon 전환 자체는
// display 즉시 전환이라 캡처 프레임에 순간 컷으로만 찍혀 움직인 프레임이 안 는다.
seqIdx = (seqIdx + 1) % AUTO_SEQ.length;
slideTo(AUTO_SEQ[seqIdx]);
}
renderMonth(current);
setHead(current);
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!reduce) { autoTimer = setInterval(tick, 480); }
})();import { useEffect, useRef, useState } from 'react';
import styles from './DatePickerMonthYearJump.module.scss';
type Box<T> = { current: T };
const YEAR = 2026; // 계산 겹 상수: 연도 고정, new Date() 미사용
const MONTH_LABEL = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'];
const HEADER_LABEL = [
'2026년 1월', '2026년 2월', '2026년 3월', '2026년 4월', '2026년 5월', '2026년 6월',
'2026년 7월', '2026년 8월', '2026년 9월', '2026년 10월', '2026년 11월', '2026년 12월',
];
const AUTO_SEQ = [3, 7, 11, 3]; // 계산 겹: 자동재생이 도는 목표 월 순서, 오프셋 부호로 슬라이드 방향을 정한다
const CELL_TOTAL = 42; // 계산 겹: 6행 고정 — 월마다 칸 수가 달라도 뷰포트 높이가 흔들리지 않는다
function isLeap(y: number) {
return (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
}
function daysInMonth(y: number, m: number) {
const t = [31, isLeap(y) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
return t[m - 1];
}
function dow(y: number, m: number, d: number) { // Zeller 합동식 — 0=일 ~ 6=토
let yy = y;
let mm = m;
if (mm < 3) { yy -= 1; mm += 12; }
const K = yy % 100;
const J = Math.floor(yy / 100);
let h = (d + Math.floor((13 * (mm + 1)) / 5) + K + Math.floor(K / 4) + Math.floor(J / 4) - 2 * J) % 7;
h = ((h % 7) + 7) % 7;
return (h + 6) % 7;
}
function buildCells(m: number) {
const n = daysInMonth(YEAR, m);
const lead = dow(YEAR, m, 1);
const trail = CELL_TOTAL - lead - n;
const cells: { key: string; d: number | null }[] = [];
for (let i = 0; i < lead; i++) cells.push({ key: 'l' + i, d: null });
for (let d = 1; d <= n; d++) cells.push({ key: 'd' + d, d });
for (let i = 0; i < trail; i++) cells.push({ key: 't' + i, d: null });
return cells;
}
const WEEKDAY = ['일', '월', '화', '수', '목', '금', '토'];
export default function DatePickerMonthYearJump() {
const viewportRef = useRef(null) as unknown as Box<HTMLDivElement>;
const gridRef = useRef(null) as unknown as Box<HTMLDivElement>;
const [isDemo, setIsDemo] = useState(true);
const [mode, setMode] = useState<'month' | 'year'>('month');
const [current, setCurrent] = useState(3);
const seqIdxRef = useRef(0) as unknown as Box<number>;
function human() {
setIsDemo(false);
}
function slideTo(target: number) {
// 계산 겹: 뷰포트 실측 폭을 슬라이드 거리로, 목표-현재 월 오프셋 부호를 슬라이드 방향으로 쓴다
const viewport = viewportRef.current;
const grid = gridRef.current;
if (!viewport || !grid) { setCurrent(target); setMode('month'); return; }
const dist = viewport.getBoundingClientRect().width;
const dir = target >= current ? 1 : -1;
setCurrent(target);
setMode('month');
grid.style.transition = 'none';
grid.style.transform = 'translateX(' + dir * dist + 'px)';
void grid.offsetWidth;
grid.style.transition = '';
grid.style.transform = 'translateX(0)';
}
function stepMonth(delta: number) {
let target = current + delta;
if (target < 1) target = 12;
if (target > 12) target = 1;
slideTo(target);
}
useEffect(() => {
function onKey(e: KeyboardEvent) {
if (e.key === 'ArrowLeft') { human(); stepMonth(-1); }
if (e.key === 'ArrowRight') { human(); stepMonth(1); }
}
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
});
useEffect(() => {
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!isDemo || reduce) return;
const timer = window.setInterval(() => {
seqIdxRef.current = (seqIdxRef.current + 1) % AUTO_SEQ.length;
slideTo(AUTO_SEQ[seqIdxRef.current]);
}, 480);
return () => window.clearInterval(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isDemo]);
const cells = buildCells(current);
return (
<div className={`${styles.mj} ${isDemo ? styles.isDemo : ''} ${mode === 'year' ? styles.mjYear : ''}`}>
<div className={styles.mjBar}>
<button type="button" className={styles.mjNav} aria-label="이전 달" onClick={() => { human(); stepMonth(-1); }}>‹</button>
<button
type="button"
className={styles.mjHead}
onClick={() => { human(); setMode((v) => (v === 'year' ? 'month' : 'year')); }}
>
{HEADER_LABEL[current - 1]}
</button>
<button type="button" className={styles.mjNav} aria-label="다음 달" onClick={() => { human(); stepMonth(1); }}>›</button>
</div>
<div className={styles.mjWeekday} aria-hidden="true">
{WEEKDAY.map((w) => <span key={w}>{w}</span>)}
</div>
<div className={styles.mjViewport} ref={viewportRef}>
<div className={styles.mjGrid} ref={gridRef}>
{cells.map((c) => (
<span key={c.key} className={`${styles.mjCell} ${c.d === null ? styles.mjCellOut : ''}`}>{c.d ?? ''}</span>
))}
</div>
</div>
<div className={styles.mjYearGrid}>
{MONTH_LABEL.map((label, i) => (
<button
type="button"
key={label}
className={`${styles.mjYcell} ${i + 1 === current ? styles.mjYcellNow : ''}`}
onClick={() => { human(); slideTo(i + 1); }}
>
{label}
</button>
))}
</div>
</div>
);
}02 Month-year unit value toggle — when the display-unit value switches from month to year, several years can be skipped at once — Measured area 5.122% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · Date Picker UI: 9 Popover & Calendar Patterns
The post this demo lives in
Date Picker UI: 9 Popover & Calendar PatternsDate picker UI: One of nine opens a calendar popover when a focus value turns on, one fades out-of-range dates to unsele See all demos in this post
If you like this effect - similar demos
Pickers08 Color-temperature track — interpolating hue and saturation from the value ratioCSS Range Slider: 9 Physics-Grade Custom Sliders
Pickers01 Hover preview fill — the filled value previews up to the hovered star's order value, then reverts once the pointer leavesStar Rating UI: 9 Interactive Rating Patterns
Pickers08 An Equalizer Whose Bars Jump at Once While the Peak Caps Linger and Then Fall Like GravityCSS Waveform: 9 Peak Columns and Trim Pickers