Exclusivebeta

Self-Destruct — <mv-self-destruct>

One-time secret display (API key, recovery codes, shared password, temporary link) that is shown once and then truly destroyed. It starts sealed behind a neutral cover, with nothing secret in the DOM; “Reveal” lights a fuse along the top edge and a countdown ring (default 30s). A built-in Copy button can shorten the remaining time (after-copy) and optionally wipe the clipboard at the end; the secret can also be destroyed when the tab is hidden, or be readable only while a button is held. At zero the characters scramble and drop out, every text node of the secret is removed and the JS reference is dropped, and a tombstone (“This secret was destroyed · 2:41 PM”) takes its place, with a slot for a “Generate a new key” action.

CategoryData display
TypeWeb Component (<mv-self-destruct>)
Statusbeta
Keywordsexclusive, culture, secret, one-time, api-key, token, recovery-codes, password, reveal, countdown, timer, copy, clipboard, security, privacy, ephemeral, hold-to-view, disintegrate

When to use

Avoid when

Install

node scripts/add.mjs self-destruct --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["self-destruct"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/self-destruct/self-destruct.js, components/self-destruct/self-destruct.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div id="sd-demo" style="width:min(100%,70rem);margin-inline:auto">
  <style>
    #sd-demo { display:grid; gap:1.25rem }
    #sd-demo .sd-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(19rem, 1fr)); gap:1.25rem; align-items:start }
    #sd-demo .sd-card { display:grid; gap:.875rem; padding:1.125rem 1.125rem 1rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); box-shadow:var(--mv-shadow-xs) }
    #sd-demo .sd-head { display:grid; gap:.25rem }
    #sd-demo .sd-head h3 { display:flex; align-items:center; gap:.5rem; margin:0; font-size:.9375rem; letter-spacing:-.01em }
    #sd-demo .sd-head p { margin:0; color:var(--mv-fg-muted); font-size:.8125rem; line-height:1.45 }
    #sd-demo .sd-log { margin:0; min-height:1.25rem; color:var(--mv-fg-subtle); font:400 .75rem/1.5 var(--mv-font-mono); overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
    #sd-demo .sd-log b { color:var(--mv-fg-muted); font-weight:600 }
    #sd-demo .sd-top { display:flex; align-items:center; justify-content:space-between; gap:1rem; flex-wrap:wrap }
    #sd-demo .sd-top p { margin:0; color:var(--mv-fg-muted); font-size:.8125rem }
    #sd-demo #sd-codes { --mv-self-destruct-font-size:.9375rem }
  </style>

  <div class="sd-top">
    <p>Each secret is shown once. Reveal, copy, or wait for the fuse to burn out.</p>
    <button class="mv-button" data-variant="outline" data-size="sm" type="button" id="sd-reset">Generate again</button>
  </div>

  <div class="sd-grid">
    <!-- 1 · Fresh API key: 30s, shortened to 5s after a copy, clipboard wiped at the end -->
    <section class="sd-card" aria-labelledby="sd-key-title">
      <div class="sd-head">
        <h3 id="sd-key-title">Production API key <span class="mv-badge" data-variant="success" data-size="sm">New</span></h3>
        <p>Store it in your secrets manager now. For your security, we can’t show it again.</p>
      </div>
      <mv-self-destruct id="sd-key" label="API key" seconds="30" after-copy="5" clear-clipboard>
        <button slot="destroyed" class="mv-button" data-size="sm" type="button" data-regen="sd-key">Generate a new key</button>
      </mv-self-destruct>
      <p class="sd-log" id="sd-key-log" aria-live="off">30s · 5s after copy · clipboard wiped</p>
    </section>

    <!-- 2 · Recovery codes from a <template>: grouped by 4, destroyed if the tab is hidden -->
    <section class="sd-card" aria-labelledby="sd-codes-title">
      <div class="sd-head">
        <h3 id="sd-codes-title">Two-factor recovery codes</h3>
        <p>Each code works once. Print them or save them offline before they disappear.</p>
      </div>
      <mv-self-destruct id="sd-codes" label="Recovery codes" seconds="60" group="4" on-hide>
        <template>
          <li>7k2m9x4q</li><li>p8w3n6fa</li><li>c5j1t9vr</li><li>h4d7s2ly</li>
          <li>q9b6e3mz</li><li>u2g8k5wn</li><li>r7x4c1pt</li><li>e3v9h6jd</li>
        </template>
        <button slot="destroyed" class="mv-button" data-variant="outline" data-size="sm" type="button" data-regen="sd-codes">Regenerate codes</button>
      </mv-self-destruct>
      <p class="sd-log" id="sd-codes-log" aria-live="off">60s · groups of 4 · ends on tab switch</p>
    </section>

    <!-- 3 · Shared password: visible only while held -->
    <section class="sd-card" aria-labelledby="sd-pass-title">
      <div class="sd-head">
        <h3 id="sd-pass-title">Shared vault password</h3>
        <p>Maya Chen shared “Office Wi-Fi · Guest” with you. It stays readable only while you hold.</p>
      </div>
      <mv-self-destruct id="sd-pass" label="Password" seconds="20" hold>
        <button slot="destroyed" class="mv-button" data-variant="outline" data-size="sm" type="button" data-regen="sd-pass">Request again</button>
      </mv-self-destruct>
      <p class="sd-log" id="sd-pass-log" aria-live="off">20s · press and hold (or hold Space) to view</p>
    </section>
  </div>

  <script type="module">
    await customElements.whenDefined("mv-self-destruct");
    const root = document.getElementById("sd-demo");
    const pick = (alphabet, n) => {
      const bytes = crypto.getRandomValues(new Uint8Array(n));
      return Array.from(bytes, (b) => alphabet[b % alphabet.length]).join("");
    };
    const lower = "abcdefghjkmnpqrstuvwxyz23456789";
    const make = {
      "sd-key": () => `mvk_live_${pick("ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz0123456789", 32)}`,
      "sd-codes": () => Array.from({ length: 8 }, () => pick(lower, 8)),
      "sd-pass": () => `${pick("ABCDEFGHJKLMNPQRSTUVWXYZ", 1)}${pick(lower, 5)}-${pick(lower, 4)}-${pick("0123456789", 2)}${pick("!#%&*?", 1)}`,
    };
    const initial = {
      "sd-key": "30s · 5s after copy · clipboard wiped",
      "sd-codes": "60s · groups of 4 · ends on tab switch",
      "sd-pass": "20s · press and hold (or hold Space) to view",
    };
    const time = () => new Date().toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", second: "2-digit" });
    const log = (id, text) => {
      const el = document.getElementById(`${id}-log`);
      el.replaceChildren();
      const b = document.createElement("b");
      b.textContent = text;
      el.append(b, ` · ${time()}`);
    };

    document.getElementById("sd-key").secret = make["sd-key"]();
    document.getElementById("sd-pass").secret = make["sd-pass"]();

    for (const id of Object.keys(make)) {
      const el = document.getElementById(id);
      el.addEventListener("mv-reveal", (e) => log(id, `mv-reveal { seconds: ${e.detail.seconds} }`));
      el.addEventListener("mv-copy", (e) => log(id, `mv-copy { remaining: ${e.detail.remaining.toFixed(1)} }`));
      el.addEventListener("mv-destroy", (e) => log(id, `mv-destroy { reason: "${e.detail.reason}" }`));
    }
    const regen = (id) => {
      document.getElementById(id).secret = make[id]();
      document.getElementById(`${id}-log`).textContent = initial[id];
    };
    root.addEventListener("click", (e) => {
      const btn = e.target.closest("[data-regen]");
      if (btn) regen(btn.dataset.regen);
    });
    document.getElementById("sd-reset").addEventListener("click", () => Object.keys(make).forEach(regen));
  </script>
</div>

Cultural reference

Mission: Impossible — Bruce Geller (1966, series). Each briefing exists only for the time needed to read it, then self-destructs. In the UI, a one-time secret is revealed on demand, burns down on a visible fuse, and is then genuinely removed from the DOM and from memory.

API

Attributes

NameTypeDefaultDescription
secondsnumber30How long the secret stays available once revealed (1–86400). The countdown is wall-clock based: it keeps running in a background tab and when the element is moved in the DOM.
after-copynumberAfter a successful copy (Copy button, or selecting the value and pressing Ctrl/Cmd+C), shortens the remaining time to this many seconds if more is left. The destruction reason then becomes "copy". Absent: copying does not change the countdown.
clear-clipboardbooleanAt destruction, overwrites the clipboard with an empty string if the last copy made in this document was the secret. Best effort: it only works while the page has focus (browsers reject clipboard writes from a background tab), and it cannot know whether something else was copied in another app since — in that case it would overwrite it.
on-hidebooleanDestroys the revealed secret as soon as the tab becomes hidden (tab switch, minimized window, locked screen). Reason "hidden".
holdbooleanHold-to-view mode: the value is rendered only while the “Hold to view” button is pressed (pointer, touch, or Space / Enter held) and removed from the DOM again on release. The first press starts the countdown; Copy stays available while the countdown runs.
groupnumberVisually groups characters by n ("4" → 7k2m 9x4q) using margins only, so copies and selections stay exact. Applies to each item of a list.
no-copybooleanHides the built-in Copy button (the value can still be selected by hand).
labelstringSecretWhat the secret is (“API key”): accessible name of the panel and wording of the announcements.
reveal-labelstringRevealLabel of the reveal button (hold mode: “Hold to view” / “Release to hide”).
destroyed-labelstringThis secret was destroyedTombstone title, followed by the time of destruction (en-US, e.g. 2:41 PM).
data-stateempty | sealed | revealed | destroying | destroyedSet by the component.
data-urgencysoon | finalSet by the component in the last 10 / 5 seconds (fuse and ring turn warning, then danger).
data-peekbooleanSet by the component while the value is held open (hold mode).

Properties

NameTypeDescription
secretstring | string[]Write-only. Loads a new secret and seals it (cancelling any countdown, replacing a tombstone): a string, a multi-line string (one item per line) or an array (rendered as a numbered list). Reading it always returns null so the value can't be scraped back from the element. Preferred over the <template> child. Can be set before the element is defined.
hasSecretbooleanRead-only: a secret is loaded and not yet destroyed.
statestringRead-only: empty | sealed | revealed | destroying | destroyed.
remainingnumberRead-only: seconds left (sealed: the full duration; destroyed: 0).

Methods

NameDescription
reveal()Shows the secret (in hold mode: starts the countdown) and lights the fuse. Returns false unless sealed.
copy()Copies the secret (Clipboard API, hidden-textarea fallback) and applies after-copy. Resolves to true on success.
destroy(reason?)Destroys now (default reason "manual"); also works while sealed. The secret leaves the DOM synchronously, before the disintegration plays.

Events

NameDescription
mv-revealThe secret was revealed (hold mode: first press). detail: { seconds }.
mv-copyThe secret was copied. detail: { remaining } — never the secret itself.
mv-destroyThe secret was purged (fired right away; the animation then plays for about a second). detail: { reason: "timeout" | "manual" | "hidden" | "copy" | "removed", at: Date }. "removed": the element was detached from the page while revealed.

Content structure

NameDescription
<template> childAlternative to the secret property: its (inert) content is read once at mount, then the template is removed. Text lines, or one element per item (<li>, <code>…), or a single list element.
destroyedContent shown in the tombstone once destroyed, e.g. a “Generate a new key” button. Receives focus if focus was inside the component.

CSS classes

NameDescription
mv-self-destruct-panelLive card (role=group, named by label): fuse, stage, bar.
mv-self-destruct-stage / -cover / -mask / -noteValue area and the neutral cover shown while sealed (fixed placeholder, never derived from the secret).
mv-self-destruct-value / -code / -chunk / -listRendered secret (monospace), its groups and the numbered list for several items.
mv-self-destruct-fuse / -sparkTop-edge fuse burning down to the left and its glowing tip.
mv-self-destruct-timer / -ring / -secsCountdown ring and “Self-destructs in 24s”.
mv-self-destruct-btnReveal / hold, Copy (data-state copied | error) and Destroy now buttons.
mv-self-destruct-tombstone / -slotWhat remains after destruction: title, time, hint and the destroyed slot.

CSS variables

NameDefaultDescription
--mv-self-destruct-colorvar(--mv-accent)Fuse and ring color while time is comfortable.
--mv-self-destruct-soonvar(--mv-warning)Color in the last 10 seconds.
--mv-self-destruct-finalvar(--mv-danger)Color in the last 5 seconds and during destruction.
--mv-self-destruct-font-sizevar(--mv-text-base)Size of the monospace value.
--mv-self-destruct-radiusvar(--mv-radius-lg)Corner radius of the panel and tombstone.

Accessibility

The panel is a role=group named by label (“API key”). Sealed, it contains only a fixed placeholder and a text note (“Hidden · visible for 30s once revealed”); the Reveal button is a real <button>. After reveal, focus moves to the value (tabindex=-1, focus ring) so screen readers read it, then Tab reaches Copy and Destroy now. A polite role=status region announces the reveal (“API key revealed. It will be destroyed in 30 seconds.”), a 10-second warning, copies (“Copied to clipboard. Destroying in 5 seconds.”) and the destruction; the ticking seconds are visible text but not a live region, so nothing chatters every second. Hold mode: the button works with pointer, touch (no context menu, no scroll while holding) and Space / Enter held (key repeat ignored, blur releases); the value region is aria-live=polite so it is read out when it appears. At destruction the characters are replaced by random glyphs synchronously and the value is aria-hidden during the animation; the tombstone is focusable (tabindex=-1) and focus moves to the destroyed slot's first control (or the tombstone) if it was inside the component; regenerating from there lands on the new Reveal button. Urgency is never color alone: the seconds count and the ring length carry it. Reduced motion (system or data-motion="reduce"): no flicker, no scramble or fall — the value is masked and fades out, then the tombstone fades in; the fuse and ring still move because they carry the remaining time. Security notes: nothing secret is in the DOM before reveal nor after destruction, event details never contain the secret, the secret getter returns null; JavaScript strings cannot be zeroed, so “removed from memory” means every reference is dropped for the garbage collector. Content in a <template> is present in the HTML source: prefer the secret property.