viewa/docs
Widget

Theming

Make the widget look like part of your site, and choose how much of it shows.

The widget's styles live inside its shadow root, where a utility class from your theme cannot reach them. So every knob is a CSS custom property, set on the element from your own stylesheet. That is the whole theming contract.

viewa-visualizer {
  --viewa-aspect: 4 / 5;
  --viewa-accent: #101012;
  --viewa-radius: 14px;
}

Typography is deliberately not on the list. Type inherits from your page, so the visualizer reads as part of the store rather than an iframe bolted on.

Properties

PropertyWhat it sets
--viewa-aspectThe shape of the widget, in every step. 1 by default
--viewa-min-heightA floor for the whole widget, for pinning it outright. 0 by default
--viewa-accentThe primary color: buttons, area outlines, handles, sliders
--viewa-accent-contrastWhat sits on the accent, and label outlines over a dark photo
--viewa-surfaceThe widget's own surface
--viewa-stageThe ground a photo is letterboxed against. transparent by default, so the surface shows through
--viewa-textBody text
--viewa-mutedSecondary text
--viewa-borderHairlines and outlines
--viewa-dangerError copy
--viewa-radiusCorner radius for the frame and buttons
--viewa-handle-sizeThe drawn corner dot. The tap target stays 44px whatever this is
--viewa-font-sizeThe base every type size is a multiple of. 1rem by default

The type scale is one property rather than one per label. Every size in the widget is an em off --viewa-font-size, so raising it scales them together and keeps the proportions the design has:

viewa-visualizer { --viewa-font-size: 15px; }

1rem by default, which is what the sizes were relative to before it existed, so setting nothing changes nothing. inherit makes the widget follow whatever its container is set to.

--viewa-aspect is the one that keeps your page still. The widget is a square by default and holds that shape from the upload panel through to the finished render, with the photo contained inside it — so nothing below the widget moves as the customer works through the steps. Set it to whatever your gallery slot is. auto gives back the older behaviour of following each photo's own proportions, jumps included.

The area toolbar sits inside that shape rather than floating over the photo, so it can never cover a corner the customer is trying to drag. The photo takes the space the toolbar leaves; the two together always add up to the same height.

The ground a photo is letterboxed against is --viewa-stage, transparent by default so the photo sits on the same surface as the rest of the widget. Set a colour only if you want the photo lifted off the page; a dark one reads as a lightbox — as though your page had been replaced by a viewer — where this is a photo inside a product gallery.

One thing is deliberately not themeable: the corner handles keep a 44px tap target however small you draw them.

Your logo, or none

The upload panel carries the Viewa lockup by default. It is a large brand statement on your product page, so you can replace it or remove it:

<viewa-visualizer publishable-key="pk_…" logo="/assets/our-logo.svg"></viewa-visualizer>

<viewa-visualizer publishable-key="pk_…" logo="none"></viewa-visualizer>

A data: URI is worth preferring over a URL. The widget is a single file with nothing to fetch, and an <img src> pointing at another origin is a request that can fail on its own — a logo that fails to load removes itself rather than leaving a broken-image glyph, but a gap is still not what you asked for.

This covers the panel mark. The small "powered by viewa" attribution underneath is separate, and not currently configurable.

Choosing what is drawn

ui controls which pieces the widget draws for itself. It matters because a product page's gallery slot is mostly photo, and everything the widget adds competes with that.

PartWhat it drawsDefault
chromeThe layer of controls over the photo. The umbrella — with it off, none of the parts below are drawn eitherOn
controlsThe pad below the photoOn
zoomThe zoom button and its sliderOn
measurementsThe sizes drawn on the photo, and the ruler that toggles themOn
compareThe before/after wipe over a finished renderOn
downloadThe save-photo buttonOn
change-photoThe change-photo buttonOn
coachThe first-run "align your area" tipsOff
photos"Use a photo from before" on the upload screenOff
historyThe previous-renders stripOff
<!-- the photo and nothing else; you draw the rest -->
<viewa-visualizer publishable-key="pk_…" ui="none"></viewa-visualizer>

<!-- everything except the before/after wipe -->
<viewa-visualizer
  publishable-key="pk_…"
  ui="chrome controls zoom measurements download change-photo"
></viewa-visualizer>

Declaring anything replaces the whole set rather than adding to the default. That is what makes the fine-grained parts usable as subtractions: write out the ones you want. It also means ui="chrome" keeps meaning exactly what it means today, rather than quietly picking up whatever gets added in a later release.

A value the widget does not recognize falls back to the default rather than to nothing — a typo in a template should not strip the controls a customer needs to finish.

With ui="none" you own every control, including the one that commits the areas. See the JS API for what you need to drive.

How far this goes

Four layers, loosest first. Most storefronts need the first two.

To changeUse
Colours, radius, shape, type scaleThe --viewa-* properties
Your logo instead of ours, or nonelogo
Which controls exist at allui, per part or none
The controls themselvesui="none" and the JS API

ui="none" is the escape hatch. The widget draws the photo and the corner editor; your theme draws everything else against getStatus() and the JS API. That covers a fully bespoke interface without giving up the pipeline — the upload, the measurement, the render and the visitor token all still work.

Past that is a direct API integration, and the line is worth naming: the widget exists so you do not have to hold the pipeline's state machine. Once you want an interface it cannot express, you want the API, not more attributes.

Next

  • Embedding — the attributes, and splitting the journey across pages
  • JS API — pushing selections in, reading state back out

On this page