Reveal Mask <mv-reveal-mask>
Círculo suave que segue um ponteiro preciso e revela uma segunda camada, com um rastro opcional que se apaga; toques na tela, o foco do teclado e revealed o abrem como uma íris, e com movimento reduzido ele aparece com um fade.
| Categoria | Cursores |
|---|---|
| Tipo | Web Component (<mv-reveal-mask>) |
| Status | estável |
| Keywords | mask, reveal, spotlight, flashlight, hover, cursor, trail, before-after |
When to use
- A hover circle should uncover an alternate layer: a punchline, a second image or a day version of a scene
- A flashlight or paint-trail reveal suits a playful hero or an easter egg that still works on touch and keyboard
Avoid when
- Two images must be compared precisely with a keyboard-accessible divider → use Image Compare instead
- Content should be magnified under the pointer rather than swapped → use Lens instead
- Essential information lives only in the hidden layer; meaning must stay in the visible layer
Instalação
node scripts/add.mjs reveal-mask --out ./src/marvelousAgente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["reveal-mask"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Arquivos copiados (dependências incluídas): tokens/tokens.css, core/base.css, core/element.js, core/focus.js, core/motion.js, components/reveal-mask/reveal-mask.js, components/reveal-mask/reveal-mask.css.
Uso
Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis CSS:
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:1rem;width:100%">
<div style="display:grid;gap:.6rem;align-content:start">
<mv-reveal-mask id="mv-reveal-demo" size="120" softness="3" trail="8" aria-label="Manifesto" style="border-radius:var(--mv-radius-xl);border:1px solid var(--mv-border);background:var(--mv-surface);min-height:260px">
<div style="display:grid;align-content:center;gap:.75rem;min-height:260px;padding:2rem">
<p style="margin:0;font:600 .72rem/1 var(--mv-font-sans);letter-spacing:.18em;text-transform:uppercase;color:var(--mv-fg-subtle)">Manifesto</p>
<p style="margin:0;font:800 clamp(1.6rem,3.2vw,2.4rem)/1.08 var(--mv-font-sans);letter-spacing:-.035em;color:var(--mv-fg-muted)">We ship sites that are <span style="color:var(--mv-fg)">fast</span>, clean and tidy.</p>
</div>
<div data-reveal style="display:grid;align-content:center;gap:.75rem;padding:2rem;background:var(--mv-accent);color:var(--mv-fg-on-accent)">
<p style="margin:0;font:600 .72rem/1 var(--mv-font-sans);letter-spacing:.18em;text-transform:uppercase;opacity:.8">Between the lines</p>
<p style="margin:0;font:800 clamp(1.6rem,3.2vw,2.4rem)/1.08 var(--mv-font-sans);letter-spacing:-.035em">And we hide a surprise in every pixel.</p>
</div>
</mv-reveal-mask>
<p style="margin:0;font-size:.8rem;color:var(--mv-fg-muted)">Hover to paint with a fading trail. Tap, or focus with Tab, to open it all.</p>
</div>
<div style="display:grid;gap:.6rem;align-content:start">
<mv-reveal-mask id="mv-reveal-demo-night" size="110" softness="44" aria-label="The Alps, by night and by day" style="border-radius:var(--mv-radius-xl);min-height:260px;background:linear-gradient(180deg,oklch(0.18 0.04 270),oklch(0.26 0.06 290))">
<div style="position:relative;min-height:260px;display:grid;align-content:end;padding:1.25rem;color:oklch(0.92 0.02 270)">
<div aria-hidden="true" style="position:absolute;inset:0;background:radial-gradient(1.5px 1.5px at 20% 25%,white,transparent),radial-gradient(1.5px 1.5px at 70% 15%,white,transparent),radial-gradient(1px 1px at 45% 40%,white,transparent),radial-gradient(1.5px 1.5px at 85% 45%,white,transparent),radial-gradient(1px 1px at 30% 60%,white,transparent);opacity:.8"></div>
<div aria-hidden="true" style="position:absolute;left:0;right:0;bottom:0;height:45%;background:oklch(0.14 0.03 280);clip-path:polygon(0 60%,12% 35%,20% 55%,34% 20%,46% 50%,58% 30%,70% 55%,82% 25%,100% 50%,100% 100%,0 100%)"></div>
<strong style="position:relative;font-size:1rem">The Alps by night</strong>
<span style="position:relative;font-size:.8rem;opacity:.75">Find the daylight with the flashlight.</span>
</div>
<div data-reveal style="display:grid;align-content:end;padding:1.25rem;color:oklch(0.22 0.05 250);background:linear-gradient(180deg,oklch(0.85 0.09 220),oklch(0.93 0.06 90))">
<div aria-hidden="true" style="position:absolute;right:18%;top:18%;width:56px;height:56px;border-radius:50%;background:oklch(0.92 0.14 95);box-shadow:0 0 40px oklch(0.92 0.14 95)"></div>
<div aria-hidden="true" style="position:absolute;left:0;right:0;bottom:0;height:45%;background:oklch(0.62 0.12 150);clip-path:polygon(0 60%,12% 35%,20% 55%,34% 20%,46% 50%,58% 30%,70% 55%,82% 25%,100% 50%,100% 100%,0 100%)"></div>
<strong style="position:relative;font-size:1rem">The Alps by day</strong>
<span style="position:relative;font-size:.8rem;opacity:.8">Bright sun over the ridges.</span>
</div>
</mv-reveal-mask>
<button class="mv-button" data-variant="outline" id="mv-reveal-demo-toggle" aria-pressed="false" style="justify-self:start">Reveal all</button>
</div>
</div>
<script type="module">
const btn = document.getElementById("mv-reveal-demo-toggle");
const mask = document.getElementById("mv-reveal-demo-night");
btn?.addEventListener("click", () => {
const on = !mask.hasAttribute("revealed");
mask.toggleAttribute("revealed", on);
btn.setAttribute("aria-pressed", String(on));
btn.textContent = on ? "Back to night" : "Reveal all";
});
</script>API
Attributes
| Name | Tipo | Default | Description |
|---|---|---|---|
size | number | 160 | Circle radius on hover (px). |
idle | number | 0 | Radius when not hovered (px): a small permanent peephole if > 0 (pointer mode only). |
softness | number | 2 | Soft edge width (px; large = flashlight effect). |
smoothing | 0.02..0.9 | 0.18 | Fraction of the distance covered each frame (smaller = more lag). |
trail | 0..12 | 0 | Number of fading circles left behind the pointer (a soft wake that fades out in half a second). |
revealed | boolean | Fully opens the hidden layer (iris from the current center). | |
disabled | boolean | Disables and hides the hidden layer. | |
data-active | (state) | Set by the component while a mouse or hovering pen is over it. | |
data-open | (state) | Set by the component after a tap opened the layer; the next tap closes it. | |
data-still | (state) | Set when the circle does not track (reduced motion, or no fine hovering pointer): the layer fades in instead. |
Events
| Name | Description |
|---|---|
mv-toggle | A tap opened or closed the layer. detail: { open }. |
Content structure
| Name | Description |
|---|---|
(first content) | Visible layer. |
[data-reveal] | Revealed layer, laid on top (inset: 0). Stays in the accessibility tree; add aria-hidden="true" yourself only if it is a pure decorative duplicate. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-reveal-mask-r | Current radius (registered, animatable property). | |
--mv-reveal-mask-duration | var(--mv-duration-slower) | Circle opening duration. |
Accessibility
Both layers stay in the accessibility tree, so screen readers read the visible text and the hidden text (give the element an aria-label when the pair needs a name). The pointer effect runs only under (hover: hover) and (pointer: fine), and the system cursor is never hidden. Keyboard: when nothing inside is focusable the element becomes a tab stop (role group), and keyboard focus, on it or on a link inside the hidden layer, opens the layer fully; Tab away closes it. Touch: a tap toggles the whole layer, no press-and-drag needed. Reduced motion (or no fine pointer): no tracking and no trail, the layer fades in on hover, tap or focus. Forced colors: the focus ring uses Highlight. Still offer revealed through a visible button when the second layer matters.