Base Gallery

Use .gallery for the grid and .gallery-item for each tile. The component enforces a consistent 16:10 image ratio and token-based surface styling.

<div class="gallery">
  <div class="gallery-item">
    <img src="screenshot-1.png" alt="Dashboard overview" />
  </div>
  <div class="gallery-item">
    <img src="screenshot-2.png" alt="Subscription list" />
  </div>
</div>

Caption Overlay

Add an optional .gallery-item-caption to reveal context on hover. Useful for project names, environments, or release tags.

<div class="gallery-item">
  <img src="project-shot.png" alt="Project screenshot" />
  <span class="gallery-item-caption">Project screenshot</span>
</div>

Resume-Style Project Entry

The CV uses a project entry block with a title/date row, summary text, and a gallery grid of <figure> tiles. This variant includes both screenshot figures and a Mermaid diagram figure, each with persistent <figcaption> labels.

Deal Monitoring UI

Full-stack monitoring surface with virtualized tables, URL-synced filters, and reusable component architecture.

<div class="entry-header">
  <strong>Deal Monitoring UI</strong>
  <span class="entry-date">Jan 2025 - Oct 2025</span>
</div>
<p>Full-stack monitoring surface with virtualized tables...</p>

<div class="gallery">
  <figure class="gallery-item gallery-item-figure">
    <img src="table-view.png" alt="Virtualized table view" />
    <figcaption>Virtualized table view with reusable column config</figcaption>
  </figure>

  <figure class="gallery-item gallery-item-figure gallery-item-diagram">
    <div class="mermaid">
flowchart LR
  A[Data API] --> B[Adapter]
  B --> C[View Model]
  C --> D[Virtualized Table]
    </div>
    <figcaption>Data flow diagram used in architecture notes</figcaption>
  </figure>
</div>

Gallery + Lightbox Setup

Add data-lightbox-trigger to clickable gallery figures and include a single lightbox overlay at page root. This mirrors the resume interaction pattern where clicking a screenshot opens a focused, full-size preview.

<figure
  class="gallery-item gallery-item-figure"
  data-lightbox-trigger
  data-lightbox-src="/assets/deal-monitoring-table.png"
  data-lightbox-alt="Deal Monitoring table view"
>
  <img src="/assets/deal-monitoring-table-thumb.png" alt="Deal Monitoring table view" />
  <figcaption>Click to open full-size table view</figcaption>
</figure>

<div class="gallery-lightbox-overlay" data-gallery-lightbox aria-hidden="true">
  <button class="gallery-lightbox-close" type="button" aria-label="Close lightbox">&times;</button>
  <img class="gallery-lightbox-img" src="" alt="" />
</div>
const overlay = document.querySelector('[data-gallery-lightbox]');
const lightboxImg = overlay.querySelector('.gallery-lightbox-img');

document.querySelectorAll('[data-lightbox-trigger]').forEach((node) => {
  node.addEventListener('click', () => {
    lightboxImg.src = node.dataset.lightboxSrc;
    lightboxImg.alt = node.dataset.lightboxAlt || '';
    overlay.classList.add('active');
    overlay.setAttribute('aria-hidden', 'false');
  });
});

overlay.addEventListener('click', (event) => {
  if (event.target === overlay || event.target.closest('.gallery-lightbox-close')) {
    overlay.classList.remove('active');
    overlay.setAttribute('aria-hidden', 'true');
  }
});

Paginated Lightbox Setup

For project sets, group figures with data-lightbox-group so the lightbox can paginate between items using previous/next controls.

<figure
  class="gallery-item gallery-item-figure"
  data-lightbox-trigger
  data-lightbox-group="deal-monitoring"
  data-lightbox-src="/assets/deal-table.png"
  data-lightbox-alt="Deal Monitoring table view"
>
  <img src="/assets/deal-table-thumb.png" alt="Deal Monitoring table view" />
  <figcaption>Slide 1 · Table overview</figcaption>
</figure>

<div class="gallery-lightbox-overlay" data-gallery-lightbox-paginated aria-hidden="true">
  <button class="gallery-lightbox-close" type="button" aria-label="Close lightbox">&times;</button>
  <button class="gallery-lightbox-nav gallery-lightbox-nav-prev" type="button" aria-label="Previous image">&#8249;</button>
  <img class="gallery-lightbox-img" src="" alt="" />
  <button class="gallery-lightbox-nav gallery-lightbox-nav-next" type="button" aria-label="Next image">&#8250;</button>
  <div class="gallery-lightbox-status" data-gallery-lightbox-status>1 / 1</div>
</div>
const overlay = document.querySelector('[data-gallery-lightbox-paginated]');
const img = overlay.querySelector('.gallery-lightbox-img');
const status = overlay.querySelector('[data-gallery-lightbox-status]');

const group = [...document.querySelectorAll('[data-lightbox-group="deal-monitoring"]')];
let index = 0;

function render() {
  const node = group[index];
  img.src = node.dataset.lightboxSrc;
  img.alt = node.dataset.lightboxAlt || '';
  status.textContent = `${index + 1} / ${group.length}`;
}

overlay.querySelector('.gallery-lightbox-nav-prev').addEventListener('click', () => {
  index = (index - 1 + group.length) % group.length;
  render();
});

overlay.querySelector('.gallery-lightbox-nav-next').addEventListener('click', () => {
  index = (index + 1) % group.length;
  render();
});

Placeholder Image Design

Use .gallery-placeholder inside .gallery-item to represent missing screenshots without breaking layout rhythm. This keeps spacing and visual weight consistent while teams wait for final assets.

<div class="gallery-item">
  <div class="gallery-placeholder">
    <div class="gallery-placeholder-content">
      <p class="gallery-placeholder-title">Screenshot Pending</p>
      <p class="gallery-placeholder-meta">Dashboard · Desktop</p>
    </div>
  </div>
  <span class="gallery-item-caption">Planned view</span>
</div>