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
| Property | What it sets |
|---|---|
--viewa-aspect | The shape of the widget, in every step. 1 by default |
--viewa-min-height | A floor for the whole widget, for pinning it outright. 0 by default |
--viewa-accent | The primary color: buttons, area outlines, handles, sliders |
--viewa-accent-contrast | What sits on the accent, and label outlines over a dark photo |
--viewa-surface | The widget's own surface |
--viewa-stage | The ground a photo is letterboxed against. transparent by default, so the surface shows through |
--viewa-text | Body text |
--viewa-muted | Secondary text |
--viewa-border | Hairlines and outlines |
--viewa-danger | Error copy |
--viewa-radius | Corner radius for the frame and buttons |
--viewa-handle-size | The drawn corner dot. The tap target stays 44px whatever this is |
--viewa-font-size | The 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.
| Part | What it draws | Default |
|---|---|---|
chrome | The layer of controls over the photo. The umbrella — with it off, none of the parts below are drawn either | On |
controls | The pad below the photo | On |
zoom | The zoom button and its slider | On |
measurements | The sizes drawn on the photo, and the ruler that toggles them | On |
compare | The before/after wipe over a finished render | On |
download | The save-photo button | On |
change-photo | The change-photo button | On |
coach | The first-run "align your area" tips | Off |
photos | "Use a photo from before" on the upload screen | Off |
history | The previous-renders strip | Off |
<!-- 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 change | Use |
|---|---|
| Colours, radius, shape, type scale | The --viewa-* properties |
| Your logo instead of ours, or none | logo |
| Which controls exist at all | ui, per part or none |
| The controls themselves | ui="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.