Hold Button <mv-hold-button>
Bouton « Maintenir pour confirmer » pour les actions destructrices : tant qu’il est pressé, un liquide monte à l’intérieur (ou arrive par le côté) et inverse le libellé là où il le recouvre, relâcher trop tôt le fait redescendre, et un maintien complet se conclut par une coche, un libellé de fin et mv-confirm.
| Catégorie | Micro-interactions |
|---|---|
| Type | Web Component (<mv-hold-button>) |
| Statut | stable |
| Keywords | hold, long-press, confirm, destructive, delete, progress, safety, liquid |
When to use
- A destructive action such as deleting an account or project should require a deliberate press and hold
- An important but non-destructive action should be protected from accidental clicks with an accent hold gauge
Avoid when
- The user must read consequences or type a confirmation before proceeding → use Dialog instead
- The action stops a long-running process and users should see what they keep before stopping → use Salvage Stop instead
- The action is frequent and harmless; forcing a hold slows users down for no benefit → use Button instead
Installation
node scripts/add.mjs hold-button --out ./src/marvelousAgent IA avec le serveur MCP de Marvelous UI : install_components({ slugs: ["hold-button"], 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/hold-button/hold-button.js, components/hold-button/hold-button.css.
Utilisation
Démarrage rapide, le balisage minimal qui fonctionne :
<mv-hold-button done-label="Project deleted">Hold to delete</mv-hold-button>Balisage de référence : partez de celui-ci et personnalisez-le avec les attributs, data-* et les variables CSS :
<div style="display:grid;gap:1.25rem;width:100%;max-width:440px">
<div style="display:grid;gap:.9rem;padding:1.1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);background:var(--mv-surface)">
<div style="display:grid;gap:.25rem">
<strong style="font-size:.95rem">Delete “Lumen website redesign”</strong>
<span style="font-size:.82rem;color:var(--mv-fg-muted)">All 48 files and their history will be permanently deleted.</span>
</div>
<div style="display:flex;gap:.6rem;align-items:center;flex-wrap:wrap">
<mv-hold-button duration="1600" done-label="Project deleted" id="hb-demo-delete">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M10 11v6M14 11v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4h6v3"/></svg>
Hold to delete
</mv-hold-button>
<span id="hb-demo-log" style="font-size:.8rem;color:var(--mv-fg-subtle)">Let go early to cancel.</span>
</div>
</div>
<div style="display:flex;gap:.75rem;align-items:center;justify-content:center;flex-wrap:wrap">
<mv-hold-button data-variant="accent" data-fill="sweep" duration="1000" done-label="Published" style="--mv-hold-button-radius:var(--mv-radius-full)">Hold to publish</mv-hold-button>
</div>
</div>
<script type="module">
const el = document.getElementById("hb-demo-delete");
el?.addEventListener("mv-confirm", () => {
const log = document.getElementById("hb-demo-log");
if (log) log.textContent = "mv-confirm received at " + new Date().toLocaleTimeString("en-US");
});
</script>API
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
duration | number | 1500 | Required hold time (ms). |
done-label | string | Confirmed | Label (and announcement) once confirmed. |
hint | string | Press and hold to confirm | Instruction: accessible description, and the bubble shown after a tap that did not hold. |
reset | number | 2400 | Return to the initial state after confirmation (ms, 0 = stays confirmed). |
disabled | boolean | Disables the button. | |
data-variant | accent | (danger) | Accent color instead of danger (non-destructive actions). |
data-fill | sweep | (rise) | The liquid fills from the start edge instead of rising from the bottom. |
data-state | idle | holding | rewinding | armed | done | Set by the component. |
Properties
| Name | Type | Description |
|---|---|---|
progress | number | Progress 0..1 (read-only). |
strings | { hint?, done?, again? } | Overrides for the default texts (translation). The hint and done-label attributes win; again = "Press again to confirm". |
Methods
| Name | Description |
|---|---|
resetState({ instant }) | Returns to the initial state (rewinds, or instantly). |
Events
| Name | Description |
|---|---|
mv-confirm | The hold went all the way. |
Content structure
| Name | Description |
|---|---|
(content) | Label, SVG icon included. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-hold-button-color | var(--mv-danger) | Fill and text color. |
--mv-hold-button-fg | white | Text on the fill. |
--mv-hold-button-radius | var(--mv-radius-md) | Radius. |
Accessibility
A real <button> described by the instruction (aria-describedby). Keyboard: hold Space or Enter (key repeat is ignored, releasing drains the liquid, Escape or losing focus cancels). A tap never confirms: it shows the instruction and nudges the button. A click with no press behind it (screen readers, switch access) arms the button (“Press again to confirm”, announced, 5 s) and a second one confirms, so the action never depends on a timed gesture alone. Confirmation is announced by a role=status region and the button becomes aria-disabled while confirmed. Touch: no context menu during the press, scrolling cancels, a short vibration on completion where supported. Reduced motion: the level stays (it carries information), the rolling surface, the nudge and the slides go away. Forced colors: the liquid is drawn in Highlight with HighlightText on it.