> For the complete documentation index, see [llms.txt](https://help.mimeeq.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://help.mimeeq.com/developer-guides/custom-ui.md).

# Custom UI

Comprehensive developer guide for building and deploying Custom UIs in the Mimeeq 3D Configurator.

## 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 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 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 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:**

```html
<your-wrapper>
  <slot></slot>
</your-wrapper>
```

When this component is shipped to the client browser, it renders as: **Rendered output on client:**

```html
<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.\
\
<https://app.mimeeq.com/INN-OVEET/private/admin/settings/embedTemplate/EMTP-7cac06c7-5456-47ed-a97b-391d0bc60f30>

*Image unavailable in the source article: Screenshot 2026-02-26 at 14.20.48.png. Replacement required.*

**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:**

```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:**

```html
<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.  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:

```javascript
// 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

```javascript
// ─── 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:

```javascript
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.

```javascript
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:

```javascript
// 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:

```javascript
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:**

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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():

```javascript
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:

```javascript
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:

```javascript
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 for the canvas
* [ ] In Mimeeq Admin, create a development template with **Use Custom UI** enabled and no UI version set
* [ ] Locally, nest 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                                 |

***


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://help.mimeeq.com/developer-guides/custom-ui.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
