Customizing the Configurator UI
Mimeeq provides two approaches for customizing the configurator UI, each suited for different levels of customization:
- Inline Custom CSS/JS (Simple) - Add small adjustments directly in embed templates
- Custom UI System (Advanced) - Create versioned UI packages with HTML, CSS, and JavaScript
Important: In both approaches, window.mimeeqApp is already available when your code runs. No need to wait for any events or check for its existence.
Inline Custom CSS/JS (Simple Customizations)
For small adjustments like styling tweaks or simple modal logic, you can add custom code directly to your embed template:
How to Add Inline Code
- Go to your embed template in the admin panel
- Navigate to the bottom of the settings panel
- Add your custom CSS and/or JavaScript
- Save the template
Use Cases for Inline Customization
- Adjust element positioning or spacing
- Add simple click handlers or modal logic
- Hide/show elements based on conditions
- Small additions like some information applets
- Small style overrides
- Quick fixes and tweaks
- Translations
- PDF customization
Example: Simple Modal Logic
// Inline JS in embed template
// mimeeqApp is already available - no need to wait for any events
const closeBtn = document.createElement('button');
closeBtn.textContent = 'Close';
closeBtn.className = 'custom-close-btn';
closeBtn.onclick = () => {
document.getElementById('configurator-modal').style.display = 'none';
};
document.querySelector('.mmq-container').appendChild(closeBtn);
/* Inline CSS in embed template */
.custom-close-btn {
position: absolute;
top: 10px;
right: 10px;
z-index: 1000;
}
/* Adjust spacing */
.mmq-options-panel {
padding: 20px !important;
}
Custom UI System (Advanced Customizations)
For complex UI changes, structural modifications, or reusable components, use the Custom UI system:
If you're building a complete custom UI, review the UI/UX Requirements for Custom UI Implementation for Figma deliverable requirements, responsive breakpoints, and component state specifications.
Setting Up Custom UI
- Go to Settings > Embed Custom UIs
- Click New
- Give it a name (e.g., "Accordion Layout", "Industry Calculator")
- Create versions and upload your files
- Mark a version as "active"
- Select this Custom UI in your embed template
Custom UI Structure
Each Custom UI can contain:
- HTML template - Define new layouts and components which should be added in the body to append as a child of embed.
- CSS files - Complete styling systems
- JavaScript files - Complex functionality
- Images, fonts and other assets - Asset files you are going to use at your Custom UI.
- Multiple versions - Test changes without breaking production
How It Works
- Create a Custom UI in customer settings
- Add versions (e.g., "v1.0", "v2.0-beta")
- Upload your CSS, JS and asset files to each version
- Set one version as "active"
- In your embed template, select the Custom UI (and optionally a specific version)
Example: Complete Accordion UI
Set template at your Custom UI
HTML Template
<div class="custom-accordion-container">
<div id="accordion-root"></div>
<div id="product-summary" class="summary-panel"></div>
</div>
And create these files in your Custom UI:
accordion.js
class AccordionConfigurator {
constructor() {
this.blocks = [];
this.selectedOptions = {};
this.setup();
}
setup() {
// mimeeqApp is already available when Custom UI loads
// Subscribe to product structure
window.mimeeqApp.observers.optionSets.blocks.subscribe(({ newValue }) => {
this.blocks = newValue;
this.render();
});
// Track selections — selectedOptions is keyed by instance ID
window.mimeeqApp.observers.optionSets.selectedOptions.subscribe(({ newValue }) => {
this.selectedOptions = {};
if (!newValue) return;
// For standard products the key is always "SINGLE_PRODUCT_ID"
const options = newValue['SINGLE_PRODUCT_ID'] ?? [];
options.forEach((opt) => {
this.selectedOptions[opt.blockId] = opt;
});
this.updateUI();
});
}
render() {
const root = document.getElementById('accordion-root');
root.innerHTML = '';
this.blocks.forEach((block, index) => {
const section = this.createAccordionSection(block, index);
root.appendChild(section);
});
}
createAccordionSection(block, index) {
const section = document.createElement('div');
section.className = 'accordion-section';
const header = document.createElement('div');
header.className = 'accordion-header';
header.innerHTML = `
<span>${block.name}</span>
<span class="accordion-value">${this.getSelectedValue(block)}</span>
`;
header.onclick = () => this.toggleSection(index);
const content = document.createElement('div');
content.className = 'accordion-content';
content.id = `section-${index}`;
content.style.display = index === 0 ? 'block' : 'none';
// Create option grid
const grid = document.createElement('div');
grid.className = 'option-grid';
block.options.forEach((option) => {
const optionEl = this.createOption(option, block);
grid.appendChild(optionEl);
});
content.appendChild(grid);
section.appendChild(header);
section.appendChild(content);
return section;
}
createOption(option, block) {
const el = document.createElement('div');
el.className = 'option-item';
el.dataset.optionId = option.id;
if (option.image) {
el.innerHTML = `<img src="${option.image}" alt="${option.name}">`;
}
el.innerHTML += `<span>${option.name}</span>`;
el.onclick = () => {
window.mimeeqApp.actions.markOption(option, block.id, 'accordion', true, block.code);
};
return el;
}
toggleSection(index) {
const content = document.getElementById(`section-${index}`);
const isOpen = content.style.display === 'block';
// Close all sections
document.querySelectorAll('.accordion-content').forEach((el) => {
el.style.display = 'none';
});
// Open clicked section
if (!isOpen) {
content.style.display = 'block';
}
}
getSelectedValue(block) {
const selected = this.selectedOptions[block.id];
return selected ? selected.name : 'Select...';
}
updateUI() {
// Update accordion headers with selected values
this.blocks.forEach((block, index) => {
const header = document.querySelectorAll('.accordion-header')[index];
if (header) {
header.querySelector('.accordion-value').textContent = this.getSelectedValue(block);
}
});
// Update selected state on options
document.querySelectorAll('.option-item').forEach((el) => {
const optionId = el.dataset.optionId;
const isSelected = Object.values(this.selectedOptions).some((opt) => opt.id === optionId);
el.classList.toggle('selected', isSelected);
});
}
}
// Initialize
new AccordionConfigurator();
accordion.css
.custom-accordion-container {
display: flex;
gap: 30px;
height: 100%;
}
#accordion-root {
flex: 1;
max-width: 600px;
}
.accordion-section {
border: 1px solid #e0e0e0;
border-radius: 8px;
margin-bottom: 10px;
overflow: hidden;
}
.accordion-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: #f8f8f8;
cursor: pointer;
font-weight: 600;
}
.accordion-header:hover {
background: #f0f0f0;
}
.accordion-value {
color: #666;
font-weight: 400;
}
.accordion-content {
padding: 20px;
border-top: 1px solid #e0e0e0;
}
.option-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 15px;
}
.option-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.option-item:hover {
border-color: #999;
transform: translateY(-2px);
}
.option-item.selected {
border-color: #000;
background: #f0f0f0;
}
.option-item img {
width: 60px;
height: 60px;
object-fit: contain;
margin-bottom: 8px;
}
.summary-panel {
width: 300px;
padding: 20px;
background: #f8f8f8;
border-radius: 8px;
}
Version Management
The Custom UI system supports multiple versions:
Custom UI: "Advanced Accordion"
├── Version 1.0 (active)
│ ├── accordion.js
│ └── accordion.css
├── Version 2.0-beta
│ ├── accordion.js
│ ├── accordion.css
│ └── utils.js
└── Version 1.1-bugfix
└── accordion.js
│ └── accordion.css
You can:
- Test new versions without affecting production
- Roll back to previous versions instantly
- A/B test different UI approaches
- Maintain different UIs for different products
Important Notes
About mimeeqApp Availability
When using either inline custom JS or the Custom UI system in production, window.mimeeqApp is always available when your code runs. The embed initializes fully before loading your custom code, so there is no need to poll or wait for events in production.
This does not apply to:
- Local development — your code runs independently and the embed may not have loaded yet. See Local Development for the recommended pattern.
- Embed template preview in the admin panel — custom JS may execute before the embed finishes initializing.
- External scripts on the host page that are not part of the Custom UI or inline JS — these should listen for the
mimeeq-app-loadedevent ormimeeq-3d-product-initializedif they need the product to be fully rendered.
About HTML Templates
The HTML template field in Custom UI is for simple HTML snippets only:
- Keep templates concise (not thousands of lines)
- For complex HTML structures, create them dynamically via JavaScript
- The HTML is inserted as a child of the embed element
Local Development
Building a custom UI locally works differently from how it runs in production. Understanding this distinction avoids common issues during development.
Production vs Local Architecture
In production, when using Mimeeq "Custom UI" at embed templates approach, the embed mounts your custom UI inside itself:
<!-- Production: embed loads and injects your Custom UI -->
<mmq-embed short-code="ABC123" template="my_template"></mmq-embed>
<!-- Your Custom UI code is fetched and mounted by the embed internally -->
During local development, you typically invert this — your custom UI wraps the embed:
<!-- Local dev: your app hosts the embed -->
<div id="my-custom-ui">
<div id="option-panel"><!-- your custom options UI --></div>
<mmq-embed short-code="ABC123" template="dev_template"></mmq-embed>
</div>
<script src="https://cdn.mimeeq.com/read_models/embed/app-embed.js" async></script>
This lets you develop locally against a live embed instance without building and uploading files to the Custom UI system on every change. Upload the final build to production when ready.
In local development you don't use the Custom UI feature from embed templates at all. Your embed template can be a plain template without custom UI enabled — your local code wraps the embed directly.
mimeeqApp Availability in Local Dev
The guarantee that window.mimeeqApp is always available applies to production Custom UI and inline custom JS contexts, where the embed initializes fully before loading your code.
In local development your code may run before the embed finishes loading. Guard against this with the mimeeq-app-loaded event:
// Initialize your custom UI once the embed is ready
function initCustomUI() {
window.mimeeqApp.observers.optionSets.blocks.subscribe(({ newValue }) => {
renderOptions(newValue);
});
}
if (window.mimeeqApp) {
initCustomUI();
} else {
document.addEventListener('mimeeq-app-loaded', initCustomUI, { once: true });
}
In the embed template preview (admin panel), custom JS may also load before the embed initializes. Hot module replacement during local development may not re-trigger mimeeq-app-loaded after a code change. Always use defensive checks in development environments.
Cleaning Up
When your custom UI unmounts — whether from page navigation, SPA route changes, or HMR — clean up subscriptions and event listeners to prevent memory leaks and duplicate handlers:
class CustomConfigurator {
constructor() {
this.subscriptions = [];
this.init();
}
init() {
// Store subscription references for cleanup
this.subscriptions.push(
window.mimeeqApp.observers.optionSets.blocks.subscribe(({ newValue }) => {
this.renderBlocks(newValue);
}),
);
this.subscriptions.push(
window.mimeeqApp.observers.pricing.prices.subscribe(({ newValue }) => {
this.updatePrice(newValue);
}),
);
// Store bound references for event listeners
this.handleAddToCart = (e) => this.onAddToCart(e.detail);
document.addEventListener('mimeeq-add-to-cart', this.handleAddToCart);
}
destroy() {
// Unsubscribe all observers
this.subscriptions.forEach((sub) => sub.unsubscribe());
this.subscriptions = [];
// Remove event listeners
document.removeEventListener('mimeeq-add-to-cart', this.handleAddToCart);
}
}
Always store subscription references when you create them. Calling .unsubscribe() on each reference in a cleanup method prevents observers from firing after your UI has been removed from the DOM.
Runtime Environment
When your custom UI code runs, several global objects provide access to the embed's API, environment configuration, and reactive data streams.
Global Objects
| Object | Description |
|---|---|
window.mimeeqApp | The main embed API — observers, actions, and utility functions |
window.mimeeqAuth | Authentication API (available after mimeeq-auth-loaded event) |
mimeeqApp API
| Resource | Access | Description |
|---|---|---|
| Observers | window.mimeeqApp.observers.* | Reactive data streams for configuration state, pricing, product data, and UI settings |
| Actions | window.mimeeqApp.actions.* | Methods to control configuration, generate AR, take screenshots, produce PDFs, and navigate |
| Utils | window.mimeeqApp.utils.* | Utility functions for common operations like adding to cart, generating short codes, and toggling configurator visibility |
For the complete observer reference, see the Observers API Reference. For actions, see the Actions API Reference.
Key Config Observers
Several observers provide useful context about the embed environment. Use these when your custom UI needs to adapt to runtime settings:
// Customer settings — includes CDN path, branding, feature limits
window.mimeeqApp.observers.config.customerConfig.subscribe(({ newValue }) => {
if (newValue) {
// CDN path for loading assets
const cdnPath = newValue.CDNPath;
// Customer branding
const accentColor = newValue.theme.palette.accent.main;
document.documentElement.style.setProperty('--custom-accent', accentColor);
}
});
// Which standard UI elements are visible or hidden
window.mimeeqApp.observers.config.uiConfig.subscribe(({ newValue }) => {
if (newValue) {
// Show your own price display when the standard one is hidden
document.getElementById('custom-price').style.display =
newValue.hidePrice ? 'block' : 'none';
}
});
Choosing Between Inline and Custom UI System
Use Inline Custom CSS/JS When:
- Making small style adjustments
- Adding simple functionality
- Quick fixes or temporary changes
- Testing ideas before full implementation
Use Custom UI System When:
- Building complex interfaces
- Need version control
- Creating reusable components
- Implementing structural changes
- Managing multiple UI variations
Widget Types Reference
When building a custom UI, you may need to replicate or extend the behavior of standard Mimeeq widgets. The following widget types exist in the standard configurator:
- Filter (Expanded/Compact) — material filtering with parent/child categories
- Thumbnails — grid-based visual option selection
- Dropdown — select-based option selection
- Radio — radio button option selection
- Button — interactive buttons that trigger rules
- Colour Picker — dynamic color selection with optional presets
- Text & Engraving — text input for personalization
- Upload Image — customer image upload for Print on Demand
- Print on Demand — image positioning on product surfaces
- Slider - slider for adjusting numeric values
Option data for all widget types is available through window.mimeeqApp.observers.optionSets.blocks.
When building custom UIs, it's important to understand option visibility scenarios. Options can be fully visible, hidden from UI but used by rules, visible in UI but hidden from specs, or completely removed. See Understanding Hiding Options for the four scenarios.
More Examples
Example: Industry-Specific Calculator (Custom UI System)
For complex tools, create a Custom UI with these files:
HTML Template (simple snippet in template field)
<div id="calculator-root"></div>
calculator.js (creates the full UI dynamically)
// Create the calculator UI dynamically
const calculatorHTML = `
<div class="calculator-panel">
<h3>Room Coverage Calculator</h3>
<div class="calculator-inputs">
<label>
Room Width (m):
<input type="number" id="room-width" value="4">
</label>
<label>
Room Length (m):
<input type="number" id="room-length" value="5">
</label>
</div>
<div class="calculator-results">
<p>Room Area: <span id="room-area">20</span> m²</p>
<p>Units Needed: <span id="units-needed">-</span></p>
<p>Coverage: <span id="coverage">-</span> m²</p>
</div>
</div>
`;
document.getElementById('calculator-root').innerHTML = calculatorHTML;
class CoverageCalculator {
constructor() {
this.unitCoverage = 2; // get it from your BE or somewhere
this.currentQuantity = 1;
this.setupListeners();
this.subscribeToData();
}
setupListeners() {
['room-width', 'room-length'].forEach((id) => {
document.getElementById(id).addEventListener('input', () => this.calculate());
});
}
subscribeToData() {
// mimeeqApp is already available
// Track quantity changes
window.mimeeqApp.observers.pricing.quantity.subscribe(({ newValue }) => {
this.currentQuantity = newValue || 1;
this.calculate();
});
}
calculate() {
const width = parseFloat(document.getElementById('room-width').value) || 0;
const length = parseFloat(document.getElementById('room-length').value) || 0;
const area = width * length;
document.getElementById('room-area').textContent = area.toFixed(1);
if (this.unitCoverage > 0) {
const unitsNeeded = Math.ceil(area / this.unitCoverage);
const totalCoverage = this.currentQuantity * this.unitCoverage;
document.getElementById('units-needed').textContent = unitsNeeded;
document.getElementById('coverage').textContent = totalCoverage.toFixed(1);
// Update quantity if needed
if (this.currentQuantity < unitsNeeded) {
window.mimeeqApp.actions.setQuantity(unitsNeeded);
}
}
}
}
new CoverageCalculator();
Example: Quick Style Fix (Inline CSS)
For simple adjustments, add this directly to your embed template:
/* Hide price for B2B customers */
.mmq-price-display {
display: none !important;
}
/* Make buttons larger on mobile */
@media (max-width: 768px) {
.mmq-button {
padding: 16px 24px !important;
font-size: 18px !important;
}
}
/* Custom loading animation */
.mmq-loader {
border-color: var(--brand-color) !important;
}
Best Practices
For Inline Custom Code
- Keep it simple and focused
- Use CSS custom properties when possible
- Avoid complex DOM manipulation
- Comment your code for future reference
For Custom UI System
- Create clear file structures
- Use semantic versioning (1.0, 1.1, 2.0)
- Document breaking changes between versions
- Test thoroughly before marking as active
General Best Practices
- mimeeqApp is always available in custom UI/JS - no need to check or wait
- Check if DOM elements exist before modifying
- Handle null/undefined observer values
- Unsubscribe from observers when removing elements
- Test on multiple devices and browsers
Common Patterns
Pattern: Progressive Enhancement
Start with inline CSS/JS, then move to Custom UI system as complexity grows:
// Start with inline JS
// Simple feature flag
if (window.enableAdvancedUI) {
document.body.classList.add('advanced-ui-enabled');
}
Later, create a full Custom UI package with the complete implementation.
Pattern: Defensive Coding
Always check if DOM elements exist before using them:
// Safe DOM manipulation
const container = document.querySelector('.mmq-container');
if (container) {
// Safe to use container
container.appendChild(myElement);
}
// Safe observer usage - check for data
window.mimeeqApp.observers.product.mainProductData.subscribe(({ newValue }) => {
if (newValue && newValue.metadata) {
// Safe to use metadata
}
});
Pattern: Responsive Customization
Adapt your custom UI based on device:
const isMobile = window.innerWidth < 768;
if (isMobile) {
// Load mobile-optimized UI
document.body.classList.add('custom-mobile-ui');
} else {
// Load desktop UI
document.body.classList.add('custom-desktop-ui');
}
Troubleshooting
Custom UI Not Loading
- Check that Custom UI is selected in embed template
- Verify files are uploaded to the active version
- Look for JavaScript errors in console
- Ensure your custom code doesn't have syntax errors
Styles Not Applying
- Check CSS specificity (you may need !important)
- Verify shadow DOM boundaries
- Ensure CSS file is uploaded correctly
- Check for syntax errors
JavaScript Errors
- Always check if DOM elements exist first
- Handle async data properly
- Use try-catch for external API calls
- Check browser compatibility
Next Steps
- CSS Variables - For pure visual customization
- CSS Parts - Style shadow DOM components
- API Reference - All available methods
- Observers Reference - All data streams
Choose the right approach for your needs - inline custom code for quick changes, or the Custom UI system for complex, versioned interfaces.