Brad Holmes web developer, designer and digital strategist.

Dad, husband and dog owner. If you’re here, you either found something I built on Google or you’re just being nosey. Either way, this is me — the work, the thinking, and the bits in between.

Brought to you by Brad Holmes

Mimeo Photos UXP Plugin Gallery UI

Creating a Gallery UI with JSON and Filters

Brad Holmes By Brad Holmes
7 min read

This was the first time I had to build a gallery UI inside a UXP plugin. I didn’t want a mess of inline logic and tangled DOM code—but I also didn’t want to overengineer it with fake modularity or abstract patterns I wouldn’t use again.

The goal was simple:

  • Load a list of photobook files from a remote JSON file
  • Let the user filter by category
  • Switch between grid and list views
  • Click a file to open it in InDesign

And do it all inside main.js, cleanly.


What I Was Trying to Build

Each file had a name, category, thumbnail, and file URL. Since UXP uses Spectrum Web Components, I could lean on familiar tags like <sp-button> and <sp-heading> to keep the UI feeling native inside InDesign—without needing to style everything from scratch.

  • Show thumbnails with labels
  • Group them by category
  • Toggle layout (grid/list)
  • Filter by category using a dropdown
  • Let the user click a file and open it

The manifest format was straightforward:

[
  {
    "name": "Hardcover 8x8",
    "category": "Hardcover",
    "thumbnail": "https://.../thumbs/hc-8x8.png",
    "fileUrl": "https://.../books/hc-8x8.indd"
  },
  ...
]

This gave me everything I needed: visual data, filter keys, and a way to load the file.

Tip: Let your JSON do the heavy lifting—build filters and labels from the data, not hardcoded values.


How I Structured It (Without Overthinking)

I kept everything in main.js, but split logic into functional blocks grouped by purpose. Nothing fancy—just clear and predictable:

let fileLibrary = [];
let currentCategory = "all";
let currentView = "grid";

Functions I used:

async function fetchFileList() { ... }
function setupToolbar() { ... }
function setViewMode(mode) { ... }
function populateGallery() { ... }
function renderCategoryGroup(...) { ... }

I didn’t invent a plugin architecture—I just made sure I could scroll through the file and know where each concern lived.


Toolbar: Filter + View Mode Controls

The dropdown and view toggles are just normal DOM elements. I bound them early so they stay out of the gallery logic. UXP supports most DOM and event patterns you’d expect—things like querySelector, addEventListener, and appendChild work just like they would in a browser, which makes it easy to keep logic clean. (Adobe UXP DOM Reference)

function setupToolbar() {
  const select = document.getElementById("categorySelect");
  if (!select) return;

  select.addEventListener("change", (e) => {
    currentCategory = e.target.value;
    populateGallery();
  });

  document.getElementById("gridView")?.addEventListener("click", () => setViewMode("grid"));
  document.getElementById("listView")?.addEventListener("click", () => setViewMode("list"));
}

This gave me scoped control: filter logic lives in the toolbar, rendering logic stays in the gallery.


Rendering the Gallery

Once I had the toolbar and state sorted, the next step was actually drawing the gallery itself. I didn’t want to hard-code any categories or layouts—everything comes from the JSON.

I clear the container, filter the files based on category, and then group them for display:

function populateGallery() {
  const gallery = document.getElementById("gallery");
  if (!gallery) return;

  gallery.innerHTML = "";
  gallery.className = currentView;

  const filtered = currentCategory === "all"
    ? fileLibrary
    : fileLibrary.filter(file => file.category === currentCategory);

  const grouped = groupFilesByCategory(filtered);

  Object.entries(grouped).forEach(([category, files]) => {
    renderCategoryGroup(category, files, gallery);
  });
}

Each group is collapsible, and each file gets rendered with its thumbnail, label, and a click handler.

function renderCategoryGroup(category, files, gallery) {
  const group = document.createElement("div");
  group.className = `category-group ${currentView}`;

  const header = document.createElement("h5");
  header.textContent = category;
  header.addEventListener("click", () => {
    group.classList.toggle("collapsed");
  });

  const wrapper = document.createElement("div");
  wrapper.className = "category-files";

  files.forEach(file => {
    const container = document.createElement("div");
    container.className = `file-container ${currentView}`;

    const img = document.createElement("img");
    img.src = file.thumbnail;
    img.alt = file.name;

    const label = document.createElement("p");
    label.textContent = file.name;

    img.addEventListener("click", async () => {
      img.style.opacity = 0.5;
      label.textContent = "Loading...";
      await openPhotobookFile(file.fileUrl);
      img.style.opacity = 1;
      label.textContent = file.name;
    });

    container.appendChild(img);
    container.appendChild(label);
    wrapper.appendChild(container);
  });

  group.appendChild(header);
  group.appendChild(wrapper);
  gallery.appendChild(group);
}

That was enough to make it feel like a working UI.

Tip: Group your functions by purpose, not perfection. Clean beats clever every time.

The loading interaction is simple but helpful. When a user clicks an item, the image dims and the label changes to ‘Loading…’. Once the file is open, everything resets visually. It’s a small thing, but it gives feedback without needing a modal or spinner.

If the file fails to load, I’ve also got a fallback error handler inside openPhotobookFile()—it logs to the console and shows a message in the UI. Nothing fancy, just enough to catch issues if the file URL is broken or the network hangs.

This part of the plugin was the turning point. Once the gallery was loading and clickable, it unlocked a lot of future possibilities. It meant I could extend things later—add tags, search, maybe even live previews—without rethinking the structure.

If you wanted to take it further, here are a few easy wins:

  • Dynamic category list: Instead of hardcoding category options, you could extract them from the JSON file and build the dropdown on load.
  • Search filter: Add a text input that filters files by name as you type.
  • Lazy load or pagination: If your file list grows large, render them in chunks or use an intersection observer.
  • Preview panel: Clicking a file could open a side panel or modal with more details, rather than jumping straight to opening the .indd file.

But even without all that, just getting the core gallery working gave the plugin a proper foundation.

Final Thoughts

If you’re working inside a UXP plugin, you don’t need complexity to make things usable. This gallery wasn’t built to impress—it was built to work, to be expanded later, and to give users something immediate and useful.

The structure is simple, the logic is scoped, and every part serves a clear purpose. That’s enough to carry the rest of the plugin.

Enforcing Page Count Rules in InDesign UXP →

Related Articles

Brad Holmes

Brad Holmes

Web developer, designer and digital strategist.

Brad Holmes is a full-stack developer and designer based in the UK with over 20 years’ experience building websites and web apps. He’s worked with agencies, product teams, and clients directly to deliver everything from brand sites to complex systems—always with a focus on UX that makes sense, architecture that scales, and content strategies that actually convert.

Thanks Brad, I found this really helpful
TOP