익스클루시브

Timed Choice <mv-timed-choice>

눈에 보이게 줄어드는 타이머와 함께 선택지를 제안하고, 아무도 응답하지 않으면 안전한 기본값을 적용하는 중요하지 않은 프롬프트입니다. “Next episode in 8s · Cancel / Play now”, “Keep these display settings?

문화적 레퍼런스

블랙 미러: 밴더스내치, 찰리 브루커(Netflix)(2018, 영화). 이 인터랙티브 영화에서는 각 결정이 줄어드는 타이머 막대와 함께 나타나고, 시청자가 제때 고르지 않으면 이야기는 기본 경로를 따라 계속 진행됩니다. UI에서는 중요하지 않은 프롬프트가 줄어드는 타이머와 함께 선택지를 제안하고 안전한 기본값 쪽으로 채워지며, 누군가 읽고 있을 수 있는 동안에는 멈추고, 파괴적인 동작을 기본값으로 택하는 일은 결코 없습니다.

작동 방식

중요하지 않은 프롬프트가 눈에 보이게 줄어드는 타이머와 함께 선택지를 제안하고, 아무도 답하지 않으면 안전한 기본값을 택합니다: “Next episode in 8s · Cancel / Play now”, “Keep these display settings? Reverting in 15s”, “Restore your previous session? Starting fresh in 20s”. 옵션은 직접 만든 버튼이며, data-default가 지정된 버튼은 시간이 줄어들수록 가장자리부터 채워지고 작은 카운트다운 칩을 답니다. 그동안 얇은 트랙이 프롬프트를 따라 줄어들고, 문구 안의 모든 [data-remaining]이 카운트다운됩니다. 시간은 누군가 실제로 프롬프트를 읽을 수 있을 때만 흐릅니다: 호버할 때, 키보드나 스크린 리더 포커스가 내부에 있을 때(항상), 탭이 숨겨졌을 때, 프롬프트의 절반 미만이 화면에 보일 때 멈추며, 카운트다운은 performance.now() 구간을 누적해 계산하므로 스로틀링된 타이머 때문에 어긋나지 않습니다. WCAG 2.2.1이 기본 내장되어 있습니다: “+10s” 연장 버튼, 보조 배율(요소별 또는 <html data-timing-assist>를 통한 페이지 전체, 더 강한 쪽이 적용됨), 그리고 타이머를 완전히 멈추는 Escape. 파괴적인 옵션(data-destructive 또는 data-variant="destructive")은 기본값으로 허용되지 않습니다: 타이머가 꺼지고 사용자가 직접 선택해야 합니다. 세 가지 변형: 인라인 바, 플로팅 카드(뷰포트 모서리 또는 플레이어 내부), 그리고 기본적으로 비모달인 대화 상자. 모든 선택은 취소 가능한 mv-choice { value, reason: "user" | "timeout" | "api" }를 거치며, 시간 초과를 거부하면 사용자 몰래 선택하는 대신 타이머가 멈춥니다.

카테고리오버레이
유형Web Component (<mv-timed-choice>)
상태안정
함께 설치되는 항목button
Keywordsexclusive, culture, timer, countdown, default-choice, timeout, autoplay, up-next, next-episode, revert, confirm, prompt, auto-advance, wcag-2-2-1, timing-adjustable, pause-on-hover, non-modal, alertdialog

When to use

  • A video or playlist should continue to the next item unless the viewer cancels within a few seconds
  • A risky settings change must revert on its own unless the user confirms it can still see and use the screen
  • A prompt such as restoring the previous session or applying recommended settings should not block someone who ignores it
  • An automatic step must stay adjustable and extendable for users who need more time (WCAG 2.2.1)

Avoid when

  • The timer protects a server session that must be deliberately extended before sign-out → use Hatch instead
  • The decision is critical or destructive and must never be taken automatically → use Dialog instead
  • There is nothing to choose, only transient feedback that fades on its own → use Toast instead

설치

node scripts/add.mjs timed-choice --out ./src/marvelous

Marvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["timed-choice"], 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/focus.js, core/motion.js, components/timed-choice/timed-choice.js, components/timed-choice/timed-choice.css.

사용법

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

<div id="tc-demo" style="width:min(100%,60rem);margin-inline:auto">
  <style>
    #tc-demo { display:grid; gap:1rem; align-content:start }
    #tc-demo .tc-toolbar { 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-lg); background:var(--mv-bg-subtle) }
    #tc-demo .tc-group { display:flex; align-items:center; gap:.625rem; flex-wrap:wrap }
    #tc-demo .tc-k { color:var(--mv-fg-muted); font-size:.75rem; font-weight:500 }
    #tc-demo .tc-assist .mv-button[aria-pressed="true"] { --_bg:var(--mv-fg); --_bg-hover:var(--mv-fg); --_fg:var(--mv-bg); --_border:transparent }
    #tc-demo .mv-choice { font-size:.8125rem }
    #tc-demo .tc-log { display:flex; align-items:center; gap:.5rem; min-height:1.5rem; margin:0; color:var(--mv-fg-muted); font-size:.75rem }
    #tc-demo .tc-log b { color:var(--mv-fg); font-weight:500 }

    /* Scene 1: display settings */
    #tc-demo .tc-settings { display:grid; gap:.875rem; padding:1rem 1.125rem 1.125rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-sm) }
    #tc-demo .tc-settings h3 { margin:0; font-size:.9375rem; letter-spacing:-.01em }
    #tc-demo .tc-rows { display:grid; border:1px solid var(--mv-border); border-radius:var(--mv-radius-lg); overflow:hidden }
    #tc-demo .tc-row { display:flex; align-items:center; justify-content:space-between; gap:1rem; padding:.625rem .875rem; font-size:.8125rem }
    #tc-demo .tc-row + .tc-row { border-top:1px solid var(--mv-border) }
    #tc-demo .tc-row span:first-child { color:var(--mv-fg-muted) }
    #tc-demo .tc-row b { font-weight:500; font-variant-numeric:tabular-nums }

    /* Scene 2 + 3 */
    #tc-demo .tc-pair { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1rem }
    #tc-demo .tc-player { position:relative; min-height:23rem; border-radius:var(--mv-radius-xl); overflow:hidden; background:
      radial-gradient(120% 90% at 20% 15%, oklch(0.42 0.09 230) 0%, transparent 55%),
      radial-gradient(90% 80% at 85% 90%, oklch(0.36 0.08 300) 0%, transparent 60%),
      oklch(0.17 0.02 260); color:oklch(0.97 0 0); box-shadow:var(--mv-shadow-md) }
    #tc-demo .tc-credits { position:absolute; inset:1.25rem 1.25rem auto; display:grid; gap:.25rem; font-size:.75rem; color:oklch(0.97 0 0 / .7) }
    #tc-demo .tc-credits b { color:oklch(0.97 0 0); font-size:.9375rem; font-weight:600; letter-spacing:-.01em }
    #tc-demo .tc-status { position:absolute; inset:auto 1.25rem 1.25rem; font-size:.8125rem; color:oklch(0.97 0 0 / .85) }
    #tc-demo .tc-thumb { display:grid; place-items:end start; padding:.3125rem .375rem; background:linear-gradient(135deg, oklch(0.55 0.1 210), oklch(0.32 0.08 270)) !important; color:oklch(0.98 0 0); font-size:.625rem; font-weight:600; letter-spacing:.04em }

    #tc-demo .tc-window { position:relative; min-height:23rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg-subtle); overflow:hidden; box-shadow:var(--mv-shadow-sm) }
    #tc-demo .tc-titlebar { display:flex; align-items:center; gap:.375rem; height:2.25rem; padding:0 .875rem; border-bottom:1px solid var(--mv-border); background:var(--mv-surface); font-size:.75rem; color:var(--mv-fg-muted) }
    #tc-demo .tc-titlebar i { width:.625rem; height:.625rem; border-radius:50%; background:var(--mv-bg-emphasis) }
    #tc-demo .tc-titlebar span { margin-inline-start:.5rem }
    #tc-demo .tc-lines { display:grid; gap:.625rem; padding:1.25rem 1.5rem }
    #tc-demo .tc-lines i { height:.5rem; border-radius:4px; background:var(--mv-bg-emphasis); opacity:.7 }
    #tc-demo .tc-tabs { display:grid; gap:.25rem; margin:0; padding:.5rem; list-style:none; border:1px solid var(--mv-border); border-radius:var(--mv-radius-md); background:var(--mv-bg-subtle); font-size:.75rem }
    #tc-demo .tc-tabs li { display:flex; justify-content:space-between; gap:.75rem; padding:.125rem .25rem; color:var(--mv-fg) }
    #tc-demo .tc-tabs li span:last-child { color:var(--mv-fg-subtle) }
    #tc-demo .tc-done { position:absolute; inset:auto 1.5rem 1.25rem; margin:0; font-size:.8125rem; color:var(--mv-fg-muted) }
    @media (max-width:46rem) { #tc-demo .tc-pair { grid-template-columns:minmax(0,1fr) } }
  </style>

  <div class="tc-toolbar">
    <div class="tc-group tc-assist" role="group" aria-label="Timing assist">
      <span class="tc-k" aria-hidden="true">Timing assist</span>
      <button type="button" class="mv-button" data-variant="outline" data-size="sm" aria-pressed="true" data-assist="1">1×</button>
      <button type="button" class="mv-button" data-variant="outline" data-size="sm" aria-pressed="false" data-assist="2">2×</button>
      <button type="button" class="mv-button" data-variant="outline" data-size="sm" aria-pressed="false" data-assist="4">4×</button>
    </div>
    <div class="tc-group">
      <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="tc-hover" checked> Pause on hover</label>
      <button type="button" class="mv-button" data-variant="outline" data-size="sm" id="tc-replay">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v5h5"/></svg>
        Replay all
      </button>
    </div>
  </div>

  <!-- Inline bar: the safe default is to revert display changes nobody confirmed -->
  <section class="tc-settings" aria-labelledby="tc-settings-title">
    <h3 id="tc-settings-title">Display</h3>
    <div class="tc-rows">
      <div class="tc-row"><span>Resolution</span><b id="tc-res">2560 × 1440 (Recommended)</b></div>
      <div class="tc-row"><span>Refresh rate</span><b>144 Hz</b></div>
    </div>
    <mv-timed-choice id="tc-bar" open duration="15s">
      <span class="mv-timed-choice-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/></svg></span>
      <div class="mv-timed-choice-body">
        <p class="mv-timed-choice-title">Keep these display settings?</p>
        <p class="mv-timed-choice-text">Otherwise it reverts to 1920 × 1080 on its own. If the screen looks wrong, just wait.</p>
      </div>
      <div class="mv-timed-choice-actions">
        <button class="mv-button" data-variant="outline" data-size="sm" value="revert" data-default>Revert</button>
        <button class="mv-button" data-size="sm" value="keep">Keep changes</button>
      </div>
    </mv-timed-choice>
  </section>

  <div class="tc-pair">
    <!-- Floating card, contained in a video player -->
    <div class="tc-player" data-theme="dark" role="region" aria-label="Video player">
      <div class="tc-credits"><span>Harbor Lines · Season 2</span><b>Episode 3 · Low Tide</b><span>End credits</span></div>
      <p class="tc-status" id="tc-player-status" hidden></p>
      <mv-timed-choice id="tc-card" variant="card" contain open duration="10s" style="--mv-timed-choice-width:19rem">
        <span class="mv-timed-choice-media tc-thumb" aria-hidden="true">S2 · E4</span>
        <div class="mv-timed-choice-body">
          <p class="mv-timed-choice-eyebrow">Up next · Season 2</p>
          <p class="mv-timed-choice-title">The Night Ferry</p>
          <p class="mv-timed-choice-text">48 min · Mara takes the last boat out.</p>
        </div>
        <div class="mv-timed-choice-actions">
          <button class="mv-button" data-variant="ghost" data-size="sm" value="cancel">Cancel</button>
          <button class="mv-button" data-size="sm" value="play" data-default>Play now</button>
        </div>
      </mv-timed-choice>
    </div>

    <!-- Non-modal dialog, contained in an app window -->
    <div class="tc-window">
      <div class="tc-titlebar" aria-hidden="true"><i></i><i></i><i></i><span>Atlas Notes</span></div>
      <div class="tc-lines" aria-hidden="true"><i style="width:62%"></i><i style="width:88%"></i><i style="width:74%"></i><i style="width:80%"></i><i style="width:45%"></i></div>
      <p class="tc-done" id="tc-window-status" hidden></p>
      <mv-timed-choice id="tc-dialog" variant="dialog" contain open duration="20s" count="none" style="--mv-timed-choice-width:21rem">
        <p class="mv-timed-choice-title">Restore your previous session?</p>
        <p class="mv-timed-choice-text">6 tabs from Tuesday, Sep 22 at 4:18 PM. <span data-countdown>Starting fresh in <b data-remaining="long"></b>;</span> your tabs stay in History either way.</p>
        <ul class="tc-tabs">
          <li><span>Q4 roadmap</span><span>Edited 4:02 PM</span></li>
          <li><span>Hiring plan · Lisbon</span><span>Edited 3:47 PM</span></li>
          <li><span>+ 4 more</span><span></span></li>
        </ul>
        <div class="mv-timed-choice-actions">
          <button class="mv-button" data-variant="outline" data-size="sm" value="fresh" data-default>Start fresh</button>
          <button class="mv-button" data-size="sm" value="restore">Restore 6 tabs</button>
        </div>
      </mv-timed-choice>
    </div>
  </div>

  <p class="tc-log" id="tc-log" aria-live="polite"><span class="mv-badge" data-variant="secondary" data-shape="pill">mv-choice</span> Waiting: hover or tab into a prompt to pause it.</p>

  <script type="module">
    const root = document.getElementById("tc-demo");
    const prompts = [...root.querySelectorAll("mv-timed-choice")];
    const log = root.querySelector("#tc-log");
    const res = root.querySelector("#tc-res");
    const playerStatus = root.querySelector("#tc-player-status");
    const windowStatus = root.querySelector("#tc-window-status");
    const replayTimers = new Map();

    const say = (name, text) => {
      const badge = log.querySelector(".mv-badge");
      badge.textContent = name;
      log.replaceChildren(badge, " ", text);
    };
    const results = {
      "tc-bar": { revert: "Reverted to 1920 × 1080.", keep: "Kept 2560 × 1440." },
      "tc-card": { play: "Playing S2 · E4, The Night Ferry", cancel: "Autoplay canceled. Browse episodes" },
      "tc-dialog": { fresh: "Started fresh. Previous tabs are in History.", restore: "Restored 6 tabs." },
    };

    const replay = (p, delay) => {
      clearTimeout(replayTimers.get(p));
      replayTimers.set(p, setTimeout(() => {
        playerStatus.hidden = true;
        windowStatus.hidden = true;
        res.textContent = "2560 × 1440 (Recommended)";
        p.show();
      }, delay));
    };

    for (const p of prompts) {
      p.addEventListener("mv-choice", (e) => {
        const { value, reason } = e.detail;
        const text = results[p.id][value];
        say("mv-choice", `${p.id.replace("tc-", "")} → “${value}” (${reason === "timeout" ? "timed out, safe default" : "chosen by the user"}). ${text}`);
        if (p.id === "tc-bar" && value === "revert") res.textContent = "1920 × 1080";
        if (p.id === "tc-card") { playerStatus.textContent = text; playerStatus.hidden = false; }
        if (p.id === "tc-dialog") { windowStatus.textContent = text; windowStatus.hidden = false; }
        replay(p, 4000);
      });
      p.addEventListener("mv-pause", (e) => say("mv-pause", `${p.id.replace("tc-", "")} paused (${e.detail.reasons.join(", ")}), ${Math.ceil(e.detail.remaining / 1000)} s left.`));
      p.addEventListener("mv-extend", (e) => say("mv-extend", `${p.id.replace("tc-", "")}: +${e.detail.added / 1000} s, ${Math.ceil(e.detail.remaining / 1000)} s left.`));
    }

    root.querySelector("#tc-replay").addEventListener("click", () => {
      for (const p of prompts) replay(p, 0);
      say("mv-choice", "Replaying all three prompts.");
    });
    root.querySelector("#tc-hover").addEventListener("change", (e) => {
      for (const p of prompts) p.pauseOn = e.target.checked ? "hover focus hidden offscreen" : "focus hidden offscreen";
    });
    for (const b of root.querySelectorAll("[data-assist]")) {
      b.addEventListener("click", () => {
        for (const x of root.querySelectorAll("[data-assist]")) x.setAttribute("aria-pressed", String(x === b));
        for (const p of prompts) { p.assist = Number(b.dataset.assist); replay(p, 0); }
        say("assist", `Timing assist ${b.dataset.assist}×: every timer is ${b.dataset.assist === "1" ? "at its normal length" : `${b.dataset.assist} times longer`}.`);
      });
    }
  </script>
</div>

API

Attributes

Name유형DefaultDescription
openbooleanfalseShows the prompt and starts a fresh timer. Removed by the component once a choice is made (or with close()). Setting it again reopens with a new timer.
durationtime ("10s", "1m30s", "800ms", or a number of ms)10sTime before the default option is taken, before the assist multiplier and extensions. Minimum 1 s.
assistnumber (1-20)1Timing multiplier for users who need more time: 2 doubles every timer. The page-wide <html data-timing-assist> is also read and the larger of the two wins, so a user preference is never overridden downward. Values below 1 are ignored.
extend-bytime10sTime added by the built-in “+10s” button (label follows the value). 0 hides the button.
pause-onspace- or comma-separated list: hover, focus, hidden, offscreen, blurhover focus hidden offscreenWhat pauses the timer: pointer hovering the prompt (only once it moves, so a pointer merely resting where the prompt appears doesn't count), focus inside, hidden tab, less than half of the prompt visible, window without focus. Keyboard / screen-reader focus inside always pauses, even if focus is left out of the list.
variantbar | card | dialogbarbar: inline strip in the flow, track along the bottom edge. card: floating card (fixed to a viewport corner, or absolute with contain), track along the top. dialog: author content moves into a <dialog> with role="alertdialog", non-modal unless modal is set.
placementbottom-end | bottom-start | top-end | top-start | bottom | topbottom-endCorner of the card variant (viewport, or containing block with contain). Offset: --mv-timed-choice-inset.
containbooleancard and non-modal dialog are positioned absolute inside the nearest positioned ancestor (a video player, an app window) instead of the viewport.
modalbooleandialog variant only: opens with showModal() (backdrop, inert page) and focuses the dialog itself, so reading it doesn't stop the clock but tabbing to an option does. Escape stops the timer instead of dismissing; focus returns where it was on close.
countbutton | nonebuttonbutton shows a small countdown chip at the end of the default option (a pause glyph while paused); none hides it when your copy already shows [data-remaining].
labelstringAccessible name of the prompt when it has no .mv-timed-choice-title / [data-title] / heading.
data-defaultmarker attributeOn an option: the safe choice taken when the time runs out. The first one wins. Refused (console warning, timer off, data-refused on the host) when the option is also destructive.
data-destructivemarker attributeOn an option: can never be the default. data-variant="destructive" (mv-button) counts too.
value / data-optionon buttonsOptions are the button[value] and [data-option] elements inside (all buttons when there are none). The choice value is value, then data-value, then the button's text. Buttons without type get type="button".
data-remaining"" (8s) | "number" (8) | "long" (8 seconds)On any element in your copy: its text is replaced by the remaining time every second.
data-countdownmarker attributeOn a phrase in your copy (“Next episode in 8s”): hidden once the timer is stopped or off, so the text never lies.
data-timer / data-paused-by / data-chosenset by the componentOn the host: running | paused | held | off | idle, and the active pause reasons while paused (e.g. "hover focus"). data-chosen marks the option picked last. data-closing during the exit transition.

Properties

Name유형Description
valuestring | nullValue of the last choice (read-only).
reason"user" | "timeout" | "api" | nullWhy the last choice was made (read-only).
remaining / totalnumber (ms)Time left, and the full length (duration × assist + extensions), recomputed on every read (read-only).
pausedbooleanTrue while an open timer is paused by hover, focus, a hidden tab… (read-only).
state"running" | "paused" | "held" | "off" | "idle"Current timer state, mirrored in data-timer (read-only).
options / defaultOptionElement[] / Element | nullThe options found, and the accepted default (null when missing or refused) (read-only).
stringsPartial<Record<string, string>>Overrides for built-in texts (keys: extend ({time}), extendLabel, defaultHint, timer, timerPaused, timerHeld ({option}, {time}), announceStart, announceSoon, announceExtended ({added}, {time}), announceHeld, announceTimeout, escapeHint). English defaults.

Methods

NameDescription
show()Opens (or reopens) the prompt with a fresh timer.
close()Closes without choosing anything (no mv-choice).
choose(option | value | index)Chooses from code, reason "api" (cancelable like the others). Returns false if nothing matched or the event was cancelled.
extend(amount?)Adds time (ms or a time string; extend-by by default) and resumes a stopped timer. Emits mv-extend.
hold() / resume()Stops the timer for good, the prompt becoming a plain choice (what Escape does), and restarts it from where it was.

Events

NameDescription
mv-choiceCancelable, before anything is applied. detail: { value, reason: "user" | "timeout" | "api", option, label }. Not cancelled: the option gets data-chosen and the prompt closes. Cancelled: the prompt stays open and the timer is stopped (never retried behind the user's back).
mv-tickOnce per displayed second while open, and on pause / resume. detail: { remaining, total, progress (0 → 1), paused }.
mv-pause / mv-resumeThe timer stopped or restarted counting. detail: { reasons ("hover", "focus", "hidden", "offscreen", "blur" or "hold"), remaining }.
mv-extendTime was added. detail: { added, remaining, total }.

Content structure

NameDescription
(content)Your copy and your option buttons, in any structure. Styled parts you can use: .mv-timed-choice-icon or .mv-timed-choice-media (leading visual), .mv-timed-choice-body with .mv-timed-choice-eyebrow, .mv-timed-choice-title, .mv-timed-choice-text, and .mv-timed-choice-actions for the buttons. In the dialog variant the content is moved into the <dialog> on first connection: wrap framework-rendered content in one stable element.

CSS classes

NameDescription
mv-timed-choice-panelAdded to the element that carries role and labels: the host (bar, card) or the <dialog> (.mv-timed-choice-dialog).
mv-timed-choice-track / -meterThe draining timer line (aria-hidden); the meter is scaled to the remaining time.
mv-timed-choice-default / -fill / -countAdded to the accepted default option: the fill layer that sweeps across it and the countdown chip (-count-num, -count-icon).
mv-timed-choice-extendThe built-in “+10s” ghost button, inserted right before the first option.

CSS variables

NameDefaultDescription
--mv-timed-choice-tonevar(--mv-accent)Color of the draining meter and the icon tint.
--mv-timed-choice-fillcolor-mix(in oklab, currentColor 17%, transparent)Wash that fills the default option (currentColor works on any button style).
--mv-timed-choice-track3pxThickness of the draining track.
--mv-timed-choice-insetvar(--mv-space-4)Distance of the card from its corner.
--mv-timed-choice-width21.5rem (card), 26rem (dialog)Width of the card and the dialog.

Accessibility

Bar and card are role="group", the dialog variant role="alertdialog", each labelled by its title and described by its text plus a visually hidden role="timer" (“Play now in 8 seconds”, “…paused”, “Timer stopped”) whose implicit aria-live is off: it is read on demand and never chatters. A polite live region speaks exactly twice: once when the prompt opens (“Up next: The Night Ferry. Play now will be chosen automatically in 10 seconds.”) and once at 5 seconds left, skipped for timers of 7 seconds or less; plus a short message when time is added, when the timer is stopped and when the default was taken automatically. Non-modal variants never move focus when they open; when any prompt closes with focus inside, focus goes back to where it came from, or to the nearest control around the prompt when there is none (a click with no prior focus), never to the page body. Keyboard, screen-reader and scripted focus anywhere inside pauses the timer, whatever pause-on says, so exploring the options can never be cut short; a mouse click's focus doesn't count (hover already pauses). Escape inside stops the timer for good (WCAG 2.2.1 “turn off”), the “+10s” button extends it (“Add 10 seconds”), and assist or <html data-timing-assist> lengthens it up to 20×. The default option is described as “Chosen automatically when the timer runs out”; the fill and the countdown chip are aria-hidden and the paused state also changes shape (pause glyph, grey meter), not only color. modal uses a native <dialog> (inert page, focus contained) and moves the timer, hint and live region inside it while it is open, so they are never inert; the dialog itself receives focus so the countdown keeps running while it is read, the announcement adds “Press Escape to stop the timer.”, Escape never dismisses the question, and focus returns to where it was on close. A destructive default is refused, so nothing irreversible ever happens without an explicit action. Reduced motion (OS or data-motion="reduce"): the fill and the meter step once per second instead of sliding and extensions jump. Forced colors: meter and fill use Highlight, the fill becomes an underline so the label keeps system contrast. Known limits: with the default pause-on, the real time before the default is taken can be longer than duration (hidden tab, prompt scrolled away); without modal, the dialog variant does not contain focus (it is meant for prompts the user may ignore).

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