Theming
Every override targets the .treatment-consultant class (or any ancestor of it) with CSS custom properties — no build-time theming step needed.
Colour ramps
The component reads four semantic 11-step ramps (00 light through 10 dark): --brand-*, --success-*, --warning-* and --error-*. Import the defaults from srcdev-hair-treatments/dist/tokens.css, or supply your own to match your brand:
:root {
--brand-00: oklch(98% 0.0086 195);
--brand-01: oklch(94% 0.0342 195);
/* ...through */
--brand-10: oklch(25% 0.1216 195);
--success-00: oklch(98% 0.0086 157);
/* ... */
--warning-00: oklch(98% 0.0086 50);
/* ... */
--error-00: oklch(98% 0.0086 30);
/* ... */
}This is the same idea GuideMyHair's own marketing site uses to theme the widget — in app/assets/styles/main.css, --brand-00 through --brand-10 are aliased to the site's own teal design-system ramp instead of hand-written OKLCH values, replacing the package's default warm-orange brand colour entirely:
:root {
--brand-00: var(--teal-00);
--brand-01: var(--teal-01);
/* ...through */
--brand-09: var(--teal-09);
--brand-10: oklch(25% 0.0896 195);
}Images
Hair type, colour, and granular shade options can each show a real photo instead of (or alongside) a flat colour swatch. The package ships a full set of default images — one per hair type, plus the entire default shades / naturalShades catalog — under public/images/treatment-consultant/, copied into your own app's public/ directory by the setup:assets script (see Installation). Nothing renders from node_modules directly — every image path the component uses at runtime is one your app itself serves.
Every option type with visual swatches carries the same optional fields:
| Field | Type | Used on |
|---|---|---|
image | string | HairTypeOption, ColourOption (naturalColours), DesiredColourOption, DesiredShadeOption, NaturalShadeOption |
image2x | string | DesiredShadeOption, NaturalShadeOption only — a 2x-density variant rendered via srcset for crisp high-DPI rendering |
Both are optional and fall back gracefully when omitted: a hair type option without image falls back to its ASCII pattern text; a colour/shade option without image falls back to a flat swatch in its colour; a shade without image2x just renders image at every density (no broken-image risk from a guessed URL). To override an image, replace the field with a path to your own asset — same shape as the defaults, no special API:
import { defaultConfig } from "srcdev-hair-treatments";
const config = {
hairTypes: defaultConfig.hairTypes.map((t) =>
t.id === "curly" ? { ...t, image: "/images/my-salon/curly.webp" } : t
),
};See Option types for the full shape of each option, and Granular shade catalog mode for where shades / naturalShades come from.
Fonts
| Property | Used for | Default |
|---|---|---|
--treatment-consultant-font-body | Body text, sublabels, notes | system-ui, sans-serif |
--treatment-consultant-font-heading | Step titles, Results title, section titles | Georgia, Cambria, "Times New Roman", Times, serif |
--treatment-consultant-font-label | Progress status, option labels | Same as font-body |
.treatment-consultant {
--treatment-consultant-font-body: "My Font", sans-serif;
--treatment-consultant-font-heading: "My Heading Font", serif;
}Other tokens
| Property | Used for |
|---|---|
--treatment-consultant-max-inline-size | Overall width cap on the widget (default 1216px) |
--treatment-consultant-checked-surface-colour | Background of the checkmark badge on a selected option |
--treatment-consultant-checked-stroke-colour | Checkmark icon colour |
--treatment-consultant-emerald | "Great" suitability / "ok" compatibility badge on the Results step |
--treatment-consultant-amber | "Possible" suitability badge |
--treatment-consultant-orange | "Difficult" suitability badge, colour-compatibility warning badges |
--treatment-consultant-red | "Not recommended" suitability badge |
--shade-tabs-fade-size | Width of the scroll-edge fade hinting more tabs, shown in granular shade catalog mode (Premium) |
Option buttons & swatches
Every selectable card — hair type, colour, application, cut, treatment, and the Results summary cards — is an OptionButton, optionally paired with an OptionSwatch for colour/image steps. Both expose a full set of custom properties, each with a matching -hover variant for the hover/focus-visible state that falls back to its resting counterpart unless overridden:
.treatment-consultant {
/* Option card button */
--option-gap: var(--_spacing-sm);
--option-border-width: 1px;
--option-border-color: var(--_border-active);
--option-border-radius: var(--_border-radius);
--option-outline-width: 2px;
--option-outline-color: var(--_outline-default); /* transparent — invisible until hover */
--option-outline-offset: 0;
--option-background-color: var(--_surface-active);
--option-padding-block: var(--_spacing-lg);
--option-padding-inline: var(--_spacing-lg);
--option-font-size: var(--_font-size-md);
--option-font-weight: 400;
/* Hover / focus-visible */
--option-border-width-hover: 1px;
--option-border-color-hover: var(--_border-active);
--option-outline-width-hover: 2px;
--option-outline-color-hover: var(--_outline-active);
--option-outline-offset-hover: 0;
/* Colour/image swatch (hair type, colour steps, results summary) */
--option-swatch-size: 100px;
--option-swatch-border-width: 1px;
--option-swatch-border-color: var(--_border-active);
--option-swatch-outline-width: 2px;
--option-swatch-outline-color: var(--_outline-default); /* transparent — invisible until hover */
--option-swatch-outline-offset: 0px;
--option-swatch-img-scale: 1;
/* Swatch hover / focus-visible, triggered by the parent option button */
--option-swatch-border-width-hover: 1px;
--option-swatch-outline-width-hover: 2px;
--option-swatch-img-scale-hover: 1;
}Swatch shape is controlled separately, via behaviour.swatchVariant (circle / square / portrait / landscape) — see Configuration → behaviour.
The outline colour defaults are theme-driven rather than fixed values — --_outline-default is transparent, so the ring is invisible at rest and only shows on hover/focus, where it switches to --_outline-active. See Themes below for where the theme comes from.
Themes
behaviour.theme switches the component's colour treatment as a whole, on top of any customisation — set it via the behaviour config config. Default "default".
| Value | Description |
|---|---|
"default" | The existing dark, brand-coloured canvas with light text. |
"monochrome-light" | White canvas, foreground darkening from white through greys to near-black. Fully desaturated regardless of any --brand-* customisation. |
"monochrome-dark" | Same dark-canvas/light-text layout as "default", with the brand hue removed. |
const config = { behaviour: { theme: "monochrome-light" } };Both monochrome themes are built on a dedicated chroma-0 grey ramp (--mono-00…--mono-10, shipped in srcdev-hair-treatments/dist/tokens.css) rather than --brand-*, so they stay fully desaturated even if you've customised your app's brand ramp.
Sharp corners
Set behaviour.sharpCorners to true to square off every rounded corner — option cards, buttons, panels, the license watermark. Deliberately circular/pill shapes (the circle swatch, the checkmark badge, progress dots/bars) keep their own radius and are unaffected. Default false.
const config = { behaviour: { sharpCorners: true } };Sizing & your page's root font-size
No html { font-size: 62.5% } reset is needed to use this component. It sizes itself via a local --_unit custom property (default 1px) instead of raw rem, so it renders at the same intended pixel sizes regardless of your app's root font-size. If you'd rather have it scale with your page's root font-size or the user's browser text-zoom setting, opt in explicitly:
.treatment-consultant { --_unit: 0.0625rem; } /* 1px at a default 16px root */SSR & ClientOnly
TreatmentConsultant is a plain client-side Vue component — it has no server-rendering support. In a Nuxt app (or any SSR framework), rendering it during SSR will 500 on a production build. Wrap it in <ClientOnly>:
<ClientOnly>
<TreatmentConsultant />
</ClientOnly>This is a hard requirement, not a workaround for a bug that'll be fixed later — always wrap it when embedding on a server-rendered page.