Articles on: Developers

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 <slot>

Tag

Standard <mmq-embed> tag is used by end clients

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 Managementhttps://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


Screenshot 2026-02-26 at 14.20.48.png


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.



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

glbPath

string

CDN-relative path to GLB file (Android)

usdzPath

string

CDN-relative path to USDZ file (iOS)

s3Path

string

Product thumbnail image URL

url

string

Original product page URL

allowScaling

string

Whether AR resizing is enabled

conversionStatus

string

'completed' or 'failed'

completeSubscription

Promise

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 rel='ar' → Apple AR Quick Look

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

mimeeq-generate-ar-short-code

Mobile embed — shortcode ready for parent page

{ shortCode, allowScaling }

mimeeq-ar-get-back-to-configuration

User wants to return from AR (embed only)

{ variantCode, shortCode, isModular }


API Methods


Method

Returns

Description

actions.showAR()

void

Built-in complete AR flow (handles all steps automatically)

actions.generateAR()

Promise<string

Creates AR model from current config, returns shortcode

actions.getARShortCodeData(code, withSub?)

Promise<ARShortcodeData \ null>

Fetches model file URLs, metadata, and optional completion subscription

actions.regenerateAR(arData)

Promise<string \ null>

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 mmq-embed

Shipped inside mmq-embed by admin

Embed script

Loaded via <script> tag in HTML

Managed by admin shipping

Versioning control

Code repository

Admin panel per template

Suitable for deployment

No

Yes





Updated on: 26/02/2026

Was this article helpful?

Share your feedback

Cancel

Thank you!