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.
| Category | Data display |
|---|---|
| Type | Web Component (<mv-self-destruct>) |
| Status | beta |
| Keywords | exclusive, 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
- A freshly generated API key, token or webhook secret must be shown exactly once after creation
- Two-factor recovery codes are displayed for the user to save before they leave the page
- A shared password or temporary link should be readable briefly, then gone from the screen
- Support or admin tools reveal a sensitive value on demand and must not leave it lying on screen
Avoid when
- The value is not sensitive and should stay visible to be copied at any time → use Copy Button instead
- The secret must stay out of the page source: never server-render it inside the template, set the secret property from JS
Install
node scripts/add.mjs self-destruct --out ./src/marvelousAI 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
| Name | Type | Default | Description |
|---|---|---|---|
seconds | number | 30 | How 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-copy | number | After 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-clipboard | boolean | At 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-hide | boolean | Destroys the revealed secret as soon as the tab becomes hidden (tab switch, minimized window, locked screen). Reason "hidden". | |
hold | boolean | Hold-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. | |
group | number | Visually groups characters by n ("4" → 7k2m 9x4q) using margins only, so copies and selections stay exact. Applies to each item of a list. | |
no-copy | boolean | Hides the built-in Copy button (the value can still be selected by hand). | |
label | string | Secret | What the secret is (“API key”): accessible name of the panel and wording of the announcements. |
reveal-label | string | Reveal | Label of the reveal button (hold mode: “Hold to view” / “Release to hide”). |
destroyed-label | string | This secret was destroyed | Tombstone title, followed by the time of destruction (en-US, e.g. 2:41 PM). |
data-state | empty | sealed | revealed | destroying | destroyed | Set by the component. | |
data-urgency | soon | final | Set by the component in the last 10 / 5 seconds (fuse and ring turn warning, then danger). | |
data-peek | boolean | Set by the component while the value is held open (hold mode). |
Properties
| Name | Type | Description |
|---|---|---|
secret | string | 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. |
hasSecret | boolean | Read-only: a secret is loaded and not yet destroyed. |
state | string | Read-only: empty | sealed | revealed | destroying | destroyed. |
remaining | number | Read-only: seconds left (sealed: the full duration; destroyed: 0). |
Methods
| Name | Description |
|---|---|
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
| Name | Description |
|---|---|
mv-reveal | The secret was revealed (hold mode: first press). detail: { seconds }. |
mv-copy | The secret was copied. detail: { remaining } — never the secret itself. |
mv-destroy | The 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
| Name | Description |
|---|---|
<template> child | Alternative 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. |
destroyed | Content shown in the tombstone once destroyed, e.g. a “Generate a new key” button. Receives focus if focus was inside the component. |
CSS classes
| Name | Description |
|---|---|
mv-self-destruct-panel | Live card (role=group, named by label): fuse, stage, bar. |
mv-self-destruct-stage / -cover / -mask / -note | Value area and the neutral cover shown while sealed (fixed placeholder, never derived from the secret). |
mv-self-destruct-value / -code / -chunk / -list | Rendered secret (monospace), its groups and the numbered list for several items. |
mv-self-destruct-fuse / -spark | Top-edge fuse burning down to the left and its glowing tip. |
mv-self-destruct-timer / -ring / -secs | Countdown ring and “Self-destructs in 24s”. |
mv-self-destruct-btn | Reveal / hold, Copy (data-state copied | error) and Destroy now buttons. |
mv-self-destruct-tombstone / -slot | What remains after destruction: title, time, hint and the destroyed slot. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-self-destruct-color | var(--mv-accent) | Fuse and ring color while time is comfortable. |
--mv-self-destruct-soon | var(--mv-warning) | Color in the last 10 seconds. |
--mv-self-destruct-final | var(--mv-danger) | Color in the last 5 seconds and during destruction. |
--mv-self-destruct-font-size | var(--mv-text-base) | Size of the monospace value. |
--mv-self-destruct-radius | var(--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.