
Structuring a UXP Plugin
How to build clean, maintainable plugins inside a single main.js
file
When you’re building UXP plugins for InDesign, you don’t get a module system, bundler, or modern JavaScript tooling. You get one main.js
, one index.html
, and Adobe’s APIs.
But that doesn’t mean you’re stuck with spaghetti. This post walks through how I structure real, production-grade UXP plugins—like the photobook builder for Mimeo Photos—inside a single file. It’s flat by necessity, but clean by design.
This isn’t about abstraction—it’s about practical structure. We’re not breaking down the code line-by-line. We’re talking about how to organise real, working code inside the UXP sandbox so it scales, stays readable, and actually works.
Getting Started: One Tool, One Plugin
Before you worry about structure, you need to actually have something to structure. If you haven’t already:
- Install the Adobe UXP Developer Tool from the Adobe Developer Console
- Use it to create a new plugin project – this gives you a basic
manifest.json
,index.html
, andmain.js
- Launch it inside InDesign using the Dev Tool’s built-in loader
From there, you’ll be looking at a blank canvas—and that’s where structure starts to matter.

My UXP Philosophy: Flat but Scoped
Adobe’s UXP runtime doesn’t support imports, modules, or component files. Everything has to live inside one main.js
. So instead of overengineering it, I use scoped structure, grouped by responsibility, like this:
- Imports and Global State – top-level constants, plugin state, feature flags
- File and Data Handling – anything that loads or processes data from outside (e.g. manifest JSON, temp files)
- UI Setup – binds DOM events, controls views and user input
- Page Controls – adds/removes pages and updates layout UI
- Layout Rules & Watcher – applies variants, enforces limits, watches for native user actions
- Onboarding Logic – handles the setup wizard, preset detection, and local flag tracking
- Modal and UI Helpers – simple, self-contained utility blocks
- Plugin Init – a single
DOMContentLoaded
block at the end that runs everything in order
This gives the benefits of modularity, without splitting files. Anyone reading the top of the file knows where to look for each concern.
// ---------------------------
// 1. Imports and Global State
// ---------------------------
const { app } = require("indesign");
const fs = require("uxp").storage.localFileSystem;
...
let fileLibrary = [];
let activeFileName = null;
...
// ---------------------------
// 2. File + Data Functions
// ---------------------------
async function fetchFileList() { ... }
async function openPhotobookFile(url) { ... }
...
// ---------------------------
// 3. UI + Toolbar Setup
// ---------------------------
function setupToolbar() { ... }
function setViewMode(mode) { ... }
...
// ---------------------------
// 4. Gallery + DOM Binding
// ---------------------------
function populateGallery() { ... }
function setupGallery() { ... }
...
// ---------------------------
// 5. Page Controls
// ---------------------------
async function addPage() { ... }
async function removePage() { ... }
...
// ---------------------------
// 6. Layout Enforcement + Watcher
// ---------------------------
function startActiveDocumentWatcher() { ... }
function applyOverflowUpdate(variant) { ... }
...
// ---------------------------
// 7. Onboarding + Wizard Logic
// ---------------------------
function checkWizardState() { ... }
function setupWizardUI() { ... }
...
// ---------------------------
// 8. Utility + UI Helpers
// ---------------------------
function showModal(msg) { ... }
function collapseBlock(id) { ... }
...
// ---------------------------
// 9. Init Sequence
// ---------------------------
document.addEventListener("DOMContentLoaded", async () => {
await fetchFileList();
setupToolbar();
setupGallery();
populateGallery();
setupWizardUI();
startActiveDocumentWatcher();
await openWelcomeDocument();
});
Each section has a job. You can collapse and scan them like modules—even in one file.
Final Thoughts
You don’t need Webpack or React to build a structured plugin. You need consistency, scope, and a naming system that makes sense across 1000+ lines of async logic.
Structure your UXP plugin like this and:
- Your logic won’t tangle over time
- You’ll debug faster under pressure
- Others can jump into your code without a tour
It’s not glamorous—but it works. And when you’re building serious tools inside Adobe’s limitations, that’s what matters.
Next up: Creating a Gallery UI with JSON and Filters →
Frequently Asked Questions
Do I need a bundler or build process to make a good UXP plugin?
Not necessarily. If you keep things modular inside main.js
, you can build a clean, scalable plugin without Webpack or Vite. That said, for large projects, a build step can help with minification and separating dev/prod configs.
Can I use ES6 modules or import/export in UXP?
No—not in a traditional way. UXP doesn’t support module imports unless you’re compiling everything down into a single script. That’s why keeping a clean internal structure in main.js
is so importan
Is it okay to fetch remote JSON data in a UXP plugin?
Yes—as long as it’s from a CORS-friendly, HTTPS source. Adobe allows fetch()
in UXP, and it’s a great way to keep layout rules, product catalogs, or presets external and updateable.
How do I handle state in a UXP plugin without a framework?
Use a single pluginState
object to track what matters—page count, active layout, onboarding status—and update it consistently across your logic. Combine that with localStorage or session flags for persistence.
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.