Exclusif
Self-Destruct <mv-self-destruct>
Affichage de secret à usage unique (clé d’API, codes de récupération, mot de passe partagé, lien temporaire), montré une fois puis réellement détruit.
Référence culturelle
Mission impossible, Bruce Geller (1966, série). Chaque briefing n’existe que le temps nécessaire pour le lire, puis s’autodétruit. Dans l’interface, seule cette mécanique est conservée : un secret à usage unique est révélé à la demande, reste lisible pendant qu’une simple barre de compte à rebours se vide, puis est réellement supprimé du DOM et de la mémoire.
Fonctionnement
Affichage d’un secret à usage unique (clé d’API, codes de récupération, mot de passe partagé, lien temporaire) montré une seule fois, puis réellement détruit. Il démarre scellé derrière un cache neutre, sans rien de secret dans le DOM ; « Révéler » lance un compte à rebours, figuré par une fine barre qui se vide le long du bord supérieur et par un anneau (30 s par défaut). Un bouton Copier intégré peut raccourcir le temps restant (after-copy) et, en option, vider le presse-papiers à la fin ; le secret peut aussi être détruit quand l’onglet est masqué, ou n’être lisible que tant qu’un bouton est maintenu. À zéro, les caractères se brouillent et tombent, chaque nœud texte du secret est supprimé, la référence JS est abandonnée, et une pierre tombale (« Ce secret a été détruit · 14 h 41 ») prend sa place, avec un slot pour une action « Générer une nouvelle clé ».
| Catégorie | Affichage de données |
|---|---|
| Type | Web Component (<mv-self-destruct>) |
| Statut | stable |
| Kit | Actions destructives et secrets en sécurité |
| 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/marvelousAgent IA avec le serveur MCP Marvelous UI : install_components({ slugs: ["self-destruct"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Fichiers copiés (dépendances comprises) : tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/i18n.js, core/motion.js, components/self-destruct/self-destruct.js, components/self-destruct/self-destruct.css.
Utilisation
Démarrage rapide, le balisage minimal qui fonctionne :
<mv-self-destruct label="API key" seconds="30">
<template>mvk_live_4f9a2c7e81d0</template>
</mv-self-destruct>Balisage de référence : partez de celui-ci et personnalisez-le avec les attributs, data-* et les variables CSS :
<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 countdown to run 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>API
Attributes
| Name | Type | Par défaut | 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 (countdown bar 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). |
strings | Partial<Record<string, string | { one, other }>> | Overrides for the default text (en-US): copy, copied, copyFailed (copy button), destroyNow (destroy button), holdToView, releaseToHide (hold mode button), noteEmpty, noteSealed, noteSealedHold, noteRevealedHold (cover note, {n} seconds), oneTimeView (timer text while sealed), countdown (timer text, {n} seconds), destroying (timer text during destruction), destroyedHint (tombstone hint), items (list name, plural { one, other } with {n}), announceRevealed, announceRevealedHold, announceWarning ({label} {n}), announceCopied, announceCopiedDestroying ({n}), announceCopyFailed, announceDestroyed ({label}) (live announcements). label, reveal-label and destroyed-label stay attributes. App-wide translations: setStrings() from core/i18n.js, under the key "self-destruct". Can be set before the element is defined. |
Methods
| Name | Description |
|---|---|
reveal() | Shows the secret and starts the countdown (in hold mode: on the first press). 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): countdown bar, stage, action 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-countdown / -countdown-fill | Top-edge countdown bar: a faint track and a fill that empties toward the start edge. |
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 | Par défaut | Description |
|---|---|---|
--mv-self-destruct-color | var(--mv-accent) | Countdown bar 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 Destroy now and Copy. 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 waits on the panel during the animation (the buttons are disabled), then 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 scramble or fall: the value is masked and fades out, then the tombstone fades in; the countdown bar 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. Besides label, reveal-label and destroyed-label, the built-in texts (Copy, countdown notes, announcements) default to English and can be translated with the strings property or setStrings().