익스클루시브

Assist Mode <mv-assist-mode>

시간이나 움직임에 의존하는 모든 것을 위한, 판단 없는 사이트 전체 보조 패널로, “접근성 모드”가 아니라 선택적인 도움으로 제공됩니다.

문화적 레퍼런스

셀레스트, Maddy Makes Games (Maddy Thorson & Noel Berry) (2018, 게임). 난도 높은 플랫포머가 어떠한 판단도 없이 보조 메뉴를 제공하여, 누구나 게임 속도를 늦추거나 추가 도움을 받거나 무적이 될 수 있으므로 모두가 끝까지 도달할 수 있습니다. UI에서는 사이트 전체 보조 패널로 누구나 시간 제한을 늘리거나 없애고, 자동 재생을 멈추고, 움직임을 줄이고, 대상을 크게 만들 수 있으며, 페이지의 모든 시간 기반 컴포넌트가 그 선택을 따릅니다.

작동 방식

시간이나 움직임에 의존하는 모든 것을 위한, 판단 없는 사이트 전체 보조 패널로, “접근성 모드”가 아니라 선택적인 도움으로 제공됩니다. 방문자는 시간 배율(1×, 1.5×, 2×, 4× 또는 시간 제한 없음), 자동 재생 일시 정지, 동작 줄이기, 더 안정적인 호버 팝업, 더 큰 대상, 읽기 지원(줄, 글자, 단어, 문단 간격), 더 강한 대비를 선택할 수 있습니다. 각 설정은 쉬운 말 한 줄과 효과를 보여 주는 작은 실시간 미리 보기로 구성됩니다. 선택 사항은 <html>(또는 대상 영역)에 CSS와 스크립트가 읽을 수 있는 속성으로 기록됩니다: data-timing-assist + --mv-assist-time(timed-choice가 이미 지원), 라이브러리의 data-motion 규칙, data-assist-autoplay(비디오, [autoplay] 요소, 캐러셀 같은 [data-autoplay] 요소가 실제로 일시 정지되며, 이 기능이 멈춘 것만 다시 재생됩니다), data-assist-hover, -targets(크기 토큰 기반의 모든 컨트롤이 44px로 커집니다), -reading, -contrast. 변경하지 않은 설정은 기기 설정(prefers-reduced-motion, prefers-contrast)을 실시간으로 따르며 “From your device” 태그가 붙습니다. 실제 선택만 저장되고(localStorage, sessionStorage 또는 저장 안 함, 모두 보호 처리), 탭 간과 인스턴스 간에 동기화되며, 초기화는 항상 안내되고 되돌릴 수 있습니다. 모든 변경은 전체 상태와 함께 mv-assist-change로 브로드캐스트되며, timing(ms)과 hoverDelay(ms) 내보내기, MvAssistMode.get()을 사용하면 어떤 타이머든 한 줄로 배율을 적용할 수 있습니다. 두 가지 변형: 비모달(또는 모달) 대화 상자가 있는 플로팅 런처, 설정 페이지용 인라인 패널.

카테고리유틸리티
유형Web Component (<mv-assist-mode>)
상태안정
함께 설치되는 항목button, checkbox, switch
Keywordsexclusive, culture, accessibility, a11y, preferences, settings, assist, timing, timing-adjustable, wcag-2-2-1, wcag-2-2-2, reduced-motion, pause-autoplay, target-size, text-spacing, contrast, hover-delay, persisted, multi-tab, utility

When to use

  • A site has toasts, countdowns, carousels or auto-advancing steps and must let people ask for more time or none (WCAG 2.2.1)
  • Visitors need one place to pause auto-play, reduce motion and enlarge targets, remembered on their next visit
  • An account settings page needs an Accessibility or Display section wired to real behavior, not just stored flags
  • Several timed components should share one user preference instead of each offering its own slower option

Avoid when

  • The only need is to cap or slow decorative animation, with no timing or reading preferences → use Motion Budget instead
  • The site only needs a light and dark switch → use Theme Toggle instead
  • It would replace fixing the page itself: assists add comfort but never make inaccessible markup, contrast or timing compliant

설치

node scripts/add.mjs assist-mode --out ./src/marvelous

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

복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, components/button/button.css, components/checkbox/checkbox.css, components/switch/switch.css, core/dom.js, core/element.js, core/observe.js, components/assist-mode/assist-mode.js, components/assist-mode/assist-mode.css.

사용법

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

<div id="am-demo">
  <style>
    #am-demo { display:grid; grid-template-columns:minmax(0,1fr) 25rem; gap:1.25rem; align-items:start; width:min(100%,66rem); margin-inline:auto }
    #am-demo .am-stage { position:relative; display:grid; gap:1rem; align-content:start; min-height:40rem; padding:0 0 7.75rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-bg-subtle); box-shadow:var(--mv-shadow-sm); overflow:hidden }
    #am-demo .am-bar { display:flex; align-items:center; gap:1rem; min-height:3.5rem; padding:.5rem 1rem .5rem 1.125rem; border-bottom:1px solid var(--mv-border); background:var(--mv-surface) }
    #am-demo .am-brand { display:flex; align-items:center; gap:.5rem; font-weight:650; font-size:.9375rem; letter-spacing:-.01em }
    #am-demo .am-logo { display:grid; place-items:center; width:1.75rem; height:1.75rem; border-radius:var(--mv-radius-md); background:var(--mv-accent); color:var(--mv-fg-on-accent) }
    #am-demo .am-logo svg { width:1rem; height:1rem }
    #am-demo .am-nav { display:flex; gap:.25rem; margin:0; padding:0; list-style:none }
    #am-demo .am-nav a { display:block; padding:.375rem .625rem; border-radius:var(--mv-radius-md); color:var(--mv-fg-muted); font-size:.8125rem; font-weight:500; text-decoration:none }
    #am-demo .am-nav a[aria-current] { background:var(--mv-bg-muted); color:var(--mv-fg) }
    #am-demo .am-bar .mv-button { margin-inline-start:auto }
    #am-demo .am-pad { display:grid; gap:1rem; padding:0 1.125rem }
    #am-demo .am-slide { display:grid; align-content:end; gap:.125rem; height:100%; box-sizing:border-box; padding:1rem 4rem; border-radius:var(--mv-radius-lg); color:#fff }
    #am-demo .am-slide small { font-size:.6875rem; letter-spacing:.08em; text-transform:uppercase; opacity:.85 }
    #am-demo .am-slide strong { font-size:1.375rem; letter-spacing:-.02em }
    #am-demo .am-slide span { font-size:.8125rem; opacity:.9 }
    #am-demo .am-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1rem }
    #am-demo .am-card { display:grid; gap:.625rem; align-content:start; min-width:0; padding:1rem 1.125rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-lg); background:var(--mv-surface) }
    #am-demo .am-card h4 { margin:0; font-size:.875rem; font-weight:600 }
    #am-demo .am-card p { margin:0; color:var(--mv-fg-muted); font-size:.8125rem; line-height:var(--mv-leading-normal) }
    #am-demo .am-hold { display:flex; align-items:baseline; justify-content:space-between; gap:.5rem }
    #am-demo .am-fare { font-size:1.25rem; font-weight:650; letter-spacing:-.01em; font-variant-numeric:tabular-nums }
    #am-demo .am-clock { color:var(--mv-fg-muted); font-size:.8125rem; font-variant-numeric:tabular-nums }
    #am-demo .am-clock b { color:var(--mv-fg); font-weight:600 }
    #am-demo .am-actions { display:flex; flex-wrap:wrap; gap:.5rem }
    #am-demo .am-ticker { position:relative; overflow:hidden; border:1px solid var(--mv-border); border-radius:var(--mv-radius-md); background:var(--mv-bg-subtle); mask:linear-gradient(90deg,transparent,#000 8%,#000 92%,transparent) }
    #am-demo .am-ticker-track { display:flex; gap:1.75rem; width:max-content; padding:.5rem 0; font:500 .75rem/1.2 var(--mv-font-mono); white-space:nowrap; animation:am-ticker 22s linear infinite }
    #am-demo .am-ticker-track span b { color:var(--mv-success) }
    #am-demo .am-ticker-track span i { font-style:normal; color:var(--mv-warning) }
    @keyframes am-ticker { to { translate:-50% 0 } }
    #am-demo .am-tip { position:relative; display:inline }
    #am-demo .am-tip-trigger { padding:0; border:0; background:none; color:var(--mv-accent-fg); font:inherit; text-decoration:underline dotted; text-underline-offset:3px; cursor:help }
    #am-demo .am-tip-card { position:absolute; z-index:5; left:0; bottom:calc(100% + .5rem); width:15rem; padding:.625rem .75rem; border:1px solid var(--mv-border-strong); border-radius:var(--mv-radius-md); background:var(--mv-surface-raised); box-shadow:var(--mv-shadow-lg); color:var(--mv-fg); font-size:.75rem; line-height:1.5 }
    #am-demo .am-tip-card[hidden] { display:none }
    #am-demo .am-toast { position:absolute; left:1.125rem; bottom:1.125rem; z-index:3; display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:.25rem .75rem; width:min(21rem,calc(100% - 9.5rem)); padding:.75rem .875rem .875rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-lg); background:var(--mv-surface-raised); box-shadow:var(--mv-shadow-lg); overflow:hidden }
    #am-demo .am-toast[hidden] { display:none }
    #am-demo .am-toast-icon { display:grid; place-items:center; width:1.75rem; height:1.75rem; border-radius:50%; background:color-mix(in oklab,var(--mv-success) 16%,transparent); color:var(--mv-success) }
    #am-demo .am-toast-icon svg { width:1rem; height:1rem }
    #am-demo .am-toast strong { display:block; font-size:.8125rem }
    #am-demo .am-toast span { display:block; color:var(--mv-fg-muted); font-size:.75rem }
    #am-demo .am-toast-bar { position:absolute; left:0; right:0; bottom:0; height:3px; background:var(--mv-accent); transform-origin:left; animation:am-drain linear forwards }
    #am-demo .am-toast-bar[hidden] { display:none }
    @keyframes am-drain { from { scale:1 1 } to { scale:0 1 } }
    #am-demo .am-side { display:grid; gap:.75rem }
    #am-demo .am-log { margin:0; padding:.625rem .75rem; border:1px dashed var(--mv-border-strong); border-radius:var(--mv-radius-md); color:var(--mv-fg-muted); font:.6875rem/1.5 var(--mv-font-mono); overflow-wrap:anywhere }
    #am-demo .am-log b { color:var(--mv-fg); font-weight:600 }
    @media (max-width:60rem) { #am-demo { grid-template-columns:minmax(0,1fr) } }
    @media (max-width:36rem) { #am-demo .am-grid { grid-template-columns:minmax(0,1fr) } #am-demo .am-nav { display:none } }
  </style>

  <!-- A travel site whose timers, auto-play and targets all follow the assist settings -->
  <section class="am-stage" id="am-stage" aria-label="Travel site preview">
    <header class="am-bar">
      <span class="am-brand">
        <span class="am-logo" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 15l7-2 5-8 2 1-3 7 5 1 2 3-8-1-5 5-2-1 2-4-5-1z"/></svg></span>
        Wayline
      </span>
      <ul class="am-nav">
        <li><a href="#" aria-current="page">Flights</a></li>
        <li><a href="#">Stays</a></li>
        <li><a href="#">Trips</a></li>
      </ul>
      <button type="button" class="mv-button" data-variant="outline" data-size="sm">Sign in</button>
    </header>

    <div class="am-pad">
      <mv-carousel label="Featured trips" loop autoplay="3500" style="--mv-carousel-height:8.5rem">
        <div class="am-slide" style="background:linear-gradient(135deg,#f0845c,#b14a6d 60%,#5b2a6e)"><small>Portugal · from $412</small><strong>Lisbon</strong><span>Trams, tiled facades and sunsets over the Tagus.</span></div>
        <div class="am-slide" style="background:linear-gradient(160deg,#e45f7f,#7d3c98 55%,#2c2a6b)"><small>Japan · from $684</small><strong>Tokyo</strong><span>Late trains, quiet gardens and ramen at midnight.</span></div>
        <div class="am-slide" style="background:linear-gradient(150deg,#1f9c9a,#1d5f8a 55%,#16294f)"><small>Canada · from $298</small><strong>Vancouver</strong><span>Mountains, sea walls and ferries to the islands.</span></div>
      </mv-carousel>

      <div class="am-grid">
        <article class="am-card" aria-labelledby="am-hold-title">
          <h4 id="am-hold-title">We’re holding this fare</h4>
          <div class="am-hold">
            <span class="am-fare">$684</span>
            <span class="am-clock" id="am-clock" role="timer">Held for <b>4:00</b></span>
          </div>
          <p>Lisbon to Tokyo, Oct 14-28, 1 adult. The price is guaranteed until the hold ends.</p>
          <div class="am-actions">
            <button type="button" class="mv-button" data-size="sm">Book now</button>
            <button type="button" class="mv-button" data-variant="ghost" data-size="sm" id="am-restart">Restart hold</button>
          </div>
        </article>

        <article class="am-card" aria-labelledby="am-dep-title">
          <h4 id="am-dep-title">Live departures</h4>
          <div class="am-ticker" data-autoplay aria-hidden="true">
            <div class="am-ticker-track">
              <span>LIS → NRT 14:05 <b>On time</b></span><span>YVR → LHR 15:40 <i>Boarding</i></span><span>SFO → SIN 16:10 <b>On time</b></span><span>CDG → GRU 16:55 <i>Delayed</i></span>
              <span>LIS → NRT 14:05 <b>On time</b></span><span>YVR → LHR 15:40 <i>Boarding</i></span><span>SFO → SIN 16:10 <b>On time</b></span><span>CDG → GRU 16:55 <i>Delayed</i></span>
            </div>
          </div>
          <p>Carry-on bags up to 22 lb are included on every fare, plus one
            <span class="am-tip">
              <button type="button" class="am-tip-trigger" id="am-tip-trigger" aria-describedby="am-tip-card" aria-expanded="false">personal item</button>
              <span class="am-tip-card" id="am-tip-card" role="tooltip" hidden>A bag that fits under the seat in front of you: 16 × 12 × 6 in. Laptop bags and small backpacks count.</span>
            </span>.
          </p>
          <div class="am-actions">
            <button type="button" class="mv-button" data-variant="secondary" data-size="sm" id="am-alert">Save price alert</button>
          </div>
        </article>
      </div>

      <article class="am-card" aria-labelledby="am-know-title">
        <h4 id="am-know-title">Know before you go</h4>
        <p>Japan lets US, Canadian and EU citizens visit for up to 90 days without a visa. Your passport must stay valid for the whole stay, and you may be asked for proof of onward travel at check-in.</p>
        <p>Trains from Narita to central Tokyo take about an hour. The airport express leaves every 30 minutes and seats can be reserved when you book.</p>
      </article>
    </div>

    <div class="am-toast" id="am-toast" role="status" hidden>
      <span class="am-toast-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg></span>
      <span><strong>Price alert saved</strong><span id="am-toast-text">We’ll email you if Lisbon to Tokyo drops.</span></span>
      <button type="button" class="mv-button" data-variant="ghost" data-size="sm" id="am-toast-close">Dismiss</button>
      <span class="am-toast-bar" id="am-toast-bar" aria-hidden="true"></span>
    </div>

    <!-- Floating variant, contained in the preview; it shares its settings with the inline panel -->
    <mv-assist-mode id="am-floating" target="#am-stage" persist="none" storage-key="am-demo" contained></mv-assist-mode>
  </section>

  <div class="am-side">
    <mv-assist-mode id="am-inline" variant="inline" target="#am-stage" persist="none" storage-key="am-demo" heading-level="3"></mv-assist-mode>
    <p class="am-log" id="am-log" aria-live="off">mv-assist-change …</p>
  </div>

  <script type="module">
    await customElements.whenDefined("mv-assist-mode");
    const Assist = customElements.get("mv-assist-mode");
    const root = document.getElementById("am-demo");
    const stage = root.querySelector("#am-stage");
    const inline = root.querySelector("#am-inline");
    const log = root.querySelector("#am-log");
    const $ = (id) => root.querySelector(`#${id}`);

    // Fare hold: 2 minutes of real time, stretched by the multiplier.
    const clock = $("am-clock");
    let holdStart = Date.now();
    const fmt = (ms) => { const t = Math.ceil(ms / 1000); return `${Math.floor(t / 60)}:${String(t % 60).padStart(2, "0")}`; };
    const renderHold = () => {
      if (!clock.isConnected) return clearInterval(tick);
      const total = Assist.timing(120_000, clock);
      const b = document.createElement("b");
      if (total === Infinity) { b.textContent = "until you decide"; clock.replaceChildren("Held ", b); return; }
      b.textContent = fmt(Math.max(0, total - (Date.now() - holdStart)));
      clock.replaceChildren("Held for ", b);
    };
    const tick = setInterval(renderHold, 1000);
    $("am-restart").addEventListener("click", () => { holdStart = Date.now(); renderHold(); });

    // Price alert toast: closes after 5 s × the multiplier, or never with no limits.
    const toast = $("am-toast"), bar = $("am-toast-bar");
    let hideTimer = 0;
    const showToast = () => {
      clearTimeout(hideTimer);
      const ms = Assist.timing(5000, toast);
      toast.hidden = false;
      bar.hidden = ms === Infinity;
      bar.style.animation = "none"; void bar.offsetWidth; bar.style.animation = "";
      bar.style.animationDuration = `${ms}ms`;
      if (ms !== Infinity) hideTimer = setTimeout(() => { toast.hidden = true; }, ms);
    };
    $("am-alert").addEventListener("click", showToast);
    $("am-toast-close").addEventListener("click", () => { clearTimeout(hideTimer); toast.hidden = true; });

    // Hover card: longer open delay and sticky with “Steadier hover”.
    const trigger = $("am-tip-trigger"), card = $("am-tip-card"), tip = trigger.parentElement;
    let openTimer = 0, closeTimer = 0;
    const setOpen = (on) => { card.hidden = !on; trigger.setAttribute("aria-expanded", String(on)); };
    const steady = () => Boolean(trigger.closest("[data-assist-hover]"));
    tip.addEventListener("pointerenter", () => { clearTimeout(closeTimer); openTimer = setTimeout(() => setOpen(true), Assist.hoverDelay(300, trigger)); });
    tip.addEventListener("pointerleave", () => { clearTimeout(openTimer); closeTimer = setTimeout(() => setOpen(false), steady() ? 700 : 0); });
    trigger.addEventListener("focus", () => setOpen(true));
    trigger.addEventListener("blur", () => { if (!steady()) setOpen(false); });
    trigger.addEventListener("click", () => setOpen(card.hidden));
    tip.addEventListener("keydown", (e) => { if (e.key === "Escape" && !card.hidden) { e.stopPropagation(); setOpen(false); } });

    // What other scripts hear.
    root.addEventListener("mv-assist-change", (e) => {
      const { state, source, changed } = e.detail;
      const on = Object.entries(state).filter(([k, v]) => (k === "time" ? v !== 1 : v)).map(([k, v]) => (k === "time" ? `time ${v === Infinity ? "∞" : v + "×"}` : k));
      log.replaceChildren(Object.assign(document.createElement("b"), { textContent: "mv-assist-change" }), ` · ${source}${changed.length ? ` (${changed.join(", ")})` : ""} · ${on.join(", ") || "all off"}`);
      renderHold();
    });

    // Start with two assists on so the effect is visible right away.
    inline.set({ time: 2, targets: true });
    renderHold();
    showToast();
  </script>
</div>

API

Attributes

Name유형DefaultDescription
variantfloating | inlinefloatingfloating: a pill launcher (“Assist”, with the number of assists on) pinned to a corner that opens the panel in a <dialog>. inline: the panel itself, in the flow (settings page). Mirrored as data-variant.
targetCSS selectorhtmlElement that receives the assist attributes. Leave it empty for the whole site; a region (a preview, an embedded app) is possible, but JavaScript components that read reduced motion from <html> only follow a page-wide target.
persistlocal | session | nonelocalWhere the user's choices are kept: localStorage (synced across tabs), sessionStorage, or nowhere. Storage errors are caught; if the browser refuses, the footer says the settings last until the visitor leaves.
storage-keystringmv-assistStorage key, also used to keep several instances on one page (a floating launcher and a settings page) in sync.
featuresspace- or comma-separated list: time, autoplay, motion, hover, targets, reading, contrastallSettings to offer. Settings left out are neither shown nor applied.
timeslist of multipliers, “none” = no limits1 1.5 2 4 noneSteps of the time multiplier (1× is always included). A value set through set() that is not listed gets its own step.
placementbottom-end | bottom-start | top-end | top-startbottom-endCorner of the floating launcher; the panel opens next to it.
containedbooleanPins the launcher and panel to the nearest positioned ancestor instead of the viewport (previews, app regions).
modalbooleanOpens the panel with showModal(): centered, backdrop, focus contained by the browser. By default the dialog is non-modal so the page stays visible and usable while settings change.
compactbooleanIcon-only launcher (the count moves to a corner badge); its accessible name stays “Assist”, plus the count.
openbooleanFloating variant: the panel is open. Reflects show(), close() and the launcher.
heading-level1-62Level of the panel heading, to fit the page outline.
data-activeset by the componentPresent on the host while at least one assist is on.

Properties

Name유형Description
state{ time, autoplay, motion, hover, targets, reading, contrast }Settings in effect (copy). time is a multiplier, Infinity for no limits; the others are booleans.
sourcesRecord<key, "user" | "device" | "default">Where each value comes from. device: pre-filled from prefers-reduced-motion (motion and auto-play) or prefers-contrast: more, and still following it live.
explicitPartial<state>Only the values the user changed: what is stored.
savedbooleanFalse when the browser refused to store the last change.
stringsPartial<Record<string, string>>Overrides for every visible text and announcement (title, intro, launcher, section and row titles and texts, time effect lines with {time}, footer, reset/undo, announcements). English defaults.
MvAssistMode.get()staticState of the first connected instance, or the stored settings merged with the device preferences when none is on the page. For scripts that don't want to listen to events.
MvAssistMode.set(partial)staticCalls set() on the first connected instance; false when there is none.
MvAssistMode.restore({ storageKey?, persist?, target?, features? })staticApplies the stored settings immediately, before any instance exists (early in the page, to avoid a flash of motion on load). Returns the state.
MvAssistMode.timing / MvAssistMode.hoverDelaystaticSame as the timing() and hoverDelay() exports.

Methods

NameDescription
set(partial)Applies settings as a user choice (stored, broadcast with source "api"), e.g. set({ time: 2, targets: true }); time accepts a number, "none" or Infinity. Unknown keys and invalid values are ignored. Returns the new state.
reset()Forgets every user choice so all settings follow the device again, announces it and turns the footer button into “Undo reset”. Returns false if there was nothing to reset.
undo()Restores the choices cleared by the last reset (until another change is made).
show() / close() / toggle(force?)Floating variant: opens or closes the panel. Opening focuses the checked time step (or the first control); closing returns focus to the launcher if it was inside.
timing(duration, el?)Named export. duration × the multiplier in effect for el (closest [data-timing-assist], else <html>); Infinity for no time limits, so skip scheduling the timer. SSR-safe.
hoverDelay(delay, el?)Named export. delay, or max(delay × 2.5, delay + 600) under “Steadier hover”; such hover content should also stay open until the pointer leaves both trigger and content, or Escape.

Events

NameDescription
mv-assist-changeBubbles to document on every effective change. detail: { state, sources, explicit, changed (keys), source: "init" | "user" | "api" | "device" | "sync" | "reset" | "undo", key, target }. Fired once with source "init" on connection, so listeners can adopt the initial state.
mv-assist-open / mv-assist-closeFloating variant: the panel opened or closed.

CSS classes

NameDescription
mv-assist-mode-panelThe settings panel (<section> labelled by its heading): -head, -title, -close, -intro, -section (role=group) with -section-title, -rows, -foot, -status (-count, -saved), -reset.
mv-assist-mode-rowOne setting: .mv-assist-mode-preview (data-preview=time | autoplay | motion | hover | targets | reading | contrast), -row-title, -origin (“From your device”), -row-desc and the switch. data-key, data-on.
mv-assist-mode-time / -steps / -step / -effectThe time multiplier: a role=radiogroup of native radios drawn as a segmented control, and the line that spells out its effect. data-unlimited with no limits.
mv-assist-mode-launcher / -launcher-count / -dialogFloating variant: the pill button (aria-haspopup=dialog, aria-expanded), its count badge and the <dialog> (data-modal with modal).

CSS variables

NameDefaultDescription
--mv-assist-time1Set on the target: the time multiplier (1000 for no limits). Scale CSS timers with it: animation-duration: calc(5s * var(--mv-assist-time, 1)).
--mv-assist-hover-scale1 (2.5 when steady)Set on the target: scale CSS hover delays with it.
--mv-assist-leading / --mv-assist-letter-spacing / --mv-assist-word-spacing / --mv-assist-paragraph-spacing1.8 / 0.03em / 0.12em / 1emReading support values (define them on the target to tune).
--mv-assist-mode-accentvar(--mv-accent)Tint of active previews, the launcher icon and its count.
--mv-assist-mode-offset1remDistance of the floating launcher and panel from the corner.
--mv-assist-mode-width26remMaximum width of the inline panel.

Accessibility

Everything is built from native controls: each on/off setting is an <input type="checkbox" role="switch"> named by its title and described by its one-line explanation (the whole row is its <label>, so the hit area is the full row), and the time multiplier is a role="radiogroup" of native radios (arrow keys, one tab stop) described by the line that spells out its effect in words (“A message that closes after 5 seconds now stays 10 seconds.”), never by the preview alone. Each group of settings is a labelled role="group". Previews are aria-hidden and pointer-events: none. Values that come from the device are marked in text (“From your device”), not only by state. The floating launcher is a real <button> with aria-haspopup="dialog", aria-expanded and aria-controls, whose name starts with its visible label (“Assist, 2 on”). The panel is a <dialog> labelled by its heading: opening moves focus to the checked time step (or the first control), Escape or the Close button closes it and returns focus to the launcher; it is non-modal by default so the page can be checked while settings change, and modal uses showModal() for full focus containment. Nothing is ever reset silently: a stored entry that cannot be read is ignored but never overwritten until the user changes something, Reset all is announced in a polite live region and becomes Undo reset, a change from another tab is announced, and a browser that refuses to store settings is announced once and stated in the footer. Settings apply immediately (no Save step), and the component's own previews stop under prefers-reduced-motion or data-motion="reduce". Forced colors: previews use CanvasText and Highlight, the checked step gets a Highlight outline.

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