
Creating a Gallery UI with JSON and Filters
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.
Related Articles
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.