익스클루시브

Relative Time <mv-relative-time>

스스로 최신 상태를 유지하는 “3 minutes ago”: Intl.RelativeTimeFormat 텍스트(“now”, “in 2 days”, 24시간이 아닌 달력 날짜 기준의 “yesterday”)를 담은 진짜 <time datetime>이며, 임곗값을 넘으면 절대 날짜로 바뀌고, 가장 가까운 lang의 로캘을 사용합니다.

작동 방식

스스로 최신 상태를 유지하는 “3 minutes ago”: Intl.RelativeTimeFormat 텍스트(“now”, “in 2 days”, 24시간이 아닌 달력 날짜 기준의 “yesterday”)를 담은 진짜 <time datetime>이며, 임곗값을 넘으면 절대 날짜로 바뀌고, 가장 가까운 lang의 로캘을 사용합니다. 다른 구현이 놓치는 점: 페이지의 모든 인스턴스가 타이머 단 하나를 공유하며, 이 타이머는 보이는 텍스트가 바뀔 시점(다음 분, 시, 자정이며 매초가 아님)에만 정확히 깨어나고 탭이 숨겨진 동안에는 잠듭니다. 시간대가 포함된 정확한 날짜는 호버와 키보드 포커스 시 표시되고 스크린 리더 설명으로 쓰이며, 갱신은 조용히 이루어집니다. 또한 순수 함수 format() export가 서버에서 같은 텍스트를 렌더링하므로 JS 없이도 올바르게 읽힙니다.

카테고리데이터 표시
유형Web Component (<mv-relative-time>)
상태안정
Keywordsexclusive, light, time, date, relative-time, time-ago, intl, i18n, timestamp, ssr

When to use

  • A feed, comment thread or notification list shows when each item happened as “5 minutes ago”
  • A table of deploys, orders or tickets needs compact ages that stay current without reloading
  • Readers must be able to check the exact date and time zone behind an approximate “2 days ago”
  • Server-rendered pages should print the relative time in HTML and keep it fresh once scripts load

Avoid when

  • A deadline or event spans time zones and the day shift must be spelled out → use Dateline instead
  • The value counts down to a launch or promo with ticking seconds
  • The exact timestamp matters more than its age, as in audit logs or invoices

설치

node scripts/add.mjs relative-time --out ./src/marvelous

Marvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["relative-time"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/position.js, components/relative-time/relative-time.js, components/relative-time/relative-time.css.

사용법

빠른 시작, 동작하는 가장 작은 마크업:

<mv-relative-time datetime="2026-09-24T10:00:00Z"></mv-relative-time>

기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:

<div id="mv-relative-time-demo" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,20rem),1fr));gap:1.25rem;width:min(100%,56rem);margin-inline:auto;align-items:start">
  <section aria-labelledby="mv-rt-demo-feed" style="border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface);overflow:hidden">
    <h3 id="mv-rt-demo-feed" style="margin:0;padding:.75rem 1rem;border-bottom:1px solid var(--mv-border);font-size:.875rem;font-weight:600">Notifications</h3>
    <ul style="list-style:none;margin:0;padding:.25rem 0;font-size:.875rem;line-height:1.4">
      <li style="display:flex;gap:.75rem;padding:.6rem 1rem"><span class="mv-avatar" data-size="sm"><span class="mv-avatar-fallback" aria-hidden="true">AT</span></span><span style="flex:1"><b>Aiko Tanaka</b> commented on <b>Q4 launch checklist</b><br><mv-relative-time data-ago="48" style="color:var(--mv-fg-muted);font-size:.8125rem"></mv-relative-time></span></li>
      <li style="display:flex;gap:.75rem;padding:.6rem 1rem"><span class="mv-avatar" data-size="sm"><span class="mv-avatar-fallback" aria-hidden="true">MR</span></span><span style="flex:1"><b>Mateo Rossi</b> approved invoice INV-0042 for $2,450.00<br><mv-relative-time data-ago="200" style="color:var(--mv-fg-muted);font-size:.8125rem"></mv-relative-time></span></li>
      <li style="display:flex;gap:.75rem;padding:.6rem 1rem"><span class="mv-avatar" data-size="sm"><span class="mv-avatar-fallback" aria-hidden="true">PN</span></span><span style="flex:1"><b>Priya Nair</b> mentioned you in <b>#design</b><br><mv-relative-time data-ago="7900" style="color:var(--mv-fg-muted);font-size:.8125rem"></mv-relative-time></span></li>
      <li style="display:flex;gap:.75rem;padding:.6rem 1rem"><span class="mv-avatar" data-size="sm"><span class="mv-avatar-fallback" aria-hidden="true">LM</span></span><span style="flex:1"><b>Lena Moore</b> shared <b>Brand refresh</b> with you<br><mv-relative-time data-ago="100800" style="color:var(--mv-fg-muted);font-size:.8125rem"></mv-relative-time></span></li>
      <li style="display:flex;gap:.75rem;padding:.6rem 1rem"><span class="mv-avatar" data-size="sm"><span class="mv-avatar-fallback" aria-hidden="true">KB</span></span><span style="flex:1"><b>Kwame Boateng</b> joined the workspace<br><mv-relative-time data-ago="1036800" absolute-after="7d" style="color:var(--mv-fg-muted);font-size:.8125rem"></mv-relative-time></span></li>
    </ul>
  </section>

  <div style="display:grid;gap:1.25rem">
    <div class="mv-table-wrap">
      <table class="mv-table">
        <caption>Tab to a time to read the exact date and time zone.</caption>
        <thead><tr><th>Environment</th><th>Commit</th><th data-align="end">Deployed</th></tr></thead>
        <tbody>
          <tr><td>Production</td><td><code>a3f9c1e</code></td><td data-align="end"><mv-relative-time tooltip format="short" data-ago="330"></mv-relative-time></td></tr>
          <tr><td>Staging</td><td><code>7d21b04</code></td><td data-align="end"><mv-relative-time tooltip format="short" data-ago="11400"></mv-relative-time></td></tr>
          <tr><td>Scheduled</td><td><code>e58a2f7</code></td><td data-align="end"><mv-relative-time tooltip format="short" data-ago="-180000"></mv-relative-time></td></tr>
        </tbody>
      </table>
    </div>

    <dl style="display:grid;grid-template-columns:auto 1fr;gap:.45rem 1rem;margin:0;padding:.85rem 1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);font-size:.875rem">
      <dt style="color:var(--mv-fg-muted)">long</dt><dd style="margin:0"><mv-relative-time tooltip data-ago="1260"></mv-relative-time></dd>
      <dt style="color:var(--mv-fg-muted)">short</dt><dd style="margin:0"><mv-relative-time tooltip format="short" data-ago="1260"></mv-relative-time></dd>
      <dt style="color:var(--mv-fg-muted)">narrow</dt><dd style="margin:0"><mv-relative-time tooltip format="narrow" data-ago="1260"></mv-relative-time></dd>
      <dt style="color:var(--mv-fg-muted)">lang="fr"</dt><dd style="margin:0" lang="fr"><mv-relative-time tooltip data-ago="172800"></mv-relative-time></dd>
      <dt style="color:var(--mv-fg-muted)">lang="ja"</dt><dd style="margin:0" lang="ja"><mv-relative-time tooltip data-ago="-5400"></mv-relative-time></dd>
      <dt style="color:var(--mv-fg-muted)">Asia/Tokyo</dt><dd style="margin:0"><mv-relative-time tooltip time-zone="Asia/Tokyo" absolute-after="1d" data-ago="259200"></mv-relative-time></dd>
    </dl>
  </div>
</div>
<script type="module">
  // Demo only: anchor every timestamp to "now" so the demo reads the same any day.
  const now = Date.now();
  for (const el of document.querySelectorAll("#mv-relative-time-demo [data-ago]")) el.date = now - Number(el.dataset.ago) * 1000;
</script>

API

Attributes

Name유형DefaultDescription
datetimestringThe moment: an ISO 8601 date or date-time (with an offset or Z), or epoch milliseconds. Without it, the datetime of a child <time> is used (server-rendered markup).
formatlong | short | narrowlongIntl style: “3 minutes ago”, “3 min. ago”, “3m ago”. Absolute dates use a medium date (long, short) or a numeric date (narrow).
absolute-afterspan30dBeyond this distance from now (past or future), show the absolute date instead: a number with s, m, h, d, w, mo or y (“7d”, “12h”), or “never”.
time-zoneIANA zoneTime zone for calendar days, the absolute date and the tooltip (default: the viewer’s).
tooltipbooleanMakes the time a tab stop so keyboard and touch users can open the exact date. Hover works without it; leave it off in long lists to avoid a tab stop per row.
lang (inherited)BCP 47Locale comes from the nearest [lang] ancestor, else the browser’s; changing any lang re-localizes every instance.
data-moderelative | absoluteSet by the component (for styling).
data-invalidbooleanSet by the component when the date cannot be parsed (the existing text is kept).

Properties

Name유형Description
dateDateThe moment as a Date; accepts a Date, an ISO string or epoch ms (writes datetime). Can be set before the element is defined.
localestring | undefinedResolved locale (read-only).

Methods

NameDescription
refresh()Re-render now (rarely needed: the shared scheduler does it).
format(date, now?, { locale, format, absoluteAfter, timeZone }?)Named export (and static MvRelativeTime.format): pure, server-safe, returns the same text the element shows.
formatExact(date, { locale, timeZone }?)Named export: pure, server-safe exact date, time and zone (“Thursday, September 24, 2026 at 10:00 AM GMT+2”), for a server-side title.

Content structure

NameDescription
time (optional)A server-rendered <time datetime> child is adopted and kept (its title, the no-JS fallback, is removed once the tooltip takes over).

CSS classes

NameDescription
mv-relative-time-textThe <time> element.
mv-relative-time-tip / -zoneThe exact date bubble (top layer popover) and its time zone chip.

CSS variables

NameDefaultDescription
--mv-relative-time-tip-bglight-dark(var(--mv-gray-950), var(--mv-gray-50))Bubble background.
--mv-relative-time-tip-fglight-dark(var(--mv-gray-50), var(--mv-gray-950))Bubble text.
--mv-relative-time-hintvar(--mv-fg-subtle)Dotted underline shown on hover (and always with tooltip).

Accessibility

Renders a real <time datetime> in the light DOM, so the text is plain inline content that screen readers read once, in place. There is no live region: minute-by-minute updates change the text silently, and the DOM is only written when the text actually changes. The exact date, time and time zone (“Thursday, September 24, 2026 at 10:00 AM GMT+2”) is the <time>’s accessible description (aria-describedby to the bubble, which is itself aria-hidden so it is never read twice), announced on focus rather than in the reading flow. With tooltip, the time is a tab stop: focus opens the bubble, Escape closes it, blur closes it, and a tap opens it on touch screens. With a mouse (hover: hover and pointer: fine), the bubble opens after a short delay, stays open while the pointer is over it (the text can be selected) and closes on scroll. Visible focus ring; forced colors give the bubble and chip a CanvasText border; opacity-only entry that the reduced-motion duration tokens make instant. Every string comes from Intl (RelativeTimeFormat, DateTimeFormat) in the inherited lang, so there is nothing to translate by hand and no sentence is assembled from fragments. Server-side, format() and formatExact() produce the same text and a title so the page reads correctly without JS. Known limits: months and years are counted from calendar days (a month is about 30.4 days), so “1 month ago” can differ by a day from a calendar-month reading; the locale is only the nearest lang attribute (there is no locale property), and the words come from the browser's Intl data, so wording can differ slightly between browsers.

이 페이지는 AI로 번역되었습니다. 번역 문제 신고