Content & Media

Free Spoiler Reveal Widget

Hide spoilers behind a blur that visitors click to reveal.

The Spoiler Reveal widget blurs any piece of text — plot spoilers, puzzle answers, giveaway codes — until a visitor clicks to reveal it, with a Hide button to tuck it away again. It is perfect for blogs, review sites, and course creators who want to keep sensitive content out of sight by default. Drop one script tag where the hidden text should sit and set your text and label with simple data attributes.

Live demo

Embed this widget

Paste either snippet into your page — a Custom HTML block on WordPress, Shopify, or Squarespace works too. Both snippets update live as you customize, so what you copy is exactly what you configured.

Hosted (recommended)html
<script src="https://hayneslab.dev/widgets/spoiler-reveal.js" data-hl-text="The answer to the riddle: an echo. You speak to it and it answers back, yet it never speaks first." data-hl-label="Reveal answer" async></script>
<!-- Free Spoiler Reveal widget by haynes lab — https://hayneslab.dev/tools/widgets/spoiler-reveal -->
Inline (self-contained)html
<!-- Free Spoiler Reveal widget by haynes lab — https://hayneslab.dev/tools/widgets/spoiler-reveal -->
<script data-hl-text="The answer to the riddle: an echo. You speak to it and it answers back, yet it never speaks first." data-hl-label="Reveal answer">
/*!
 * haynes lab — Spoiler Reveal widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/spoiler-reveal
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-spoiler-reveal';
  var STYLE_ID = 'hlw-spoiler-reveal-styles';

  function findScript() {
    return document.currentScript ||
      document.querySelector('script[src*="widgets/spoiler-reveal.js"]');
  }

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { position: relative; display: inline-block; width: 100%; max-width: 720px;' +
      ' font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
      ' font-size: 16px; line-height: 1.6; color: #1f2937; }' +
      '.' + PREFIX + '__content { display: block; padding: 18px 20px; background: #ffffff;' +
      ' border: 1px solid #e5e7eb; border-radius: 10px; filter: blur(var(--hlw-blur, 8px));' +
      ' transition: filter 0.35s ease; user-select: none; }' +
      '.' + PREFIX + '--revealed .' + PREFIX + '__content { filter: none; user-select: auto; }' +
      '.' + PREFIX + '__overlay { position: absolute; inset: 0; display: flex;' +
      ' align-items: center; justify-content: center; }' +
      '.' + PREFIX + '__reveal { display: inline-flex; align-items: center; gap: 6px;' +
      ' padding: 9px 18px; font: inherit; font-size: 14px; font-weight: 600; color: #ffffff;' +
      ' background: #1f2937; border: 0; border-radius: 999px; cursor: pointer;' +
      ' box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18); }' +
      '.' + PREFIX + '__reveal:hover { background: #111827; }' +
      '.' + PREFIX + '__reveal:focus-visible { outline: 2px solid #1f2937; outline-offset: 2px; }' +
      '.' + PREFIX + '__hide { display: none; margin-top: 8px; padding: 4px 10px; font: inherit;' +
      ' font-size: 13px; color: #6b7280; background: none; border: 1px solid #e5e7eb;' +
      ' border-radius: 999px; cursor: pointer; }' +
      '.' + PREFIX + '--revealed .' + PREFIX + '__hide { display: inline-block; }' +
      '.' + PREFIX + '__hide:hover { color: #374151; border-color: #d1d5db; }' +
      '.' + PREFIX + '__hide:focus-visible { outline: 2px solid #1f2937; outline-offset: 2px; }' +
      '.' + PREFIX + '__credit { position: absolute; right: 8px; bottom: 5px; font-size: 11px;' +
      ' display: inline-flex; align-items: center; gap: 4px; color: #9ca3af; text-decoration: none;' +
      ' opacity: 0; pointer-events: none; }' +
      '.' + PREFIX + '--revealed .' + PREFIX + '__credit { opacity: 1; pointer-events: auto; }' +
      '.' + PREFIX + '__credit svg { display: block; }' +
      '.' + PREFIX + '__credit:hover { color: #6b7280; text-decoration: underline; }';
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function el(tag, className, text) {
    var node = document.createElement(tag);
    if (className) node.className = className;
    if (text != null) node.textContent = text;
    return node;
  }

  function logoSvg() {
    var svgNS = 'http://www.w3.org/2000/svg';
    var svg = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('viewBox', '0 0 32 32');
    svg.setAttribute('width', '13');
    svg.setAttribute('height', '13');
    svg.setAttribute('aria-hidden', 'true');
    svg.setAttribute('focusable', 'false');
    function circle(cx, cy, r, attrs) {
      var c = document.createElementNS(svgNS, 'circle');
      c.setAttribute('cx', cx);
      c.setAttribute('cy', cy);
      c.setAttribute('r', r);
      for (var key in attrs) c.setAttribute(key, attrs[key]);
      svg.appendChild(c);
    }
    circle(16, 16, 16, { fill: '#0f0a0a' });
    circle(16, 16, 12, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.3 });
    circle(16, 16, 9, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.5 });
    circle(16, 16, 6, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1.2, opacity: 0.9 });
    circle(16, 16, 3, { fill: '#06b6d4' });
    circle(24.5, 7.5, 2, { fill: '#f97316' });
    circle(7, 16, 1.8, { fill: '#f97316' });
    circle(20, 20, 1.5, { fill: '#f97316' });
    return svg;
  }

  function build(script) {
    var text = script.getAttribute('data-hl-text');
    if (!text) {
      console.warn('[haynes lab spoiler-reveal] Missing required data-hl-text attribute. Nothing rendered.');
      return;
    }
    var label = script.getAttribute('data-hl-label') || 'Click to reveal';
    var blur = parseFloat(script.getAttribute('data-hl-blur'));
    if (isNaN(blur) || blur < 0) blur = 8;

    injectStyles();

    var root = el('div', PREFIX);
    root.style.setProperty('--hlw-blur', blur + 'px');

    var content = el('span', PREFIX + '__content', text);
    root.appendChild(content);

    var overlay = el('div', PREFIX + '__overlay');
    var reveal = el('button', PREFIX + '__reveal', label);
    reveal.type = 'button';
    reveal.setAttribute('aria-expanded', 'false');
    overlay.appendChild(reveal);
    root.appendChild(overlay);

    var hide = el('button', PREFIX + '__hide', 'Hide');
    hide.type = 'button';
    root.appendChild(hide);

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/spoiler-reveal';
    credit.target = '_blank';
    credit.rel = 'noopener';
    root.appendChild(credit);

    function setRevealed(on) {
      root.classList.toggle(PREFIX + '--revealed', on);
      overlay.style.display = on ? 'none' : '';
      reveal.setAttribute('aria-expanded', on ? 'true' : 'false');
      if (on) {
        hide.focus();
      } else {
        reveal.focus();
      }
    }

    reveal.addEventListener('click', function () { setRevealed(true); });
    hide.addEventListener('click', function () { setRevealed(false); });

    if (script.parentNode && script.parentNode.nodeName !== 'HEAD') {
      script.insertAdjacentElement('afterend', root);
    } else {
      document.body.appendChild(root);
    }
  }

  function init() {
    var script = findScript();
    if (!script) {
      console.warn('[haynes lab spoiler-reveal] Could not locate its own script tag. Nothing rendered.');
      return;
    }
    build(script);
  }

  if (document.body) {
    init();
  } else {
    document.addEventListener('DOMContentLoaded', init);
  }
})();

</script>

Configuration

Every option is an attribute on the script tag — the same attributes the customizer above exposes. All attributes are optional unless marked required — defaults apply when omitted.

AttributeDefaultDescription
data-hl-text(required)The text content that stays blurred until the visitor reveals it.
data-hl-label"Click to reveal"The text on the button that reveals the hidden content.
data-hl-blur"8"How strong the blur effect is, in pixels — higher values hide the text more thoroughly.

Frequently asked questions

How do I install the spoiler reveal?

Paste a single script tag with a data-hl-text attribute wherever you want the hidden content to appear in your page. The widget renders inline right where the tag sits, blurred behind a Click to reveal button.

Can I change the button text or blur strength?

Yes. Set data-hl-label to change the reveal button text (defaults to "Click to reveal") and data-hl-blur to adjust how heavily the text is blurred, in pixels (defaults to 8).

Does it work on WordPress, Shopify, or Squarespace?

Yes — it is a single vanilla JavaScript file with no dependencies, so it works on WordPress, Shopify, Squarespace, Wix, or any plain HTML page that lets you paste a script tag.

More free widgets

Need a custom widget?

We build custom widgets, integrations, and whole products. Tell us what your site needs and we'll scope it.

Get in Touch