エクスクルーシブ

Saved State <mv-saved-state>

自動保存のステータス行(Unsaved changes · Saving… · Saved · 2 minutes ago · Offline · Couldn’t save · Retry)。saving() / saved() / failed() や track(promise) で制御するか、フォームを指定して動作させます。

仕組み

自動保存のステータス行(Unsaved changes · Saving… · Saved · 2 minutes ago · Offline · Couldn’t save · Retry)。saving() / saved() / failed() や track(promise) で制御するか、フォームを指定して動作させます。手書きの実装が間違えがちな点:ちらつくことはありません(「Saving…」は 400 ms より遅い保存のときだけ、「Saved」は最低 1 秒表示)。オフライン中や、保存中に入力された編集がまだ保留中の間は「Saved」と表示せず、読み上げるのはエラー、オフラインになったとき、復帰したときだけです。

カテゴリーフィードバック
タイプWeb Component (<mv-saved-state>)
ステータス安定版
Keywordsexclusive, light, autosave, saving, saved, status, offline, unsaved, beforeunload, retry

When to use

  • A document, note or settings page saves on its own and should say whether the latest edits are safe
  • A form saves in the background and users need a truthful Offline or Couldn’t save message with a way to retry
  • Leaving the page with edits still pending should ask for confirmation

Avoid when

  • Changes are queued locally and synced later, with a pending list, conflicts and background retries → use Bench Sync instead
  • The form saves only when the user clicks Save and pending edits should grow more insistent → use Tell-Tale instead
  • The result of a one-off action should appear briefly and then disappear → use Toast instead

インストール

node scripts/add.mjs saved-state --out ./src/marvelous

Marvelous UI の MCP サーバーを使う AI エージェント:install_components({ slugs: ["saved-state"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。

コピーされるファイル(依存関係を含む):tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/saved-state/saved-state.js, components/saved-state/saved-state.css。

使い方

クイックスタート、動作する最小限のマークアップ:

<mv-saved-state for="doc-form" guard></mv-saved-state>
<!-- in your autosave code: savedState.track(fetch("/api/doc", { method: "PUT", body })) -->

基本となるマークアップです。ここから属性、data-*、CSS 変数でカスタマイズしてください:

<div style="display:grid;gap:1.25rem;width:100%;max-width:560px">
  <form id="mv-saved-demo-doc" class="mv-card" style="padding:0;overflow:hidden">
    <div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:.7rem 1rem;border-bottom:1px solid var(--mv-border)">
      <span style="display:flex;align-items:center;gap:.5rem;min-width:0;font-size:.8rem;color:var(--mv-fg-muted)">
        <span>Marketing</span><span aria-hidden="true">/</span><span style="color:var(--mv-fg);font-weight:600">Q4 launch brief</span>
      </span>
      <mv-saved-state id="mv-saved-demo" for="mv-saved-demo-doc" guard></mv-saved-state>
    </div>
    <div style="display:grid;gap:.6rem;padding:1rem">
      <input class="mv-input" name="title" value="Aurora 2.0 launch, Oct 14" aria-label="Title">
      <textarea class="mv-textarea" name="body" rows="4" aria-label="Brief">Launch in the US, Brazil and Japan on Tuesday, October 14. Early-bird pricing at $49/year until October 31. Owners: Priya Raman (press), Kenji Watanabe (store listings), Lucía Ortega (support macros).</textarea>
    </div>
  </form>

  <div style="display:flex;flex-wrap:wrap;gap:.5rem 1.5rem;font-size:.85rem">
    <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="mv-saved-demo-slow"><span class="mv-choice-text">Slow network</span></label>
    <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="mv-saved-demo-fail"><span class="mv-choice-text">Server error</span></label>
    <label class="mv-choice"><input type="checkbox" role="switch" class="mv-switch" id="mv-saved-demo-offline"><span class="mv-choice-text">Offline</span></label>
  </div>
  <p style="margin:-.5rem 0 0;font-size:.8rem;color:var(--mv-fg-subtle)">Type to autosave. Saves faster than 400 ms never flash “Saving…”.</p>

  <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(15rem,1fr));gap:.6rem 1.5rem;padding-top:1rem;border-top:1px solid var(--mv-border)">
    <mv-saved-state id="mv-saved-demo-a"></mv-saved-state>
    <mv-saved-state id="mv-saved-demo-b"></mv-saved-state>
    <mv-saved-state id="mv-saved-demo-c"></mv-saved-state>
    <mv-saved-state id="mv-saved-demo-d"></mv-saved-state>
  </div>
</div>

<script type="module">
  const form = document.getElementById("mv-saved-demo-doc");
  const status = document.getElementById("mv-saved-demo");
  const $ = (id) => document.getElementById(`mv-saved-demo-${id}`);
  const ago = (min) => new Date(Date.now() - min * 60e3).toISOString();
  status.setAttribute("saved-at", ago(2));

  // Stand-in for fetch(): fast by default, slow, failing or offline on demand.
  const save = () => new Promise((resolve, reject) => setTimeout(() => {
    if ($("offline").checked) reject(new TypeError("Failed to fetch"));
    else if ($("fail").checked) reject(new Error("The server answered 503 Service Unavailable"));
    else resolve();
  }, $("slow").checked ? 1800 : 250));

  let timer = 0;
  form.addEventListener("submit", (e) => e.preventDefault());
  form.addEventListener("input", () => {
    clearTimeout(timer);
    timer = setTimeout(() => status.track(save()).catch(() => {}), 800);
  });
  status.addEventListener("mv-retry", (e) => e.detail.waitUntil(save()));
  $("offline").addEventListener("change", (e) => dispatchEvent(new Event(e.target.checked ? "offline" : "online")));

  // One of each state.
  $("a").setAttribute("saved-at", ago(180));
  customElements.whenDefined("mv-saved-state").then(() => {
    $("b").unsaved();
    $("c").saving();
    $("d").failed(new Error("Your session expired"));
    $("d").addEventListener("mv-retry", (e) => e.detail.waitUntil(new Promise((r) => setTimeout(r, 1200))));
  });
</script>

API

Attributes

NameタイプDefaultDescription
forform idForm whose input and change events mark the line “Unsaved changes”. Without it, the closest ancestor form, if any.
guardbooleanAsks for confirmation before leaving the page while edits are unsaved, saving or failed. The beforeunload listener exists only during those states.
saved-atISO dateTime of the last save known when the page loads (server-rendered). Shows “Saved · …” from the start.
localeBCP 47en-USLocale for the relative time (Intl.RelativeTimeFormat) and the full date in the tooltip.
data-stateidle | unsaved | saving | saved | offline | errorSet by the component: the state currently displayed (for styling).

Properties

NameタイプDescription
stateidle | unsaved | saving | saved | errorRead-only. The state reported by the app, before the anti-flicker timing and the offline overlay.
lastSavedDate | nullRead-only. Time of the last successful save.
errorunknownRead-only. The value passed to failed(), until the next successful save.
onlinebooleanRead-only. Connectivity as seen through navigator.onLine and the online / offline events.
stringsPartial<Record<string, string>>Overrides for every visible or announced text: idle (""), unsaved, saving, saved ("Saved · {time}"), justNow, savedTitle ("Last saved {date}"), offline, offlineIdle, backOnline, error, retry. English defaults.

Methods

NameDescription
unsaved()Edits are pending. During a save, remembers them so the next saved() shows “Unsaved changes” instead of “Saved”. Ignored while an error is shown.
saving()A save started. Displayed only if it lasts longer than 400 ms.
saved(at?)The save succeeded (at: Date or ISO string, default now). “Saved · just now”, then the time updates quietly.
failed(error?)The save failed. Shows “Couldn’t save · Retry”; error.message becomes the tooltip.
track(promise)saving(), then saved() or failed(reason) when the promise settles. Only the latest tracked promise counts. Returns the promise.
reset()Back to idle, forgetting the last save and error.

Events

NameDescription
mv-retryRetry was clicked, or the connection came back after a failure. detail = { reason: "click" | "online", error, waitUntil(promise) }. Pass your save to waitUntil (synchronously) to track it. Cancelable: preventDefault() keeps the error on screen. If nobody handles it, the line falls back to “Unsaved changes”.

CSS classes

NameDescription
mv-saved-state-icon / -text / -retryGenerated parts: the icon (aria-hidden), the status text (holds a <time> element when saved) and the Retry button.

CSS variables

NameDefaultDescription
--mv-saved-state-colorvar(--mv-fg-muted)Text and neutral icon color.
--mv-saved-state-successvar(--mv-success)Check icon when saved.
--mv-saved-state-unsavedvar(--mv-warning)Dot icon for unsaved changes.
--mv-saved-state-offlinevar(--mv-warning)Offline icon.
--mv-saved-state-errorvar(--mv-danger)Text and icon color on failure.
--mv-saved-state-font-sizevar(--mv-text-sm)Text size.
--mv-saved-state-icon-size1.05emIcon size.

Accessibility

Plain text in the page, so it can be read at any time without being chatty: the visible line is not a live region, and the relative time updates silently. A separate polite role=status region announces only the transitions that matter: “Couldn’t save”, going offline (“Offline · will save when you’re back”) and “Back online”; routine Unsaved / Saving / Saved changes are never announced, and the message is cleared after a few seconds so it is not read twice later. The state is carried by the words, never by the icon alone (icons are aria-hidden and use currentColor). Retry is a real <button> with a visible focus ring; if it disappears while focused, focus moves to the status text instead of being lost. The saved time is a <time datetime> with the full date as a tooltip. Reduced motion (OS or data-motion="reduce"): the spinner stops and state changes swap instantly. Forced colors: system text and link colors. The beforeunload guard is attached only while edits are pending, keeping the back-forward cache usable otherwise. Known limits: offline detection is the browser's online/offline events (navigator.onLine), which report a lost network but not a captive portal or a dead server, so those show as “Couldn’t save”; the guard uses the browser's own leave-page dialog, whose wording cannot be changed; the announcement for going offline describes the state at that moment (edits typed afterwards are shown, not announced again).

このページは AI で翻訳されています。翻訳の問題を報告