G Gentelella v4.0.0

Component Playground

Gentelella ships a component playground at production/playground.html — every reusable component side-by-side with its exact HTML. Live-editable code blocks, copy buttons, scrollspy navigation. Stop guessing the markup.

Last updated May 22, 2026

The playground is Gentelella’s “single source of truth” for component markup. Every reusable component renders next to its exact HTML in a contenteditable code block. You can:

  • Read the markup to copy by hand
  • Edit the code live and see the preview update in real time
  • Copy the snippet straight to your clipboard with one click
  • Reset any block to the original after experimenting

Try the live demo →

How it’s wired

Each component example lives inside a <div class="pg-block" data-source> card with two siblings:

  • A [data-preview] div containing the actual rendered HTML
  • A label (data-source-label) for the snippet header

The page’s inline script discovers every [data-source] card on load and appends a code panel below each preview:

document.querySelectorAll('[data-source]').forEach((card) => {
  const preview = card.querySelector('[data-preview]');
  if (!preview) return;

  const originalHtml = preview.innerHTML.trim();
  const label = card.dataset.sourceLabel || '';
  const code = indent(originalHtml);

  // Build the source panel (label, Reset/Copy buttons, editable <pre>)
  const block = document.createElement('div');
  block.className = 'pg-source';
  // ... see the source for the full markup
  block.querySelector('.pg-code').textContent = code;
  card.appendChild(block);

  // Live edit, debounced 250ms
  block.querySelector('.pg-code').addEventListener('input', (e) => {
    clearTimeout(editTimer);
    editTimer = setTimeout(() => {
      preview.innerHTML = e.target.textContent;
    }, 250);
  });

  // Copy to clipboard
  block.querySelector('.pg-copy').addEventListener('click', async () => {
    await navigator.clipboard.writeText(code);
    showToast('Copied to clipboard', { variant: 'success' });
  });
});

The mechanism is intentionally simple — no diffing, no validation. The browser parses whatever HTML you type, recoverable errors get cleaned up by the parser, broken markup just doesn’t render. The reset button restores the original snapshot.

What’s in the playground

The page is organized into sections matching the _components.scss partials:

  • Buttons — primary, outline, ghost, sizes, icon-only, button groups, dropdowns
  • Badges — solid, soft, with dot, count badges
  • Cards — basic, with header/footer, with metric stat tiles, with actions menu
  • Alerts — info, success, warning, danger, dismissible
  • Forms — inputs, selects, checkboxes, radios, toggles, validation states
  • Tables — basic + striped, status cells, customer cells with avatar
  • Avatars — sizes, with initials, with badge dot
  • Tooltips & popovers
  • Modal triggers — confirm, form, custom
  • Toasts — info, success, warning, danger, action toasts

A left-rail navigation uses IntersectionObserver scrollspy to highlight whichever section is currently in view:

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const id = entry.target.id;
      links.forEach((link) => {
        link.classList.toggle('active', link.getAttribute('href') === `#${id}`);
      });
    }
  });
}, { rootMargin: '-40% 0px -40% 0px' });

sections.forEach((s) => observer.observe(s));

Adding your own component

The pattern is straightforward — wrap an existing card with data-source and data-source-label:

<div class="pg-block" id="my-component" data-source data-source-label="My Component">
  <h2 class="pg-block-title">My Component</h2>
  <p class="pg-block-description">One-sentence description.</p>

  <div class="pg-preview" data-preview>
    <!-- Your component's exact markup goes here -->
    <div class="my-component">
      <span class="my-component-label">Label</span>
      <button class="my-component-action">Action</button>
    </div>
  </div>
</div>

The page script will:

  1. Find the [data-source] card on load
  2. Capture [data-preview]’s inner HTML as the snippet
  3. Build the editable code panel below the preview
  4. Wire up live edit, reset, and copy

Add a matching nav-rail link if you want scrollspy to track it:

<a href="#my-component" class="pg-nav-link">My Component</a>

Markup helpers — for JS-rendered content

Some preview cards build their content dynamically using helpers from src/v4/markup.js:

import { statTile, statusBadge, customerCell, activityItem, visitorRow, emptyState } from '/src/v4/markup.js';

document.querySelector('[data-preview-stats]').innerHTML = `
  ${statTile({ label: 'Revenue', value: '$48,205', delta: '+12%', positive: true })}
  ${statTile({ label: 'New customers', value: '1,284', delta: '+8%', positive: true })}
`;

markup.js exports pure functions that return HTML strings — they auto-escape user input via escapeHtml() and accept html-trusted fields where you need to pass raw SVG or trusted markup. Use them when you’re stamping out repeating content (rows from a fetched list, cards from an array) — they’re a duplication killer, not a templating engine.

The playground demonstrates each helper in its dedicated card with the same Copy/Reset workflow.

A note on copy-paste fidelity

The Copy button captures whatever’s currently in the editable <pre> element. If you’ve edited the code, you copy your edits — not the original. The “Reset” button is the escape hatch.

The indent() helper that formats the initial snippet does a light reformat: collapses whitespace, splits on >< boundaries, and joins back with newlines. It’s not a full HTML pretty-printer — complex inline content (like a <button> with <svg> inside it) keeps its original layout. The goal is “good enough to read”, not pixel-perfect formatting.

If you copy a snippet and the indentation looks off in your IDE, run it through your editor’s auto-format (Prettier, IDE-native HTML formatter) — that’s where opinionated formatting belongs.

Why not Storybook?

A few honest reasons:

  • Storybook is a build system, not a doc. Adding it means a parallel build, a separate dev server, and a separate deploy. The playground is a page in the template — same Vite build, same bundle, same shell.
  • The playground stays in sync by construction. When a component’s markup changes in _components.scss, the preview re-renders with the new styling — no story file to update.
  • Live edit is rare in Storybook. Most stories are read-only. The playground’s contenteditable <pre> lets you A/B variations in seconds without rebuilding.

If you want Storybook on top of Gentelella, nothing in the template prevents it — but the playground covers ~90% of the “what does this component look like, and what’s its markup” workflow with zero extra infrastructure.

See also

  • Theming — the tokens every playground component reads from
  • Theme generator — the other on-page design tool
  • Architecture — how page-specific scripts like the playground’s inline module fit into the template