익스클루시브
Countdown <mv-countdown>
출시, 세일, 웨비나, 이벤트를 위한 날짜 카운트다운: 일, 시, 분, 초(설정 가능)를 고정폭 숫자와 Intl에서 가져온 복수형이 올바른 라벨로 표시하며, plain, card, compact 변형을 제공합니다.
작동 방식
출시, 세일, 웨비나, 이벤트를 위한 날짜 카운트다운: 일, 시, 분, 초(설정 가능)를 고정폭 숫자와 Intl에서 가져온 복수형이 올바른 라벨로 표시하며, plain, card, compact 변형을 제공합니다. 다른 구현이 틀리는 디테일: 매 틱을 벽시계 시간에서 다시 계산해 다음 실제 초 경계에 예약하고(드리프트도, 누적되는 interval도 없음), 탭이 숨겨지거나 타이머가 화면 밖에 있으면 렌더링을 멈췄다가 돌아오면 다시 동기화하며, 호버나 포커스 시 방문자의 현지 시간으로 목표 시각을 보여 주고(“Wed, Oct 7, 9:01 AM your time”), 음수는 절대 표시하지 않습니다. 0이 되면 슬롯에 넣은 “종료” 콘텐츠로 바뀌고, 종료 후에 페이지를 열어도 mv-end를 정확히 한 번 발생시키며, 스크린 리더에는 매초가 아니라 1분에 한 번 이름이 갱신되는 차분한 role=timer를 제공합니다.
| 카테고리 | 데이터 표시 |
|---|---|
| 유형 | Web Component (<mv-countdown>) |
| 상태 | 안정 |
| Keywords | exclusive, light, countdown, timer, launch, sale, event, webinar, time-zone, intl |
When to use
- A launch, product drop or event page needs a live countdown to a fixed date and time
- A sale or promo banner should show a compact time left that turns into an ended message on its own
- A webinar or live stream page should swap the timer for a Join button when it starts
- Visitors in other time zones need to see when the moment falls in their own local time
Avoid when
- The moment should read as relative text like “in 2 days” or “3 minutes ago” rather than a ticking timer → use Relative Time instead
- A deadline must be read as a date across two time zones with the day shift spelled out → use Dateline instead
- A prompt waits a few seconds and then takes a default choice if nobody answers → use Timed Choice instead
설치
node scripts/add.mjs countdown --out ./src/marvelousMarvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["countdown"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/observe.js, components/countdown/countdown.js, components/countdown/countdown.css.
사용법
빠른 시작, 동작하는 가장 작은 마크업:
<mv-countdown to="2026-10-07T09:01:00+02:00"></mv-countdown>기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:
<div id="mv-countdown-demo" style="display:grid;gap:1rem;width:100%;max-width:560px">
<div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:.4rem .75rem;padding:.6rem 1rem;border-radius:var(--mv-radius-lg);background:var(--mv-accent-subtle);color:var(--mv-fg);font-size:.875rem">
<strong>Summer sale: 30% off annual plans</strong>
<span style="color:var(--mv-fg-muted)">Ends in <mv-countdown data-variant="compact" data-offset="187200" to="2026-10-01T18:00:00-04:00" label="Summer sale">Oct 1, 6:00 PM EDT<template slot="ended">The sale has ended</template></mv-countdown></span>
</div>
<section style="display:grid;justify-items:center;gap:1rem;padding:1.75rem 1rem 1.5rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);background:var(--mv-surface);box-shadow:var(--mv-shadow-md);text-align:center">
<div style="display:grid;gap:.3rem">
<span style="font-size:.75rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--mv-accent-fg)">Quillon 2.0</span>
<strong style="font-size:1.25rem">Doors open in</strong>
</div>
<mv-countdown data-variant="card" data-offset="1123265" to="2026-10-07T09:01:00+02:00" label="Launch">Wednesday, October 7, 9:01 AM CEST</mv-countdown>
<span style="font-size:.8rem;color:var(--mv-fg-subtle)">Hover or focus the timer to see the time in your time zone.</span>
</section>
<div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.75rem;padding:.9rem 1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-bg-subtle)">
<div style="display:grid;gap:.15rem">
<strong style="font-size:.9rem">Webinar: Designing for 12 languages</strong>
<span style="font-size:.8rem;color:var(--mv-fg-muted)">with Aiko Tanaka and Mateo Ruiz</span>
</div>
<mv-countdown id="mv-countdown-demo-live" units="m s" data-offset="9" to="2026-10-01T16:00:00Z" label="Webinar" style="--mv-countdown-size:1.5em">
<template slot="ended">
<span style="width:.5rem;height:.5rem;border-radius:50%;background:var(--mv-danger);box-shadow:0 0 0 3px color-mix(in oklch,var(--mv-danger) 25%,transparent)"></span>
<span style="font-size:.8rem;font-weight:600">Live now</span>
<a class="mv-button" data-size="sm" href="#">Join webinar</a>
</template>
</mv-countdown>
</div>
<div style="display:flex;justify-content:center">
<button class="mv-button" data-variant="ghost" data-size="sm" id="mv-countdown-demo-replay">Replay webinar start</button>
</div>
</div>
<script type="module">
// Demo only: aim each timer a fixed number of seconds ahead so it always runs.
const root = document.getElementById("mv-countdown-demo");
const aim = (el) => { el.to = new Date(Date.now() + Number(el.dataset.offset) * 1000).toISOString(); };
root.querySelectorAll("mv-countdown[data-offset]").forEach(aim);
const live = document.getElementById("mv-countdown-demo-live");
document.getElementById("mv-countdown-demo-replay").addEventListener("click", () => aim(live));
</script>API
Attributes
| Name | 유형 | Default | Description |
|---|---|---|---|
to | ISO 8601 date-time | Target moment, ideally with an offset or Z (2026-10-07T09:01:00+02:00) so every visitor counts down to the same instant. Missing or invalid: dashes and data-state="idle". | |
units | string | d h m s | Units to show, largest first, separated by spaces or commas (d, h, m, s, or days, hours…). The largest shown unit absorbs the rest (units="h m" → 49 hours 05 minutes); without seconds the smallest unit rounds up so zero only appears at the end. Leading zero units drop out while more than two remain. |
label | string | What is being counted down to, prefixed to the accessible name (“Launch: 12 days, 4 hours, 5 minutes remaining”). | |
locale | BCP 47 tag | nearest lang, else en-US | Locale for digits, unit labels, the local-time tip and the accessible name. |
data-variant | card | compact | (plain) | Plain: large digits with labels and colons. Card: one tile per unit. Compact: one line of text (2d 04h 05m 06s) for bars, buttons and sentences. |
data-state | idle | running | ended | Set by the component (for styling). | |
data-swapped | boolean | Set by the component while the ended content replaces the digits. |
Properties
| Name | 유형 | Description |
|---|---|---|
to / units / label / locale | string | Mirror the attributes. |
strings | object | Override default texts: remaining (“{time} remaining”), soon (“Less than a minute remaining”), ended (“Ended”), labelled (“{label}: {status}”), local (“{date} your time”). |
remaining | number | Milliseconds left, 0 once ended, NaN without a valid target (read-only). |
ended | boolean | True once the target has passed (read-only). |
Events
| Name | Description |
|---|---|
mv-end | Fired once per target when it reaches zero, or one task after the upgrade when the page opens after the end; detail = { to: Date, late: boolean }. Cancelable: preventDefault() keeps the zeroed timer instead of swapping in the ended content. |
Content structure
| Name | Description |
|---|---|
(text) | Fallback shown before JavaScript runs (SSR, no JS), e.g. the date written out; replaced by the live display on upgrade. |
ended | <template slot="ended"> or any [slot=ended] child: shown instead of the digits at zero (a message, a Join or Shop button). |
CSS classes
| Name | Description |
|---|---|
mv-countdown-units / -unit / -value / -label | Generated digits (aria-hidden; data-unit = d | h | m | s). |
mv-countdown-tip | Local-time tooltip (role=tooltip, linked with aria-describedby). |
mv-countdown-ended | Added to the ended content. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-countdown-size | 2.25em / 2em card / 1em compact | Digit size. |
--mv-countdown-gap | 0.8 × size / var(--mv-space-2) card / 0.4em compact | Space between units. |
--mv-countdown-color | var(--mv-fg) | Digit color. |
--mv-countdown-separator | var(--mv-fg-subtle) | Colon color (plain variant). |
Accessibility
The host is a role=timer with aria-live=off, so nothing is announced by itself; the visible digits are aria-hidden and the accessible name is a full sentence built with Intl.NumberFormat units and Intl.ListFormat, rewritten at most once a minute (“Launch: 2 days, 4 hours, 5 minutes remaining”, then “Less than a minute remaining”, then “Ended”). The timer is focusable (tabindex=0 unless you set one) and describes itself with the local-time tooltip (role=tooltip, aria-describedby), shown on hover, keyboard focus or tap focus, hoverable and dismissed with Escape (WCAG 1.4.13). When ended content is swapped in, the timer role, name and tab stop are removed so your message or button reads as normal content. All texts come from strings and Intl in the chosen locale (plural-correct units, local digits). Tabular digits keep the width steady; no continuous motion, the ended content fades in (instant under reduced motion or data-motion=reduce). Forced colors: tooltip and tiles get system borders, focus shows a system outline. SSR-safe (no window access at import); the fallback text is readable without JavaScript. If the countdown ends while it has focus, focus stays on it and its tab stop is removed on blur, so focus never drops to <body>. Known limits: it trusts the visitor's clock (a wrong system clock shows a wrong time left; pass a server-corrected to if that matters); the largest unit is days (no months or years); the offscreen and hidden-tab pauses mean the digits jump to the right value on return rather than animating.