익스클루시브
Dateline <mv-dateline>
날짜가 바뀌는 상황을 절대 놓치지 않도록 해 주는, 시간대를 인식하는 시각 또는 마감일 표시.
문화적 레퍼런스
80일간의 세계 일주, 쥘 베른(1872년, 소설). 여행자는 하루 차이로 내기에서 졌다고 믿지만, 동쪽으로 가면서 날짜변경선을 넘어 하루를 벌었다는 사실을 깨닫습니다. 같은 순간이 그가 있던 곳에서는 다른 날짜였던 것입니다. UI에서는 모든 시각을 양쪽 기준으로 동시에 적고, 보는 사람에게 해당하는 날짜를 명시적으로 표시하므로 누구도 하루 차이로 마감을 놓치지 않습니다.
작동 방식
날짜가 바뀌는 상황을 절대 놓치지 않도록 해 주는, 시간대를 인식하는 시각 또는 마감일 표시. 절대 시각(오프셋이 포함된 datetime 또는 기준 시간대의 현지 시각)과 기준 시간대를 지정하면 명시적인 날짜 변경 칩과 함께 양쪽을 한 문장으로 적어 줍니다: “Fri, Oct 2, 5:00 PM PT, that’s Sat, Oct 3, 2:00 AM your time (+1 day)”. 이는 실제 <time datetime> 요소로 출력됩니다. 인라인 변형은 본문 안에 자연스럽게 들어가고, 카드 변형은 절대 시간에 맞춰 정렬된 시간대별 24시간 띠(근무 시간, 저녁, 밤, 주말은 음영 처리, 각 시간대의 자정은 해당 위치에 표시, 해당 시각은 모든 행을 관통하는 선 하나로, 가까울 때는 현재 시각을 점선으로 표시), 누가 근무 중이고 누가 자고 있는지 보여 주는 팀 시간대, 텍스트가 바뀔 때만 갱신되는 실시간 카운트다운, 일광 절약 시간 경고(해당 시각 전후 일주일 이내의 시간 변경, 오늘과 다를 시차, 존재하지 않거나 두 번 발생하는 현지 시각), 보는 사람의 시간대 선택기, 캘린더 도우미(선택적 알림이 포함된 .ics 다운로드, Google Calendar 링크, 복사)를 추가합니다. 모든 시간대 계산은 Intl.DateTimeFormat만 사용합니다: 모든 IANA 시간대, 30분 단위 오프셋과 DST 전환, 로캘별 12시간제 또는 24시간제를 지원합니다.
| 카테고리 | 데이터 표시 |
|---|---|
| 유형 | Web Component (<mv-dateline>) |
| 상태 | 안정 |
| 키트 | 정직한 시스템 상태 |
| 함께 설치되는 항목 | button |
| Keywords | exclusive, culture, time-zone, timezone, date, time, deadline, countdown, world-clock, meeting-planner, dst, daylight-saving, intl, ics, calendar, google-calendar, distributed-team, remote, working-hours, relative-time |
When to use
- A deadline, launch or webinar is set in one time zone and read by people all over the world
- An email, status page or changelog must state a maintenance window without anyone misreading the day
- A distributed team needs to see who is working, off or asleep at a proposed meeting time
- A recurring meeting falls in the weeks when regions switch daylight saving time on different dates
Avoid when
- The user has to pick a date or time rather than read one → use Date Picker instead
- The countdown guards a signed-in session that must be kept alive → use Hatch instead
- The time is purely local (a store's opening hours for people standing in it): one plain time is clearer
설치
node scripts/add.mjs dateline --out ./src/marvelousMarvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["dateline"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, components/button/button.css, core/dom.js, core/element.js, core/observe.js, components/dateline/dateline.js, components/dateline/dateline.css.
사용법
빠른 시작, 동작하는 가장 작은 마크업:
<p>Submissions close <mv-dateline datetime="2026-10-02T17:00" zone="America/Los_Angeles"></mv-dateline>.</p>기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:
<div id="dl-demo" style="width:min(100%,60rem);margin-inline:auto">
<style>
#dl-demo { display:grid; grid-template-columns:minmax(0,1fr); gap:1.25rem; align-content:start }
#dl-demo .dl-bar { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1.25rem; flex-wrap:wrap; padding:.75rem 1rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg-subtle) }
#dl-demo .dl-view { display:flex; align-items:center; gap:.5rem .75rem; flex-wrap:wrap; min-width:0; max-width:100% }
#dl-demo .dl-scroll { max-width:100%; overflow-x:auto; scrollbar-width:none }
#dl-demo .dl-k { color:var(--mv-fg-muted); font-size:.8125rem; font-weight:500 }
#dl-demo .dl-tools { display:flex; align-items:center; gap:1rem; flex-wrap:wrap }
#dl-demo .dl-grid { display:grid; grid-template-columns:minmax(0,1.1fr) minmax(0,1fr); gap:1.25rem; align-items:start }
#dl-demo .dl-prose { display:grid; gap:.875rem; padding:1.25rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm) }
#dl-demo .dl-prose h4 { margin:0; font-size:.8125rem; font-weight:500; color:var(--mv-fg-muted) }
#dl-demo .dl-prose p { margin:0; font-size:.9375rem; line-height:1.65 }
#dl-demo .dl-prose .dl-from { display:block; margin-bottom:.125rem; color:var(--mv-fg-subtle); font-size:.75rem }
#dl-demo .dl-sep { height:1px; background:var(--mv-border) }
#dl-demo .dl-log { display:flex; align-items:center; gap:.5rem; min-height:1.5rem; color:var(--mv-fg-muted); font-size:.75rem }
@media (max-width:52rem) { #dl-demo .dl-grid { grid-template-columns:minmax(0,1fr) } }
</style>
<div class="dl-bar">
<div class="dl-view">
<span class="dl-k" id="dl-view-label">Preview as a viewer in</span>
<div class="dl-scroll"><mv-segmented id="dl-viewer" aria-labelledby="dl-view-label" value="">
<button value="">This device</button>
<button value="Europe/Madrid">Madrid</button>
<button value="America/New_York">New York</button>
<button value="Asia/Singapore">Singapore</button>
<button value="Pacific/Auckland">Auckland</button>
</mv-segmented></div>
</div>
<div class="dl-tools">
<label class="mv-choice">
<input type="checkbox" role="switch" class="mv-switch" id="dl-24">
<span class="mv-choice-text"><span class="mv-choice-title">24-hour clock</span></span>
</label>
<button type="button" class="mv-button" data-variant="outline" data-size="sm" id="dl-soon">Deadline in 15 s</button>
</div>
</div>
<!-- A global deadline set in Pacific time, with the team's zones -->
<mv-dateline id="dl-main" variant="card" data-dl-roll
label="Global Hackathon · submissions close"
datetime="2026-10-02T17:00" zone="America/Los_Angeles"
zones="Europe/Paris Asia/Kolkata=Bengaluru Asia/Tokyo"
countdown countdown-text="Closes in {time}" ended-text="Closed {time} ago"
actions="google ics copy" picker reminder="2h"
summary="Global Hackathon: submissions close"
details="Upload your repository link and a 3-minute video on the submission portal."></mv-dateline>
<div class="dl-grid">
<!-- A recurring meeting in the weeks when Europe and the US switch clocks on different dates -->
<mv-dateline id="dl-sync" variant="card"
label="Weekly product sync"
datetime="2026-10-26T09:00" zone="America/New_York"
zones="Europe/London Asia/Singapore"
countdown countdown-text="Starts in {time}" ended-text="Started {time} ago"
duration="45m" actions="ics" location="Video call"></mv-dateline>
<div class="dl-prose">
<h4>In running text</h4>
<p><span class="dl-from">Status page · Planned maintenance</span>
The EU and US clusters go read-only on
<mv-dateline id="dl-maint" data-dl-roll datetime="2026-10-04T23:00Z" zone="UTC" countdown></mv-dateline>
for about 40 minutes.</p>
<div class="dl-sep"></div>
<p><span class="dl-from">Email · Tarnwick Conference 2027</span>
Early-bird registration opens
<mv-dateline id="dl-reg" data-dl-roll datetime="2026-10-01T09:00" zone="Pacific/Auckland" zone-label="Auckland"></mv-dateline>,
with 300 seats at $149.</p>
<div class="dl-sep"></div>
<p class="dl-log" id="dl-log" aria-live="polite"><span class="mv-badge" data-variant="secondary" data-shape="pill" id="dl-log-badge">Events</span><span id="dl-log-text">Change the viewer zone or use the picker.</span></p>
</div>
</div>
<script type="module">
const root = document.getElementById("dl-demo");
await customElements.whenDefined("mv-dateline");
const all = [...root.querySelectorAll("mv-dateline")];
const $ = (id) => document.getElementById(id);
const log = (text, variant = "secondary", badge = "Event") => {
$("dl-log-text").textContent = text;
$("dl-log-badge").textContent = badge;
$("dl-log-badge").dataset.variant = variant;
};
// Keep the demo in the future: roll wall-time dates forward by whole weeks.
const pad = (n) => String(n).padStart(2, "0");
for (const el of root.querySelectorAll("[data-dl-roll]")) {
let guard = 0;
while (el.moment && el.moment.getTime() < Date.now() + 36e5 && guard++ < 200) {
const [date, rest] = el.datetime.split("T");
const d = new Date(`${date}T00:00:00Z`);
d.setUTCDate(d.getUTCDate() + 7);
el.datetime = `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}T${rest}`;
}
}
// The sync falls on the Monday after Europe's clock change (last Sunday of October).
const sync = $("dl-sync");
const europeGapMonday = (y) => {
const d = new Date(Date.UTC(y, 9, 31));
d.setUTCDate(31 - d.getUTCDay() + 1);
return `${y}-10-${pad(d.getUTCDate())}T09:00`;
};
let y = new Date().getUTCFullYear();
sync.datetime = europeGapMonday(y);
if (sync.moment.getTime() < Date.now()) sync.datetime = europeGapMonday(y + 1);
const viewer = $("dl-viewer");
const setViewer = (zone) => { for (const el of all) el.viewerZone = zone || null; };
viewer.addEventListener("mv-change", (e) => setViewer(e.detail.value));
$("dl-24").addEventListener("change", (e) => { for (const el of all) el.clock = e.target.checked ? "24" : null; });
root.addEventListener("mv-zone-change", (e) => {
if (e.defaultPrevented) return;
if (e.detail.source !== "user") { log(`Device zone changed to ${e.detail.zone}.`, "info", "Zone"); return; }
// The picker of one card drives the whole page.
setViewer(e.detail.automatic ? "" : e.detail.zone);
viewer.value = [...viewer.querySelectorAll("button")].some((b) => b.value === (e.detail.automatic ? "" : e.detail.zone)) ? (e.detail.automatic ? "" : e.detail.zone) : viewer.value;
log(`Showing times in ${e.detail.zone.replace(/_/g, " ")}.`, "info", "Zone");
});
root.addEventListener("mv-calendar", (e) => {
if (e.detail.kind === "google") { e.preventDefault(); log("Google Calendar link ready (opening is disabled in this demo).", "info", "Calendar"); }
else log(`.ics file downloaded${e.target.reminder ? `, with a reminder ${e.target.reminder} before` : ""}.`, "success", "Calendar");
});
root.addEventListener("mv-copy", (e) => log(`Copied: “${e.detail.text}”`, "success", "Copy"));
const main = $("dl-main");
const original = main.datetime;
let restore = 0;
main.addEventListener("mv-due", () => {
log("Submissions are closed: the app can lock the form now.", "warning", "mv-due");
clearTimeout(restore);
restore = setTimeout(() => { if (main.isConnected) main.datetime = original; }, 6000);
});
$("dl-soon").addEventListener("click", () => {
clearTimeout(restore);
main.moment = Date.now() + 15_000;
log("The deadline is now 15 seconds away: the countdown ticks every second in the last hour.", "info", "Countdown");
});
</script>
</div>API
Attributes
| Name | 유형 | Default | Description |
|---|---|---|---|
datetime | ISO 8601 string | The moment. With an offset (Z, +02:00) it is absolute; without one (2026-10-02T17:00) it is read as a wall time in zone, which is what people usually mean (“5 PM Pacific”), DST included. A date alone means midnight. A wall time that does not exist (spring forward) moves forward by the gap, one that happens twice (fall back) takes the first occurrence; both are explained in a note. | |
zone | IANA zone | (the viewer's zone) | Home zone of the moment: the organizer's, the store's, the server's (America/Los_Angeles, UTC…). Unknown zones fall back to the viewer's zone with a console warning. |
zone-label | string | (city of the zone) | Name of the home zone in the rows and notes (“HQ”, “San Francisco”). The sentence uses the short generic abbreviation when the locale has one (PT, ET, UTC), otherwise “<label> time”. |
zones | string | Extra team zones for the card, space- or comma-separated, each optionally followed by =Label: "Europe/Paris Asia/Kolkata=Bengaluru" (use commas when labels contain spaces). Duplicates of the viewer or home zone are skipped. The zones property also accepts an array of strings or { zone, label, workHours }. | |
viewer-zone | IANA zone | (device zone) | Overrides the viewer's zone (a profile setting, a travel preview). Without it the device zone is used and re-checked when the tab regains focus or visibility (mv-zone-change with source "system"). |
variant | inline | card | inline | inline: the sentence and its chips, to sit inside a paragraph. card: label, countdown, the two-line sentence, the zone strips, notes and the footer. |
locale | BCP 47 | en-US | Language and formats of dates, times, units and lists. |
clock | 12 | 24 | (from locale) | Forces a 12-hour or 24-hour clock. |
label | string | What the moment is (“Submissions close”): card heading (and accessible name of the card group), leading words of the inline sentence, and the calendar title unless summary is set. | |
countdown | boolean | Shows a live relative time (“in 2 days 4 hr”, “12 min 5 sec”, “3 hr ago”). Updates every second only in the last hour, otherwise once a minute, and pauses off screen or in a hidden tab. | |
countdown-text / ended-text | string | in {time} / {time} ago | Countdown templates before and after the moment: "Closes in {time}", "Closed {time} ago". |
soon | time ("24h", "90m", "2d") | 24h | Under this remaining time data-state becomes "soon" and the countdown turns amber. |
work-hours / sleep-hours | "H[:MM]-H[:MM]" | 9-18 / 22-7 | Local working and sleeping hours used for every zone's status and strip shading (sleep may wrap midnight). Saturdays and Sundays count as weekend. |
span | number (hours, 12-72) | 24 | Width of the strips, centered on the moment. |
dst-window | number (days) | 7 | A clock change in any shown zone within this many days of the moment adds a note (0 disables it). The “time difference differs from today” and wall-time notes are always on. |
actions | "google ics copy" (any subset) | Card footer buttons: a prefilled Google Calendar link, an .ics download, and Copy (the plain sentence). | |
picker | boolean | Card footer select to show the times in another zone (every IANA zone, grouped by region, with its offset; first option = automatic). Emits a cancelable mv-zone-change, then sets viewer-zone. | |
summary / details / location | string | Calendar event title (default: label), description (the home time is appended) and location. | |
duration | time | 0 | Event length for the calendar helpers ("45m", "1h30m"); 0 for a deadline. |
reminder | time | Adds a VALARM this long before the moment to the .ics file ("2h"). | |
data-state | upcoming | soon | past | invalid | Set by the component (styleable). | |
data-shift | "-1" | "0" | "+1" … | Set by the component: calendar days between the viewer's date and the home date. |
Properties
| Name | 유형 | Description |
|---|---|---|
moment | Date | null | The absolute moment. Setting a Date or epoch ms writes an ISO UTC datetime. |
zones | string | Array<string | { zone, label?, workHours? }> | Team zones (see the attribute). |
dayShift | number | null | Viewer date minus home date, in days (read-only). |
text | string | The full sentence, exactly as announced and copied (read-only). |
notes | string[] | Daylight-saving and wall-time warnings currently computed, also for the inline variant which does not display them (read-only). |
snapshot | Array<{ zone, label, roles, local, iso, offset, status, shift }> | Every shown zone at the moment: local time text, ISO with offset, UTC offset in minutes, status (working | early | evening | asleep | weekend), day shift vs home (read-only). |
currentViewerZone | string | Effective viewer zone (read-only). |
strings | Partial<Record<string, string>> | Overrides for every visible text (keys: that, yourTime, alsoYourTime, sameDay, dayOne, dayOther, future, past, now, you, home, working, early, evening, asleep, weekend, vsHome, zonesTitle, notesTitle, gapChange, relAhead, relBehind, relSame, todayAhead, todayBehind, todaySame, forward, back, forwardMany, backMany, transition, before, after, lessThanDay, wallGap, wallTwice, pickerLabel, auto, google, ics, copy, copied, copyFailed, announceCopied, announceCopyFailed, homeTime, invalid, event). English defaults. |
Methods
| Name | Description |
|---|---|
toICS() | Returns an RFC 5545 calendar file (UTC times, stable UID, folded lines, optional VALARM). |
googleCalendarUrl() | Returns a prefilled Google Calendar event link. |
copy(format?) | Copies "text" (default, the sentence), "iso", "ics" or "google". Emits a cancelable mv-copy first. Resolves true on success. |
refresh() | Recomputes and re-renders now (e.g. after changing strings in place). |
Events
| Name | Description |
|---|---|
mv-zone-change | The viewer's zone changed. detail: { zone, previous, source: "user" | "system", automatic }. Cancelable when it comes from the picker (the select reverts); source "system" means the device zone changed (travel, OS setting) and viewer-zone is not set. |
mv-due | The moment was reached while the component was live. detail: { moment, late } (late: detected after the tab came back). |
mv-calendar | Cancelable, before the Google Calendar link opens or the .ics downloads. detail: { kind: "google" | "ics", url, ics }. preventDefault() to run your own flow. |
mv-copy | Cancelable, before copying. detail: { format, text }. |
Content structure
| Name | Description |
|---|---|
(content) | Optional fallback text (for no-JS or server rendering), replaced once the element is defined. |
CSS classes
| Name | Description |
|---|---|
mv-dateline-sentence | The sentence: .mv-dateline-lead (inline label), <time>.mv-dateline-home, .mv-dateline-join (.mv-dateline-dash + “that’s”), <time>.mv-dateline-local, .mv-dateline-you, .mv-dateline-shift[data-shift=ahead|behind|same]; .mv-dateline-paren holds visually hidden punctuation. |
mv-dateline-head / -label / -countdown | Card heading and the role="timer" countdown chip. |
mv-dateline-zones / -list / -zone | Zone rows (li[data-role=you|home|team][data-status]): -zone-info (-zone-name, -city, -tag, -zone-time, -status, -dot, -rel) and the aria-hidden -track (-bar with -seg[data-band=work|off|night|weekend], -midnight, -now, -mark; -ticks with -tick[data-day]). |
mv-dateline-notes | role="note" box with the daylight-saving warnings (-notes-title, -notes-list). |
mv-dateline-foot / -picker / -select / -actions / -action | Footer with the zone picker and the calendar buttons (data-action=google|ics|copy, data-copied after a copy). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-dateline-shift | var(--mv-warning) | Tint of the day-shift chips and of the shifted local time. |
--mv-dateline-marker | var(--mv-accent) | The moment line and dot across the strips. |
--mv-dateline-work | accent 30% on surface | Working-hours shading. |
--mv-dateline-off | var(--mv-bg-emphasis) | Off-hours shading (weekends add a fine hatch). |
--mv-dateline-night | darker muted tone | Sleeping-hours shading. |
--mv-dateline-info-width | 12.5rem | Width of the zone info column beside the strips (stacked under 34rem of card width). |
Accessibility
The sentence is the accessible text: real <time datetime> elements with the home zone's ISO offset and the viewer's, and the day shift spelled out as “(+1 day)” (parentheses visually hidden behind the chip, “−” read as minus), so screen readers get the same statement sighted users see and Copy puts on the clipboard. In the card, the strips are aria-hidden with pointer-events untouched; their text equivalent is the labelled list beside them (“At that moment”), one item per zone with its name, You / Home tag, local time, day shift and status (“Asleep · +9h vs home”). The card is a group named by its label. The countdown is a role="timer" element, whose implicit aria-live is off: it is read on demand and never announces each second; the component never announces time passing. Daylight-saving warnings are a role="note" block with a heading. The picker is a native select with a visible label, the actions are a real link (Google Calendar, new tab), a download link and a button; a copy is confirmed by a polite live region and a label change, and failures say so. Status is never color-only (dot plus words), shifts are text, and the chip, tags and strips keep borders or system colors in forced-colors mode. No animation is used, so reduced motion needs nothing. Everything visible is overridable through strings for other languages.