Composables reference
Every section uses one or more of these three composables. This page is the full reference.
useT() — translate a value
import { useT } from '~/composables/useT';
const t = useT();
// Plain string in: Plain string out.
t('Summer sale'); // → 'Summer sale'
// Locale-map in: String in active language out.
t({ en: 'Sale', fr: 'Soldes', ar: 'تخفيضات' });
// → 'تخفيضات' if active lang is AR
// Missing translation: Falls back to default lang, then 'en', then first non-empty.
t({ fr: 'Soldes' }); // (active = AR, default = EN) → 'Soldes'
// Null / undefined: Empty string. No need for `?? ''`.
t(undefined); // → ''Type
type Translatable = string | Record<string, string | undefined> | undefined | null;
function useT(): (value: Translatable) => string;When to use
Any time you render a field marked "translatable": true in the manifest. The value the merchant stored may be either a plain string or a locale map — useT() handles both transparently and returns a string in the shopper's current language.
Reactive
useT() returns a function that closes over the active language (a Pinia ref). When the shopper or merchant switches language, every computed(() => t(value)) re-evaluates automatically.
Common mistake
Forgetting to call it on a translatable field:
// ❌ Bad — renders [object Object] if the value is a locale map
{{ props.config.title }}
// ✅ Good
const title = computed(() => t(props.config?.title) || 'Default');
{{ title }}useStyles() — pick up merchant style edits
import { useStyles, type ElementStylePayload } from '~/composables/useStyles';
const headingStyles = useStyles(
'heading', // key — matches a styleKeys entry in manifest
() => props.config?.styles, // accessor — must be a function (reactive)
['self'], // optional aliases (rare)
'.descendant-class' // optional descendant selector (rare)
);Signature
function useStyles(
elementKey: string,
styles: () => Record<string, ElementStylePayload> | undefined,
aliases?: string[],
descendantSelector?: string
): {
attrs: ComputedRef<{
class: string;
style?: Record<string, string>;
'data-element-key': string;
}>;
};How to bind it
useStyles returns { attrs }. Spread the values onto your element:
<h2
:class="headingStyles.attrs.value.class"
:style="headingStyles.attrs.value.style"
:data-element-key="headingStyles.attrs.value['data-element-key']"
>Or shorthand-bind everything:
<h2 v-bind="headingStyles.attrs.value">{{ title }}</h2>The data-element-key attribute is what the builder UI uses to highlight the element when the merchant hovers over its row in the Style panel. Without it, the visual highlight breaks.
Scopes — 'self' vs descendant selector
Self scope (most common). Use when the element is unique:
const sectionStyles = useStyles('section', stylesAcc, ['self']);
const titleStyles = useStyles('title', stylesAcc);
<section v-bind="sectionStyles.attrs.value">
<h1 v-bind="titleStyles.attrs.value">{{ title }}</h1>
</section>Descendant selector. Use when the element repeats:
// Style every .product-card .price inside the section
const priceStyles = useStyles('cardPrice', stylesAcc, [], '.product-card .price');Then in the template, you do not spread priceStyles onto each price — the generated CSS rule (.section.section .product-card .price { ... }) catches them all automatically. You only need to make sure the matching descendant element exists with the right class:
<section v-bind="sectionStyles.attrs.value">
<ProductCard v-for="p in products" :key="p.id" :product="p" />
<!-- ProductCard's template includes <span class="price">$19.99</span> -->
</section>The merchant's edits on cardPrice then flow through to every price tag.
The aliases parameter
Almost always you pass ['self'] for the section root and omit it elsewhere. 'self' is a legacy key the builder still writes when the merchant has no specific element selected. Aliasing it onto 'section' means "merge whatever's under styles.self into styles.section" — old themes used self exclusively; new ones use named scopes.
useStyles('section', stylesAcc, ['self']); // ← do this on the section root
useStyles('title', stylesAcc); // ← no alias needed for named elementsCommon mistakes
- Passing the styles object directly instead of an accessor function.ts
useStyles('title', props.config?.styles); // ❌ snapshot — won't update useStyles('title', () => props.config?.styles); // ✅ accessor — reactive styleKeyin manifest doesn't match the key in code. Same string both places. Case-sensitive.- Forgetting
data-element-keyon the bound element. Styles still apply but the builder can't highlight the element. Always include it (or usev-bind="attrs.value"to spread all three).
useEditable() — inline click-to-edit
import { useEditable } from '~/composables/useEditable';
const titleEdit = useEditable('title'); // flat field
const slideEdit = useEditable(`slides[${i}].text`); // nested
const headingEdit = useEditable('heading', { translatable: true }); // i18nSignature
function useEditable(
fieldPath: string,
options?: { translatable?: boolean }
): {
editableProps: () => Record<string, unknown>;
};How to bind
v-bind the returned editableProps() onto the element you want clickable:
<h1 v-bind="titleEdit.editableProps()">{{ title }}</h1>What it does
- Outside builder mode: returns empty props. Zero runtime overhead, the element is not editable.
- Inside builder mode (
?builder=truein URL): addscontenteditable, click/blur/Enter handlers, and posts an EDIT message to the parent builder when the merchant finishes typing. The patch path is computed at edit time so it stays correct even after sections are reordered.
The path argument
useEditable(path) takes the manifest field path, not a literal object key. Examples:
| Manifest schema | useEditable(...) call |
|---|---|
{ "path": "title" } | useEditable('title') |
{ "path": "primaryCta.text" } | useEditable('primaryCta.text') |
| Slide #2 inside an array of slides | useEditable('slides[2].text') — usually useEditable(slides[${i}].text) in a v-for |
Translatable fields
Pass { translatable: true } for any field marked translatable in the manifest. Without this option, an edit overwrites the whole locale map with a plain string — losing all other languages:
const headingEdit = useEditable('heading', { translatable: true });
// Edit posts to: theme.config.templates.home[N].values.heading.<activeLang>When to use it
For text fields the merchant will want to edit as they see it (titles, body copy, CTAs). Skip it for:
- Dropdowns / pickers (use the right-hand inspector).
- Images (use the right-hand inspector's image field).
- Things the merchant should not edit in place (computed values, prices from products).
Common mistakes
- Path doesn't match the manifest. If
useEditable('heading')writes tovalues.headingbut the component readsprops.config.title, the edit goes to the wrong key. Match them. - Missed
translatable: true. Edits on{ en: "...", fr: "..." }collapse it to a plain string. Other languages disappear. - Multiple
useEditablecalls on the same path. Each call binds its own contenteditable handler — duplicates fire patches twice. Only one per field per section instance.
Composables you'll rarely touch
These exist but stagiaires usually don't need them. Mentioned for completeness:
| Composable | Purpose |
|---|---|
useThemeStore() (Pinia) | Read the raw theme document. Used by TemplateSections and a few advanced sections. |
useMainStore() (Pinia) | Shopper state — cart, wishlist, language, currency. Useful for cart/wishlist sections. |
useApiBase() | Returns the api-stores URL. Mostly an internal detail. |
useEvents() | Pub/sub for cart/wishlist updates. Use only inside cart-related sections. |
When you genuinely need one of these, look at how an existing section uses it and follow the same pattern.