Exclusifbêta

Self-Destruct <mv-self-destruct>

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 » allume une mèche le long du bord supérieur et un anneau de compte à rebours (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égorieAffichage de données
TypeWeb Component (<mv-self-destruct>)
Statutbêta
KitActions destructives et secrets en sécurité
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

  • 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/marvelous

Agent 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/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 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>

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, un secret à usage unique est révélé à la demande, se consume le long d’une mèche visible, puis est réellement supprimé du DOM et de la mémoire.

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.

Cette page a été traduite par IA. Signaler un problème de traduction