Custom UI Developer Guide
1. Custom UI Creation
Your approach to creating a Custom UI directly determines your development and deployment strategy. This section covers the recommended Web Components approach, key differences between local and production environments, and how the admin panel manages versioning and delivery.
For additional guidance, explore the developer articles available in the documentation.
https://docs.mimeeq.com/configuration-data-model/
https://docs.mimeeq.com/web/api/Types/type-aliases/LoadModularScene/
https://docs.mimeeq.com/web/api/Types/interfaces/Observers/
https://docs.mimeeq.com/events/
https://docs.mimeeq.com/custom-ui-guide/#custom-ui-system-advanced-customizations
https://docs.mimeeq.com/custom-fields/
1.1 Technology Choice
The Mimeeq 3D Configurator is built using Web Components (specifically Stencil.js, though any Web Component framework is compatible). This architectural decision directly impacts how slots are utilized and how the Custom UI is structured.
Web Components represent the approach our development team has chosen, and the entire process is tested and optimized for this technology. We recommend this approach when you plan to use the Mimeeq Admin panel for deploying and maintaining versioned Custom UI builds.
If deploying Custom UI through the admin panel is not a priority, you have complete freedom in choosing your preferred framework. Simply embed the <mmq-embed/> component in your code to get started.
1.2 Production vs. Local Structure
The HTML structure of your Custom UI differs between production (admin-deployed) and local development environments. Understanding this distinction is critical before writing any code.
Aspect | Detail |
|---|---|
Delivery | Admin panel ships the Custom UI bundle via the embed template |
Wrapper | Your Web Component wraps the canvas using a |
Tag | Standard |
Versioning | Different UI versions can be assigned per embed template |
1.2.1 Production Structure (Admin-Deployed)
In production, configurators use the standard <mmq-embed> tag to ensure a consistent customer experience. To ship your Custom UI through this tag, your Web Component must define a wrapper that exposes a default slot for the <mmq-embed>component.
Register your component in the Mimeeq Admin at: Settings → Custom UI Management — https://app.mimeeq.com/INN-OVEET/private/admin/settings/customUiList
Admin registration — wrapper component:
<your-wrapper>
<slot></slot>
</your-wrapper>
When this component is shipped to the client browser, it renders as:
Rendered output on client:
<mmq-embed>
<your-wrapper>
<your-ui-structure>
<!-- Other UI elements of your design -->
<canvas /> <!-- Actual 3D canvas -->
<!-- ... -->
</your-ui-structure>
</your-wrapper>
</mmq-embed>
1.2.2 Canvas-Only Embed
Custom UI requires Mimeeq Embed to ship a canvas-only build — no default Mimeeq UI. The most efficient way to achieve this is by selecting the Custom UI option in the embed template used by the 3D configurator. This ensures only the necessary packages are shipped.<br/><br/>https://app.mimeeq.com/INN-OVEET/private/admin/settings/embedTemplate/EMTP-7cac06c7-5456-47ed-a97b-391d0bc60f30

Benefits of this template approach:
- Only required packages are bundled and shipped to the browser
- Versioning is controlled from the admin panel per template
- Different Custom UI versions can be supplied to different embed templates
1.3 Local Development
Due to the specifics of how production embeds are delivered to the browser, local development requires a separate template that ships only the canvas. This approach can also be used in production, but it excludes shipping the Custom UI from the admin panel.
⚠ Warning: Do not use your production embed template for local development. The production delivery mechanism does not work with a locally running Custom UI component. You will end up with doubled UI, one local and second shipped with the embed.
1.3.1 Local Template Configuration
In the Mimeeq Admin, configure a dedicated development template with the following settings:
Setting | Value |
|---|---|
Use Custom UI | Enabled (checked) |
Custom UI Version | None — leave empty for local development |
1.3.2 Local HTML Structure
Structure the embed in your local environment so that your wrapper component passes the Mimeeq embed (pure canvas) as a slot child:
Local development HTML:
<your-wrapper>
<!-- Passing embed (pure canvas) as a slot -->
<mmq-embed short-code="{shortCode}" template="development">
</mmq-embed>
</your-wrapper>
<script
src="https://cdn.mimeeq.com/read_models/embed/app-embed.js"
rel="script"
type="application/javascript"
async>
</script>
ℹ Note: This structure mirrors the admin-based production approach and allows you to use the same deployment logic when you are ready to publish. It also ensures the slot passing mechanism works identically in both environments.
1.3.3 Recommended Component Structure
Following this pattern results in a clear, maintainable component hierarchy:
Recommended component tree:
<custom-ui>
<structure-wrapper>
<canvas-wrapper>
<other-ui-elements />
<slot /> <!-- 3D canvas passed from mmq-embed -->
</canvas-wrapper>
<side-panel />
</structure-wrapper>
</custom-ui>
This hierarchy provides a clean separation of concerns: the canvas sits inside a dedicated wrapper, UI chrome (side panels, toolbars) are siblings, and the slot ensures the canvas is always correctly positioned regardless of environment.<br/><br/>1.3.4 Mimeeq app availablity<br/>
There can be a race condition, where component will load after the event for mimeeq-app-loaded was fired. To handle this case use following pattern:
// PSEUDOCODE
@State() isAppLoaded = false;
@Listen('mimeeq-app-loaded', { target: 'document' })
async handleLoadedState() {
this.isAppLoaded = true;
}
private async checkAppLoadedState() {
// Poll for window.mimeeqAppLoaded until it's true
while (!this.isAppLoaded) {
if ((window as any).mimeeqAppLoaded) {
this.isAppLoaded = true;
break;
}
// Poll every 100ms
await new Promise(resolve => setTimeout(resolve, 100));
}
}
// Some lifecycle hook before component loads
beforeLoad() {
this.checkAppLoadedState();
}
@Watch('isAppLoaded') () {
// window.mimeeqApp is available
}
2. Modular Embed UI — Lifecycle & Data Flow
The mimeeq embed UI is the core component that orchestrates the 3D configurator experience when running in embed mode. It manages responsive layout, subscribes to the global store, renders the canvas and UI chrome, and handles keyboard shortcuts. Custom UI implementations must be aware of this lifecycle to integrate correctly.
Pseudo Code with proposed root structure
// ─── Component ───
function ModularEmbedUI(){
// ── Responsive state ──
const isMobile = useMediaQuery('(max-width: 839px)');
const containerWidth = useResizeObserver(rootRef);
// Subscribe to load mainproductData
if (window.mimeeqApp) {
const {
observers: {
product: { mainProductData },
},
} = window.mimeeqApp;
if (!this.productSub) {
this.productSub = mainProductData.subscribe((data) => {
const newValue = (data && data.newValue !== undefined
? data.newValue
: data) as unknown as SimpleProduct;
this.loaded = !!newValue;
});
}
}
// ── WebGL guard ──
if (!isWebGLSupported()) {
return <FallbackModal reason="webgl-unsupported" />;
}
// --- Load first scene ---- //
await window.mimeeqApp.actions.loadModularState('F95SNI');
// ── Action toasts (react to currentAction changes) ──
useEffect(() => {
if (!currentAction) return;
showToast(isMobile ? 'bottom-center' : 'top-center', actionLabel(currentAction));
}, [currentAction]);
// ── Render ──
return (
<div
ref={rootRef}
className={classNames({
'--dynamic': uiFlags.adjustToContainer || props.autoModal,
'--admin': props.isAdmin,
'--mobile': isMobile,
'--fullscreen': isFullscreen,
'--ios': isIOS(),
'--aspect-ratio': !!aspectRatio,
})}
style={{ '--canvas-width': `${containerWidth}px` }}
>
{/* ── Global notifications ── */}
<ToastContainer id="global" placement="top-center" variant="global" />
<div className="wrapper">
{/* ── Canvas content column ── */}
<div className="canvas-content">
{/* Header */}
{isMobile
? <MobileHeader modular is3d={is3d} {...props} />
: <DesktopHeader variant={props.configuratorVariant} {...props} />
}
{/* 3D Canvas */}
<div className="canvas-slot"><slot/></div>
{/* Canvas overlays */}
<CanvasButtons isModular />
<FloatingToolbar isMobile={isMobile} selectedElement={selectedEl} />
<ConfigChangeOverlay />
<ToastContainer id="inline" placement={isMobile ? 'bottom-center' : 'top-center'} />
<Footer />
</div>
{/* ── Option panel column — desktop, when loaded ── */}
{!isMobile && loaded && (
<OptionPanel />
)}
</div>
{/* Secondary content slot */}
{additionalContent}
{isMobile && <MobileOptionPanel />}
</div>
);
}
// ─── Cleanup (on unmount) ───
// All useObserver / useMediaQuery / useResizeObserver hooks
// handle their own cleanup via return () => unsubscribe()
3. AR Modal — Custom Implementation
The AR (Augmented Reality) feature must be implemented by the Custom UI in a specific way, since it integrates directly with the Mimeeq core app via window.mimeeqApp.actions. This section provides a complete, step-by-step guide to building a custom AR flow.
ℹ Note: The built-in actions.showAR() method runs the entire flow automatically. Implement manually only when you need full control over the UI and UX of the AR experience.
3.1 Architecture Overview
The AR launch flow follows a linear pipeline with branching for platform detection and error recovery:
Step | Action |
|---|---|
Step 1 | Check permission — verify the customer license includes AR |
Step 2 | Generate AR model — create an AR-ready asset from the current configuration |
Step 3 | Fetch AR data — retrieve file URLs and metadata via shortcode |
Step 4 | Wait for processing — poll or subscribe until model files are ready |
Step 5 | Detect platform — branch to iOS, Android, or Desktop (QR) experience |
Step 6 | Handle failures — offer retry if generation fails or times out |
Step 7 | Navigate back — return the user to the configurator after AR ends |
3.2 Step-by-Step Implementation
Step 1 — Check Permission
Not every customer license includes AR. Always check before rendering an AR button or initiating the flow:
function canUseAR(isEmbed) {
const store = window.mimeeqApp.getStore?.();
const limits = store?.getState()?.customerConfig?.limits;
if (!limits) return false;
return isEmbed ? limits.canShowArEmbed : limits.canShowArBusiness;
}
Step 2 — Generate the AR Model
Call generateAR() to create an AR-ready model from the current product configuration. The product must already be loaded in the configurator. This returns a unique shortcode.
const shortCode = await window.mimeeqApp.actions.generateAR();
if (!shortCode) {
showError('Could not generate AR model');
return;
}
⚠ Warning: generateAR() uses the currently loaded product configuration. Ensure the product is fully loaded before calling this function.
Step 3 — Fetch AR Model Data
Use the shortcode to retrieve model file URLs and associated metadata:
// Pass true to get a completion subscription
// for models that are still processing
const arData = await window.mimeeqApp.actions
.getARShortCodeData(shortCode, true);
Key fields returned in arData:
Field | Type | Description |
|---|---|---|
| | CDN-relative path to GLB file (Android) |
| | CDN-relative path to USDZ file (iOS) |
| | Product thumbnail image URL |
| | Original product page URL |
| | Whether AR resizing is enabled |
| | |
| | Resolves when background processing finishes |
Step 4 — Wait for Processing
Models may still be generating on the server. Check whether files exist; if not, subscribe and wait for completion with a timeout:
async function waitForArModel(arData) {
const isAndroid = /Android/.test(navigator.userAgent);
const hasFiles = isAndroid ? !!arData.glbPath : !!arData.usdzPath;
if (hasFiles) return arData;
if (!arData.completeSubscription) {
throw new Error('No files and no subscription available');
}
// 2-minute timeout
const result = await Promise.race([
arData.completeSubscription,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('AR generation timed out')), 120_000)
),
]);
if (result.conversionStatus === 'completed') {
return { ...arData, ...result };
}
throw new Error('AR generation failed');
}
Step 5 — Detect Platform & Open AR
Platform detection determines which AR viewer to launch:
Platform | Format | Mechanism |
|---|---|---|
Android | GLB | Intent URL → Google Scene Viewer (ARCore) |
iOS | USDZ | Anchor with |
Desktop | Landing page URL | QR code for user to scan with mobile device |
Platform detection:
function detectPlatform() {
const ua = navigator.userAgent;
const isIOS = /iPad|iPhone|iPod/.test(ua);
const isAndroid = /Android/.test(ua);
return { isIOS, isAndroid, isMobile: isIOS || isAndroid };
}
5a — Android: Google Scene Viewer
Construct an intent:// URL that launches ARCore Scene Viewer. The fallbackUrl redirects if ARCore is not installed:
function openAndroidAR(cdnPath, arData, productName, fallbackUrl) {
const glbUrl = `${cdnPath}/${arData.glbPath}`;
const href = [
'intent://arvr.google.com/scene-viewer/1.0',
`?file=${glbUrl}`,
'&mode=ar_preferred',
`&resizable=${arData.allowScaling ? 'true' : 'false'}`,
`&title=${encodeURIComponent(productName)}`,
`&link=${arData.url}`,
'#Intent;scheme=https;package=com.google.ar.core',
';action=android.intent.action.VIEW',
`;S.browser_fallback_url=${encodeURIComponent(fallbackUrl)}`,
';end;',
].join('');
window.location.href = href;
}
5b — iOS: AR Quick Look
iOS requires an anchor element with rel='ar' to trigger AR Quick Look. The element is created programmatically, clicked, then removed:
function openIOSAR(cdnPath, arData, productName) {
const usdzUrl = `${cdnPath}/${arData.usdzPath}`;
const isSafari = /Safari/.test(navigator.userAgent)
&& !/CriOS|Chrome/.test(navigator.userAgent);
let href = `${usdzUrl}#allowsContentScaling=${arData.allowScaling ? '1' : '0'}`;
if (isSafari) {
href += `&canonicalWebPageURL=${window.location.href}`;
href += `&callToAction=View`;
href += `&checkoutTitle=${encodeURIComponent(productName)}`;
}
const link = document.createElement('a');
link.rel = 'ar';
link.href = href;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
link.remove();
}
5c — Desktop: QR Code
On desktop there is no native AR viewer. Display a QR code linking to the Mimeeq AR landing page:
function getArLandingUrl(appUrl, customerPrefix, locale, shortCode, allowScaling) {
return `${appUrl}/${customerPrefix}/${locale}/ar/${shortCode}?scaling=${allowScaling}`;
}
// Render with any QR library (qrcode.js, qr-code-styling, etc.)
function showQrModal(url) {
// Mount your modal UI and render the QR code
// Also show a copyable text field with the URL
}
Step 6 — Handle Failures & Retry
If generation fails or times out, offer the user a retry using regenerateAR():
async function retryArGeneration(previousArData) {
const newShortCode = await window.mimeeqApp.actions
.regenerateAR(previousArData);
if (!newShortCode) {
showError('Retry failed');
return null;
}
return window.mimeeqApp.actions
.getARShortCodeData(newShortCode, true);
}
Step 7 — Navigate Back to Configurator
After the AR experience ends, return the user to the configurator. The mechanism differs between embed and standalone contexts:
function goBackToProduct(arData, isEmbed) {
if (isEmbed) {
document.dispatchEvent(new CustomEvent(
'mimeeq-ar-get-back-to-configuration',
{
detail: {
action: 'arGoBack',
variantCode: arData.isModular
? arData.modularVariantCode
: arData.variantCode,
shortCode: arData.modularShortCode,
isModular: arData.isModular,
},
}
));
} else {
window.location.href = arData.url;
}
}
3.3 Complete Implementation
All steps composed into a single launchAR entry-point function:
async function launchAR({ isEmbed, cdnPath, appUrl, locale, customerPrefix }) {
// 1. Permission check
if (!canUseAR(isEmbed)) return showError('AR not available');
// 2. Generate model
showLoading('Generating AR model...');
const shortCode = await window.mimeeqApp.actions.generateAR();
if (!shortCode) return showError('Generation failed');
// 3. Fetch AR data
let arData = await window.mimeeqApp.actions
.getARShortCodeData(shortCode, true);
if (!arData) return showError('AR data not found');
// 4. Wait for processing
try {
arData = await waitForArModel(arData);
} catch (err) {
hideLoading();
return showRetryUI(arData); // calls retryArGeneration()
}
hideLoading();
// 5. Resolve localised product name
const name = typeof arData.name === 'object'
? arData.name[locale] ?? Object.values(arData.name)[0] ?? ''
: arData.name ?? '';
// 6. Open per platform
const { isIOS, isAndroid, isMobile } = detectPlatform();
if (!isMobile) {
const url = getArLandingUrl(appUrl, customerPrefix, locale,
shortCode, arData.allowScaling);
showQrModal(url);
} else if (isAndroid) {
const fallback = `${appUrl}/${customerPrefix}/${locale}`
+ `/ar-redirect/${shortCode}/incompatible`;
openAndroidAR(cdnPath, arData, name, fallback);
} else if (isIOS) {
openIOSAR(cdnPath, arData, name);
}
}
3.4 Events & API Reference
Custom Events
Event | When to Listen | Detail Payload |
|---|---|---|
| Mobile embed — shortcode ready for parent page | |
| User wants to return from AR (embed only) | |
API Methods
Method | Returns | Description |
|---|---|---|
| | Built-in complete AR flow (handles all steps automatically) |
| | Creates AR model from current config, returns shortcode |
| | Fetches model file URLs, metadata, and optional completion subscription |
| | Retries a failed or timed-out AR generation |
4. Quick Reference
4.1 Development Checklist
- [ ] Create a Web Component wrapper with a <slot> for the canvas
- [ ] In Mimeeq Admin, create a development template with Use Custom UI enabled and no UI version set
- [ ] Locally, nest <mmq-embed> as a child of your wrapper component
- [ ] Load the embed script from cdn.mimeeq.com asynchronously
- [ ] For production, register your component in Custom UI Management== (optional, when using admin to ship the custom UI)==
- [ ] Assign your production template to the embed and set the Custom UI version== (optional, when using admin to ship the custom UI)==
- [ ] Test AR permission before rendering any AR button
- [ ] Implement all 7 AR steps; do not rely on implicit state from previous steps
- [ ] Subscribe to store observers in onMount and unsubscribe in onDestroy
4.2 Environment Comparison
Aspect | Local Development | Production (Admin) |
|---|---|---|
Template type | Development template | Production template |
Custom UI version | None (empty) | Versioned bundle from admin |
Canvas delivery | Passed as slot via local | Shipped inside |
Embed script | Loaded via | Managed by admin shipping |
Versioning control | Code repository | Admin panel per template |
Suitable for deployment | No | Yes |
Updated on: 26/02/2026
Thank you!
