Content & Media

Free FAQ Accordion Widget

Answer common questions without a wall of text.

A collapsible FAQ section that keeps your page tidy while giving visitors the answers they came for. Questions and answers are passed in as JSON, and the first item can start open. Keyboard-accessible and mobile-friendly out of the box.

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/faq-accordion.js" data-hl-config='[{"question":"Do you offer free estimates?","answer":"Yes — every project starts with a free, no-obligation estimate. Most quotes are delivered within one business day."},{"question":"What areas do you serve?","answer":"We serve the greater Charlotte metro area, including Concord, Gastonia, Huntersville, and Rock Hill."},{"question":"Are you licensed and insured?","answer":"Fully licensed and insured in North and South Carolina, with certificates available on request."}]' async></script>
<!-- Free FAQ Accordion widget by haynes lab — https://hayneslab.dev/tools/widgets/faq-accordion -->
Inline (self-contained)html
<!-- Free FAQ Accordion widget by haynes lab — https://hayneslab.dev/tools/widgets/faq-accordion -->
<script data-hl-config='[{"question":"Do you offer free estimates?","answer":"Yes — every project starts with a free, no-obligation estimate. Most quotes are delivered within one business day."},{"question":"What areas do you serve?","answer":"We serve the greater Charlotte metro area, including Concord, Gastonia, Huntersville, and Rock Hill."},{"question":"Are you licensed and insured?","answer":"Fully licensed and insured in North and South Carolina, with certificates available on request."}]'>
/*!
 * haynes lab — FAQ Accordion widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/faq-accordion
 */
(function () {
  'use strict';

  var ROOT_CLASS = 'hlw-faq-accordion';
  var STYLE_ID = 'hlw-faq-accordion-styles';
  var instanceCount = 0;

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = [
      '.' + ROOT_CLASS + ', .' + ROOT_CLASS + ' * { box-sizing: border-box; margin: 0; padding: 0; }',
      '.' + ROOT_CLASS + ' { font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.5; color: #1f2937; max-width: 720px; width: 100%; }',
      '.' + ROOT_CLASS + '__title { font-size: 22px; font-weight: 700; line-height: 1.3; color: #111827; margin-bottom: 16px; }',
      '.' + ROOT_CLASS + '__item { border: 1px solid #e5e7eb; border-radius: 10px; background: #ffffff; margin-bottom: 10px; overflow: hidden; }',
      '.' + ROOT_CLASS + '__question { display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; padding: 14px 16px; border: 0; background: transparent; font: inherit; font-weight: 600; font-size: 15px; color: #111827; text-align: left; cursor: pointer; }',
      '.' + ROOT_CLASS + '__question:hover { color: var(--hlw-accent, #f97316); }',
      '.' + ROOT_CLASS + '__question:focus-visible { outline: 2px solid var(--hlw-accent, #f97316); outline-offset: -2px; }',
      '.' + ROOT_CLASS + '__icon { flex: 0 0 auto; width: 18px; height: 18px; color: var(--hlw-accent, #f97316); transition: transform 0.2s ease; }',
      '.' + ROOT_CLASS + '__question[aria-expanded="true"] .' + ROOT_CLASS + '__icon { transform: rotate(45deg); }',
      '.' + ROOT_CLASS + '__answer { display: none; padding: 0 16px 16px; font-size: 15px; color: #4b5563; }',
      '.' + ROOT_CLASS + '__answer--open { display: block; }',
      '.' + ROOT_CLASS + '__credit { display: block; text-align: right; font-size: 11px; margin-top: 6px; }',
      '.' + ROOT_CLASS + '__credit a { display: inline-flex; align-items: center; gap: 4px; color: #9ca3af; text-decoration: none; }',
      '.' + ROOT_CLASS + '__credit a svg { display: block; }',
      '.' + ROOT_CLASS + '__credit a:hover { text-decoration: underline; }'
    ].join('\n');
    document.head.appendChild(style);
  }

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

  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 parseItems(raw) {
    if (!raw) return null;
    try {
      var data = JSON.parse(raw);
      if (!Array.isArray(data) || data.length === 0) return null;
      var items = [];
      for (var i = 0; i < data.length; i++) {
        var entry = data[i] || {};
        if (entry.question && entry.answer) {
          items.push({ question: String(entry.question), answer: String(entry.answer) });
        }
      }
      return items.length ? items : null;
    } catch (err) {
      console.warn('[haynes lab faq-accordion] data-hl-config is not valid JSON:', err);
      return null;
    }
  }

  function init() {
    var script = findScript();
    if (!script) return;
    var attr = function (name) { return script.getAttribute(name); };

    var items = parseItems(attr('data-hl-config'));
    if (!items) {
      console.warn('[haynes lab faq-accordion] Missing or invalid required attribute data-hl-config (JSON array of {"question","answer"}). Nothing rendered.');
      return;
    }

    var title = attr('data-hl-title');
    if (title === null) title = 'Frequently asked questions';
    var openFirst = attr('data-hl-open-first') !== 'false';
    var accent = attr('data-hl-accent') || '#f97316';

    injectStyles();

    var root = document.createElement('div');
    root.className = ROOT_CLASS;
    root.style.setProperty('--hlw-accent', accent);
    instanceCount += 1;
    var idBase = ROOT_CLASS + '-' + instanceCount;

    if (title !== '') {
      var h = document.createElement('h2');
      h.className = ROOT_CLASS + '__title';
      h.textContent = title;
      root.appendChild(h);
    }

    items.forEach(function (item, i) {
      var wrap = document.createElement('div');
      wrap.className = ROOT_CLASS + '__item';

      var open = openFirst && i === 0;
      var answerId = idBase + '-answer-' + i;

      var btn = document.createElement('button');
      btn.type = 'button';
      btn.className = ROOT_CLASS + '__question';
      btn.setAttribute('aria-expanded', open ? 'true' : 'false');
      btn.setAttribute('aria-controls', answerId);
      btn.setAttribute('aria-label', item.question);

      var label = document.createElement('span');
      label.textContent = item.question;
      btn.appendChild(label);

      var icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
      icon.setAttribute('class', ROOT_CLASS + '__icon');
      icon.setAttribute('viewBox', '0 0 24 24');
      icon.setAttribute('fill', 'none');
      icon.setAttribute('aria-hidden', 'true');
      var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
      path.setAttribute('d', 'M12 5v14M5 12h14');
      path.setAttribute('stroke', 'currentColor');
      path.setAttribute('stroke-width', '2.5');
      path.setAttribute('stroke-linecap', 'round');
      icon.appendChild(path);
      btn.appendChild(icon);

      var answer = document.createElement('div');
      answer.className = ROOT_CLASS + '__answer' + (open ? ' ' + ROOT_CLASS + '__answer--open' : '');
      answer.id = answerId;
      answer.setAttribute('role', 'region');
      answer.textContent = item.answer;

      btn.addEventListener('click', function () {
        var expanded = btn.getAttribute('aria-expanded') === 'true';
        btn.setAttribute('aria-expanded', expanded ? 'false' : 'true');
        answer.classList.toggle(ROOT_CLASS + '__answer--open', !expanded);
      });

      wrap.appendChild(btn);
      wrap.appendChild(answer);
      root.appendChild(wrap);
    });

    var credit = document.createElement('span');
    credit.className = ROOT_CLASS + '__credit';
    var link = document.createElement('a');
    link.href = 'https://hayneslab.dev/tools/widgets/faq-accordion';
    link.target = '_blank';
    link.rel = 'noopener';
    link.appendChild(logoSvg());
    link.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.appendChild(link);
    root.appendChild(credit);

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

  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-config(required)JSON array of questions and answers: [{"question": "...", "answer": "..."}]. Answers are plain text (no HTML).
data-hl-title"Frequently asked questions"Heading displayed above the accordion. Set to "" to render no heading.
data-hl-open-first"true"When "true", the first question starts expanded so visitors see the format immediately.
data-hl-accent"#f97316"Any CSS color used for the expand icon and question hover state.

Frequently asked questions

How do I install the FAQ accordion?

Paste the embed snippet where you want the FAQ to appear and put your questions in the data-hl-config attribute as a JSON array of {question, answer} objects. The accordion renders itself inline at that spot.

How do I add or reorder questions?

Edit the JSON in data-hl-config. Entries appear in array order, so moving an object up or down moves the question. Set data-hl-open-first to "false" if you want every item to start collapsed.

Does it work on WordPress, Shopify, or Squarespace?

Yes. It is a single dependency-free vanilla JS file with scoped styles and keyboard support built in. Paste it into any Custom HTML block or directly into your page.

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