익스클루시브

Saved State <mv-saved-state>

saving() / saved() / failed() 또는 track(promise)로, 혹은 폼을 지정하는 것만으로 동작하는 자동 저장 상태 줄(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…”은 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로 번역되었습니다. 번역 문제 신고