ExklusivBeta
Self-Destruct <mv-self-destruct>
Anzeige für ein einmaliges Geheimnis (API-Schlüssel, Wiederherstellungscodes, geteiltes Passwort, temporärer Link), das einmal gezeigt und dann wirklich zerstört wird. Es beginnt versiegelt hinter einer neutralen Abdeckung, ohne etwas Geheimes im DOM; „Reveal“ entzündet eine Zündschnur entlang der Oberkante und einen Countdown-Ring (standardmäßig 30 s). Ein eingebauter Copy-Button kann die verbleibende Zeit verkürzen (after-copy) und optional am Ende die Zwischenablage leeren; das Geheimnis kann auch zerstört werden, wenn der Tab verborgen wird, oder nur lesbar sein, solange ein Button gedrückt gehalten wird. Bei null verwürfeln sich die Zeichen und fallen heraus, jeder Textknoten des Geheimnisses wird entfernt und die JS-Referenz verworfen, und ein Grabstein („This secret was destroyed · 2:41 PM“) nimmt seinen Platz ein, mit einem Slot für eine Aktion „Generate a new key“.
| Kategorie | Datenanzeige |
|---|---|
| Typ | Web Component (<mv-self-destruct>) |
| Status | Beta |
| Kit | Sichere destruktive Aktionen und Geheimnisse |
| 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
- A destructive action needs a deliberate confirmation rather than a timed reveal → use Hold Button instead
- The secret must stay out of the page source: never server-render it inside the template, set the secret property from JS
Installation
node scripts/add.mjs self-destruct --out ./src/marvelousKI-Agent mit dem MCP-Server von Marvelous UI: install_components({ slugs: ["self-destruct"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Kopierte Dateien (inklusive Abhängigkeiten): 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.
Verwendung
Schnellstart mit dem kleinsten funktionierenden Markup:
<mv-self-destruct label="API key" seconds="30">
<template>mvk_live_4f9a2c7e81d0</template>
</mv-self-destruct>Referenz-Markup zum Starten und Anpassen mit Attributen, data-* und CSS-Variablen:
<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>Kulturelle Referenz
Mission: Impossible, Bruce Geller (1966, Serie). Jedes Briefing existiert nur so lange, wie man braucht, um es zu lesen, dann zerstört es sich selbst. In der UI wird ein einmaliges Geheimnis auf Anfrage angezeigt, brennt an einer sichtbaren Zündschnur ab und wird dann tatsächlich aus dem DOM und aus dem Speicher entfernt.
API
Attributes
| Name | Typ | 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 | Typ | 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.