Appearance
Advanced Usage
LIQA offers optional advanced methods that can make embedding LIQA more seamless and provide more control over the integration. Use them after the Quick Start is working and you know which user journey your product needs.
| Use case | Feature to use |
|---|---|
| Keep LIQA isolated from complex host-page CSS, scripts, or strict CSP rules when your application needs a clearer embed boundary. | Iframe Integration |
| Use your own upload button or native file picker while still validating the selected image in LIQA. | Programmatic upload from host UI |
| Check whether the participant's current device is good enough for LIQA capture before showing the capture step. | Device capability check |
| Start loading LIQA resources before the user reaches the capture step, so the visible loading time feels shorter. | Preload LIQA resources |
| Return a front-camera image that matches the mirrored preview the user saw during capture, when your downstream UI expects that orientation. | Mirror image from front camera |
Convert captured photos to Base64 when your backend, native bridge, or upload API does not accept browser Blob objects directly. | Convert captured image to Base64 |
| Use anonymized face-image variants when your workflow requires stronger privacy protection while preserving the data needed for processing. | Anonymized image processing pipeline |
Iframe Integration
By default, LIQA runs as a web component rendered inside Shadow DOM (and uses an isolated JavaScript execution context). This keeps the LIQA UI isolated from your application while still allowing you to theme it via the styles attribute.
use-iframe mode renders LIQA inside a sandboxed <iframe> that is loaded from the same LIQA origin as liqa.js. This provides a harder isolation boundary and is useful when your integration needs clearer separation from the host page.
When to use it
- You need stronger isolation from host page CSS/JS (e.g., large apps with global patches or unpredictable style inheritance).
- You have a restrictive CSP and want LIQA to run in its own document (you will still need to allow the LIQA origin in
frame-src). - You want to avoid debugging styling conflicts and treat LIQA as an embedded surface customized only via LIQA config.
When not to use it
- You rely on globally loaded web fonts and want them to apply to LIQA automatically (use default mode).
- You cannot allow embedding frames in CSP (
frame-src/child-src).
Enable it
html
<hautai-liqa license="LICENSE_KEY_PROVIDED_BY_HAUT.AI" use-iframe></hautai-liqa>Key differences vs default mode
- Same API surface: all configuration is still done via
<hautai-liqa>attributes and all events (ready,captures, etc.) are dispatched from the host element. - Styles: selectors from your app’s global CSS do not reach into LIQA in both modes. In default mode, inherited properties and CSS variables from the host can still influence the UI; in
use-iframemode, inheritance stops at the iframe boundary, so rely onstylesfor customization. - UI scale: in
use-iframemode, inherited host styles do not cross the iframe boundary. If your host app uses a custom rootfont-size, LIQA can appear smaller or larger than in default mode. :hostselectors: inuse-iframemode,:hostselectors fromstylesare translated to:rootinside the iframe, so the same styles string works. Avoid relying on selectors like:host([data-theme="dark"])— provide theme-specificstylesfrom the host app instead.- Fonts: fonts loaded in your app (
<link rel="stylesheet" ...>) are available in default mode, but not automatically inside the iframe. Load custom fonts via LIQAstyles(e.g.@import/@font-face) and set--font-family. See Styles. - Basic theming: LIQA propagates these host CSS variables into the iframe (if set on
<hautai-liqa>):--background-color,--color,--color-brand,--font-family. - Hosting:
use-iframeloadsliqa-iframe.htmlfrom the LIQA origin. If you self-host LIQA assets, make sure this file is available next toliqa.js. - CSP: you must allow the LIQA origin in
frame-src/child-srcin addition to the regular LIQA CSP directives. See Set up CSP.
Troubleshooting
If you see cross-origin errors after enabling use-iframe, upgrade to LIQA 6.25.1 or later.
If LIQA looks scaled after enabling use-iframe, set the base font size explicitly via styles:
html
<hautai-liqa
use-iframe="true"
styles="
:host { font-size: HOST_FONT_SIZE_PX; }
"
></hautai-liqa>HOST_FONT_SIZE_PX should match your host app root font size.
Programmatic upload from host UI
Use liqa.upload(file) when your application owns the upload button (or a native file picker) and you only need LIQA to validate the selected image. The file is fed straight into LIQA's Preview/quality flow and delivered through the usual captures event — the same as a camera capture.
liqa.upload(file) does not require upload in sources; the sources attribute only controls which source buttons and fallbacks LIQA renders itself. It can be called at any time, including before the ready event — the file is buffered until LIQA has booted.
Avoiding the camera prompt. For a camera-only configuration LIQA acquires the camera at startup. If your integration is upload-driven and should never request the camera, include upload in sources (for example sources="front_camera,upload", or sources="upload" for upload only). LIQA then keeps the camera idle until the user explicitly selects it, while your own button drives liqa.upload(file).
html
<input id="selfie-upload" type="file" accept="image/*" />
<hautai-liqa license="LICENSE_KEY_PROVIDED_BY_HAUT.AI" sources="front_camera,upload"></hautai-liqa>
<script type="module">
import "SOURCE_URL_PROVIDED_BY_HAUT_AI/liqa.js"
const input = document.querySelector("#selfie-upload")
const liqa = document.querySelector("hautai-liqa")
input.addEventListener("change", () => {
const file = input.files?.[0]
if (file) liqa.upload(file)
})
</script>Device capability check
Use checkDeviceCapability() to check, before you show LIQA, whether the participant's phone is good enough to capture a good-quality photo. It resolves to a small result object — capable plus the list of reasons for any failed checks — so you can catch unsupported devices early and tell the participant exactly what to do next, instead of running into problems mid-study.
tsx
tsximport {checkDeviceCapability } from "SOURCE_URL_PROVIDED_BY_HAUT_AI/liqa.js" const {capable ,reasons } = awaitcheckDeviceCapability () if (capable ) { // Good to go — show LIQA } else { // Show your own message based on `reasons` }
tsximport {checkDeviceCapability } from "SOURCE_URL_PROVIDED_BY_HAUT_AI/liqa.js" const {capable ,reasons } = awaitcheckDeviceCapability () if (capable ) { // Good to go — show LIQA } else { // Show your own message based on `reasons` }
The default check is static — it does not open the camera or ask for any permission, so it's safe to run as soon as the page loads. It verifies that the participant is on a phone running a supported iOS or Android version, in a browser with the features LIQA needs, and — where the browser reports it — with enough device memory for the analysis.
Each entry in reasons names one failed check:
| Reason | Meaning | Suggested message to the participant |
|---|---|---|
unsupported-device | Not a phone (desktop, tablet, or unrecognized device). | "Please open this link on your phone." |
unsupported-os | A phone, but not iOS or Android. | "Please use an iOS or Android phone." |
outdated-os | The OS version is below the supported floor (or couldn't be read). | "Please update your phone and try again." |
missing-browser-features | The browser lacks features LIQA requires. | "Please try a different or updated browser." |
insufficient-memory | The phone reports too little memory for the analysis. | "This phone can't run the capture experience." |
insufficient-camera | The front camera is missing or its resolution is too low. | "This phone's camera isn't sufficient." |
camera-unavailable | The camera couldn't be verified — see below. Retryable. | "Please allow camera access and try again." |
Verifying the camera
To also verify the front (selfie) camera — the camera LIQA captures with — pass { camera: true }:
tsx
tsximport {checkDeviceCapability } from "SOURCE_URL_PROVIDED_BY_HAUT_AI/liqa.js" const {capable ,reasons } = awaitcheckDeviceCapability ({camera : true }) if (!capable &&reasons .includes ("camera-unavailable")) { // Not a hardware verdict: the participant denied the prompt, the camera was // busy, or the page blocks camera access. Ask them to allow access and retry. }
tsximport {checkDeviceCapability } from "SOURCE_URL_PROVIDED_BY_HAUT_AI/liqa.js" const {capable ,reasons } = awaitcheckDeviceCapability ({camera : true }) if (!capable &&reasons .includes ("camera-unavailable")) { // Not a hardware verdict: the participant denied the prompt, the camera was // busy, or the page blocks camera access. Ask them to allow access and retry. }
On Android this opens the camera once (so it asks for camera permission) and checks the front camera's resolution. The two camera outcomes are deliberately distinct: insufficient-camera is a hardware verdict, while camera-unavailable only means the check couldn't run — treat it as retryable rather than as an unsupported device. On iOS the camera is not probed: every iPhone that passes the OS check has a sufficient front camera.
You can also call the check on a mounted element: await document.querySelector("hautai-liqa").checkDeviceCapability().
Browser-only
checkDeviceCapability() inspects the current device, so it must run in the participant's browser. Calling it during server-side rendering throws.
Preload LIQA resources before LIQA session
Preloading is useful when your product has a step before capture, such as a questionnaire, onboarding screen, consent step, or product recommendation flow. LIQA can download resources during that time so the user sees less of the LIQA loading screen when the capture step begins.
You can achieve this by leveraging the preload API. LIQA supports preloading both preset resources and feature-specific assets like tutorial videos.
Preloading Presets
The following code example illustrates the initiation of the Face preset preloading:
tsx
tsximport {preload } from "SOURCE_URL_PROVIDED_BY_HAUT_AI/liqa.js" // Initiate the preloading of the "face" preset's resource in advance // to ensure LIQA is ready when the user encounters itpreload ({preset : "face" })
tsximport {preload } from "SOURCE_URL_PROVIDED_BY_HAUT_AI/liqa.js" // Initiate the preloading of the "face" preset's resource in advance // to ensure LIQA is ready when the user encounters itpreload ({preset : "face" })
Preloading Tutorial Videos
To eliminate loading screens and flickering when users access the tutorial, you can preload the tutorial videos:
tsx
tsximport {preload ,FEATURE } from "SOURCE_URL_PROVIDED_BY_HAUT_AI/liqa.js" // Preload tutorial videos to ensure smooth playback without loading screenspreload ({feature :FEATURE .TUTORIAL })
tsximport {preload ,FEATURE } from "SOURCE_URL_PROVIDED_BY_HAUT_AI/liqa.js" // Preload tutorial videos to ensure smooth playback without loading screenspreload ({feature :FEATURE .TUTORIAL })
Preloading Both Preset and Tutorial
You can also preload both preset resources and tutorial videos simultaneously:
tsx
tsximport {preload ,FEATURE } from "SOURCE_URL_PROVIDED_BY_HAUT_AI/liqa.js" // Preload both preset resources and tutorial videospreload ({preset : "face",feature :FEATURE .TUTORIAL })
tsximport {preload ,FEATURE } from "SOURCE_URL_PROVIDED_BY_HAUT_AI/liqa.js" // Preload both preset resources and tutorial videospreload ({preset : "face",feature :FEATURE .TUTORIAL })
Recommendations for usage
- If your page contains some activity required before the LIQA session (e.g. filling a questionnaire/survey), it's best to execute the preloading before this user-time-consuming activity.
- Tutorial preloading is especially beneficial when users are likely to access the tutorial feature, as it eliminates the initial loading screen and prevents video playback issues.
- Considering the tiny size of the
liqa.jsscript (about 5 KB gzipped), it's safe to put the import of the preload function at the page's head without concern for its impact on the page's loading speed. - If the resources have not been fully loaded before the user starts interacting with LIQA, LIQA will efficiently reuse the previously downloaded chunks, avoiding unnecessary resource reloading.
- For tutorial-heavy workflows, preloading tutorial videos can significantly improve user experience by providing instant video playback without buffering or loading screens.
Mirror image from front camera
Use this option when your downstream display, comparison, or analysis UI must match the orientation the user saw during the selfie experience.
It is a common pattern for camera applications to display the video from front device camera horizontally flipped (mirrored) to ease the user interaction. It is also common to return the final image as "camera sees" (not mirrored).
LIQA adopts this UX pattern and returns not mirrored image from LIQA sessions that used the front camera. This means that the image retrieved from the ImageCapture API looks flipped compared to what end-user saw during the interaction with LIQA.
To overwrite this default process, LIQA also provides methods to invert or re-apply mirroring to final image. You can achieve it by leveraging two ImageCapture APIs: transform and source. The following code example illustrates how the transformation is applied to the captured image with a conditional logic based on source of the image:
ts
tsif (capture .source === "front_camera")capture =capture .transform ({horizontalFlip : true }) constimageBlob = awaitcapture .blob ()
tsif (capture .source === "front_camera")capture =capture .transform ({horizontalFlip : true }) constimageBlob = awaitcapture .blob ()
Recommendations for usage
- Please, note, that if the image from the front camera is mirrored (the code above is applied), the right side of the face on image would represent the left side of the face in reality. This should be taken into account when SaaS Face Metrics 2.0 are displayed per face area.
- The available sources for image capturing can be configured. Please, follow the Image Sources Customization Guideline.
Convert captured image to Base64-encoded string
Use this option when the next system in your workflow expects Base64 instead of a browser Blob, for example when sending captures to the upload HautAI SaaS API or forwarding an image through a native WebView bridge.
By default, LIQA emits the captured image as Blob, encoded with JPEG with 100% quality, using the blob API. The following code example provides a utility function blobToBase64 that can convert data from Blob to Base64-encoded string:
ts
tsfunctionblobToBase64 (blob ) { return newPromise ((resolve ) => { constreader = newFileReader ()reader .onloadend = () =>resolve (reader .result )reader .readAsDataURL (blob ) }) }
tsfunctionblobToBase64 (blob ) { return newPromise ((resolve ) => { constreader = newFileReader ()reader .onloadend = () =>resolve (reader .result )reader .readAsDataURL (blob ) }) }
Recommendations for usage
Add the utility function from above to the mentioned in a Quick Start section handleImageCapture event listener for the "capture" event as follows:
ts
tsasync functionhandleImageCapture (event ) { constcapture =event .detail constblob = awaitcapture .blob () // returns the captured image as Blob constbase64 = awaitblobToBase64 (blob ) // converts the captured image to Base64 string // ... app logic handling the captured image ... }
tsasync functionhandleImageCapture (event ) { constcapture =event .detail constblob = awaitcapture .blob () // returns the captured image as Blob constbase64 = awaitblobToBase64 (blob ) // converts the captured image to Base64 string // ... app logic handling the captured image ... }
Access anonymized image processing pipeline
Use anonymization when your client workflow needs to minimize identifiable visual data while preserving the skin information needed for analysis, auditing, or privacy-sensitive processing.
Haut.AI's patented Skin Atlas anonymization method ensures the highest data privacy standards while optimizing analysis quality. The process begins with detecting facial keypoints to standardize the image, followed by separating skin pixels from non-skin pixels. The system then generates photo-realistic skin patterns to replace non-skin areas, such as hair and background, producing a fully anonymized image. This eliminates irrelevant data and ensures that only pertinent skin information is retained for analysis. By transforming the images into the Skin Atlas format, anonymization not only safeguards privacy but also enables rapid, secure, and efficient processing of facial images.
LIQA leverages the Skin Atlas method to anonymize images directly on the user’s device, adding an extra layer of security by ensuring that sensitive data never leaves the device unprotected. This approach supports privacy-first workflows, enables reliable audit trails, and strengthens compliance with strict data protection standards, all while maintaining the integrity and quality of image analysis.
Enabling Anonymization
To enable anonymization, you must configure LIQA with the postprocessing parameter set to "anonymized":
html
<hautai-liqa
license="xxx-xxx-xxx"
preset="face"
postprocessing="anonymized"
>
</hautai-liqa>Or when using the JavaScript API:
ts
tsconstliqa = newLiqa ({license : "xxx-xxx-xxx",preset : "face",postprocessing : "anonymized",target : "body" })
tsconstliqa = newLiqa ({license : "xxx-xxx-xxx",preset : "face",postprocessing : "anonymized",target : "body" })
On iOS, very large uploaded images may exceed the platform canvas size limits used during anonymization.
Available options:
"original"(default) – Returns the captured image without anonymization"anonymized"– Enables anonymization and provides access to the full processing pipeline
For complete configuration details, see the postprocessing parameter in the API Reference.
Face-180 preset support: This feature is available for the face-180 preset, providing anonymized image variants for each captured angle - front, left, and right sides of the face. When using face-180, you'll receive anonymized data for each of the three captured images.
Single capture example (Face preset):
ts
tsasync functionhandleImageCapture (event ) { constcaptures =event .detail // For face preset, you'll typically get a single capture for (constcapture ofcaptures ) { // Get anonymized data including all processing stages constanonymized = awaitcapture .anonymized ()console .log (`Processing capture from ${capture .source }`) // Access different image variantsanonymized .blobs .forEach ((blobData ) => {console .log (`${blobData .type }:`,blobData .data ) // Process each image variant as needed }) // Access processing metadataconsole .log ('Transform sequence:',anonymized .transformSequence )console .log ('Face mesh data:',anonymized .mesh ) } }
tsasync functionhandleImageCapture (event ) { constcaptures =event .detail // For face preset, you'll typically get a single capture for (constcapture ofcaptures ) { // Get anonymized data including all processing stages constanonymized = awaitcapture .anonymized ()console .log (`Processing capture from ${capture .source }`) // Access different image variantsanonymized .blobs .forEach ((blobData ) => {console .log (`${blobData .type }:`,blobData .data ) // Process each image variant as needed }) // Access processing metadataconsole .log ('Transform sequence:',anonymized .transformSequence )console .log ('Face mesh data:',anonymized .mesh ) } }
Multiple capture example (Face-180 preset):
ts
tsasync functionhandleImageCapture (event ) { constcaptures =event .detail // Process each capture (face-180 provides 3 captures: front, left, right) for (constcapture ofcaptures ) { // Get anonymized data including all processing stages constanonymized = awaitcapture .anonymized ()console .log (`Processing ${capture .source } capture`) // Access different image variantsanonymized .blobs .forEach ((blobData ) => {console .log (`${blobData .type }:`,blobData .data ) // Process each image variant as needed }) // Access processing metadataconsole .log ('Transform sequence:',anonymized .transformSequence )console .log ('Face mesh data:',anonymized .mesh ) } }
tsasync functionhandleImageCapture (event ) { constcaptures =event .detail // Process each capture (face-180 provides 3 captures: front, left, right) for (constcapture ofcaptures ) { // Get anonymized data including all processing stages constanonymized = awaitcapture .anonymized ()console .log (`Processing ${capture .source } capture`) // Access different image variantsanonymized .blobs .forEach ((blobData ) => {console .log (`${blobData .type }:`,blobData .data ) // Process each image variant as needed }) // Access processing metadataconsole .log ('Transform sequence:',anonymized .transformSequence )console .log ('Face mesh data:',anonymized .mesh ) } }
Available Image Variants
The anonymized() method returns an object containing multiple image processing stages:
originalImage– The unprocessed captured image as received from the camera or uploadimageRestored– Image after initial restoration and enhancement processingsegmentationMask– Binary mask showing detected face and skin regions for privacy processinganonymizedImage– Final anonymized image with embedded metadata for traceability
Processing Metadata
In addition to image variants, you also get access to processing metadata:
transformSequence– Array of transformations applied during the anonymization processmesh– Face mesh coordinates and landmark data used for processing
EXIF Metadata
The anonymized image includes EXIF metadata containing the mesh and transform sequence data, providing full traceability of the anonymization process. This ensures that the processing pipeline can be audited and reproduced if needed.
Recommendations for usage
- Use the
segmentationMaskto understand which areas of the image were processed for privacy - The
originalImageandanonymizedImagepair allows for before/after comparisons - Store the
transformSequenceandmeshdata if you need to reproduce or validate the anonymization process - The color-corrected variant can be useful for applications requiring consistent image appearance across different lighting conditions
- For face-180 preset, process each capture separately as they represent different angles (front, left, right) of the same person
- For face-180 preset, consider the
capture.sourceproperty to identify which angle each anonymized dataset corresponds to