UI Composables
The Crane API provides Vue 3 composables for accessing content and design data inside your section components. These composables return reactive objects that update automatically when merchants edit values in the Instant Site Editor.
import { useInputboxElementContent, useTextElementDesign } from '@lightspeed/crane-api';Content Composables
Content composables provide access to user-editable content defined in settings/content.ts.
Available Composables
| Composable | Editor Type | Return Type | Purpose |
|---|---|---|---|
useInputboxElementContent | INPUTBOX | Reactive<InputBoxContent> | Single-line text input |
useTextareaElementContent | TEXTAREA | Reactive<TextAreaContent> | Multi-line text input |
useButtonElementContent | BUTTON | Reactive<ButtonContentData> | Button with text and link |
useImageElementContent | IMAGE | Reactive<ImageContent> | Image upload and settings |
useToggleElementContent | TOGGLE | Reactive<ToggleContent> | Boolean toggle switch |
useSelectboxElementContent | SELECTBOX | Reactive<SelectBoxContent> | Dropdown selection |
useDeckElementContent | DECK | Reactive<DeckContent> | Collection of cards |
useCategorySelectorElementContent | CATEGORY_SELECTOR | Reactive<CategorySelector> | Category picker |
useProductSelectorElementContent | PRODUCT_SELECTOR | Reactive<ProductSelector> | Product picker |
useLogoElementContent | LOGO | Reactive<LogoContent> | Logo image |
useMenuElementContent | MENU | Reactive<MenuContent> | Menu items |
useNavigationMenuElementContent | NAVIGATION_MENU | Reactive<MenuContent> | Navigation menu |
useTranslation | — | Translation helper | Multi-language support |
Return Properties
Text Composables (useInputboxElementContent, useTextareaElementContent):
hasContent—trueif field has non-empty textvalue— the text string
Button Composable (useButtonElementContent):
title— button text labelhasTitle—trueif button has texthasLink—trueif button has a link configuredperformAction()— triggers the button action (navigation, scroll, etc.)type— action type (HYPER_LINK,MAIL_LINK,TEL_LINK, etc.)link,email,phone— target values based on type
Image Composable (useImageElementContent):
hasContent—trueif an image is uploadedlowResolutionMobileImage— URL for mobile placeholder (100x200)highResolutionMobileImage— URL for mobile full quality (1000x2000)lowResolutionDesktopImage— URL for desktop placeholder (200x200)highResolutionDesktopImage— URL for desktop full quality (2000x2000)
Example
<script setup lang="ts">
import {
useInputboxElementContent,
useImageElementContent,
useButtonElementContent
} from '@lightspeed/crane-api';
import type { Content } from './type';
const title = useInputboxElementContent<Content>('title');
const heroImage = useImageElementContent<Content>('hero_image');
const ctaButton = useButtonElementContent<Content>('call_to_action');
</script>
<template>
<section class="hero">
<img
v-if="heroImage.hasContent"
:src="heroImage.highResolutionDesktopImage"
/>
<h1 v-if="title.hasContent">{{ title.value }}</h1>
<button
v-if="ctaButton?.hasTitle && ctaButton?.hasLink"
@click="ctaButton.performAction"
>
{{ ctaButton.title }}
</button>
</section>
</template>💡 Key Mapping
The string passed to each composable (e.g., 'title', 'hero_image') must match a key in your content.ts settings file.
Design Composables
Design composables provide access to styling settings defined in settings/design.ts.
Available Composables
| Composable | Editor Type | Return Type | Purpose |
|---|---|---|---|
useTextElementDesign | TEXT | Reactive<TextDesignData> | Text styling (font, size, color) |
useTextareaElementDesign | TEXTAREA | Reactive<TextareaDesignData> | Textarea styling |
useButtonElementDesign | BUTTON | Reactive<ButtonDesignData> | Button styling |
useBackgroundElementDesign | BACKGROUND | Reactive<BackgroundDesignData> | Background color/image |
useImageElementDesign | IMAGE | Reactive<ImageDesignData> | Image styling |
useToggleElementDesign | TOGGLE | Reactive<ToggleDesignData> | Toggle styling |
useSelectboxElementDesign | SELECTBOX | Reactive<SelectboxDesignData> | Selectbox styling |
useLayoutElementDesign | — | Reactive<LayoutDesignData> | Layout settings |
useLogoElementDesign | LOGO | ComputedRef<LogoDesignData> | Logo styling |
Return Properties
Text Composables (useTextElementDesign, useTextareaElementDesign):
visible—trueif element should be displayedsize— font size as number (append'px'for CSS)font— font family stringcolor— Color object with.hex,.rgba,.hslpropertiesbold—trueif text should be bolditalic—trueif text should be italicwhiteSpace— (textarea only) white-space CSS value
Background Composable (useBackgroundElementDesign):
background.type—'solid'or'gradient'background.solid.color— Color object for solid backgroundsbackground.gradient.fromColor/toColor— Color objects for gradients
Button Composable (useButtonElementDesign):
visible—trueif button should be displayedappearance—'solid-button','outline-button', or'text-link'size—'small','medium', or'large'style—'pill','rectangle', or'round-corner'color— Color objectfont— font family string
Example
<script setup lang="ts">
import { computed } from 'vue';
import {
useTextElementDesign,
useBackgroundElementDesign,
} from '@lightspeed/crane-api';
import type { Design } from './type';
const titleDesign = useTextElementDesign<Design>('title_style');
const backgroundDesign = useBackgroundElementDesign<Design>('section_background');
const backgroundStyle = computed(() => {
if (backgroundDesign.background?.type === 'gradient') {
return {
backgroundImage: `linear-gradient(to right, ${backgroundDesign.background.gradient?.fromColor.hex}, ${backgroundDesign.background.gradient?.toColor.hex})`
};
}
return { backgroundColor: backgroundDesign.background?.solid?.color?.hex };
});
const titleStyle = computed(() => ({
fontSize: `${titleDesign.size}px`,
fontFamily: titleDesign.font,
color: (titleDesign.color as Color).hex,
fontStyle: titleDesign.italic ? 'italic' : 'normal',
fontWeight: titleDesign.bold ? 'bold' : 'normal'
}));
</script>
<template>
<section :style="backgroundStyle">
<h1 v-if="titleDesign.visible" :style="titleStyle">
Welcome
</h1>
</section>
</template>Responsive Images
The image composable provides multiple resolution variants for responsive designs:
<template>
<picture v-if="image.hasContent">
<source media="(max-width: 768px)" :srcset="image.highResolutionMobileImage" />
<img :src="image.highResolutionDesktopImage" alt="Hero" />
</picture>
</template>For programmatic control, use a reactive width check:
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { useImageElementContent } from '@lightspeed/crane-api';
import type { Content } from './type';
const image = useImageElementContent<Content>('hero_image');
const windowWidth = ref<number>(typeof window !== 'undefined' ? window.innerWidth : 0);
function onResize() { windowWidth.value = window.innerWidth; }
onMounted(() => { window.addEventListener('resize', onResize); });
onBeforeUnmount(() => { window.removeEventListener('resize', onResize); });
const isMobile = computed(() => windowWidth.value <= 768);
const imageUrl = computed(() =>
isMobile.value ? image.highResolutionMobileImage : image.highResolutionDesktopImage
);
</script>Working with DECK
The DECK composable returns a collection of cards. Use getReactiveRef to access individual card fields:
<script setup lang="ts">
import { computed } from 'vue';
import {
useDeckElementContent,
Card,
EditorTypes,
ImageContent,
TextAreaContent,
InputBoxContent
} from '@lightspeed/crane-api';
import type { Content } from './type';
const imagesRaw = useDeckElementContent<Content>('images');
const images = computed(() => (
imagesRaw?.cards?.map((card: Card) => ({
text: imagesRaw.getReactiveRef(card, EditorTypes.TEXTAREA, 'image_text') as unknown as TextAreaContent | undefined,
content: imagesRaw.getReactiveRef(card, EditorTypes.IMAGE, 'image_content') as unknown as ImageContent | undefined,
link: imagesRaw.getReactiveRef(card, EditorTypes.INPUTBOX, 'image_link') as unknown as InputBoxContent | undefined,
})).filter((image) => (image.content !== undefined && image.content.hasContent))
));
</script>
<template>
<div class="gallery">
<div v-for="(image, index) in images" :key="index" class="gallery-item">
<img :src="image.content?.highResolutionDesktopImage" />
<p v-if="image.text?.hasContent">{{ image.text.value }}</p>
</div>
</div>
</template>💡 Type Casting
Use the as unknown as Type casting pattern for getReactiveRef results, as shown above.
Translation
Use useTranslation for static multi-language text defined in your translations file:
<script setup lang="ts">
import { useTranslation } from '@lightspeed/crane-api';
const { t } = useTranslation();
</script>
<template>
<h1>{{ t('$label.shared.title') }}</h1>
<p>{{ t('$label.shared.description') }}</p>
</template>ℹ️ Static Text Only
Use useTranslation for static text only — text that is the same across all instances of the section. For merchant-editable text, use content composables like useInputboxElementContent.
TypeScript Types
Use InferContentType and InferDesignType to derive types from your settings files:
// type.ts
import ContentSettings from './settings/content.ts';
import DesignSettings from './settings/design.ts';
export type Content = InferContentType<typeof ContentSettings>;
export type Design = InferDesignType<typeof DesignSettings>;These utility types ensure type safety — the generic parameter on composables (e.g., useInputboxElementContent<Content>('title')) validates that the key exists in your settings.
💡 Global Types
InferContentType and InferDesignType are globally available — no import needed.
Best Practices
Conditional Rendering
Always check hasContent or visible before rendering elements:
<h1 v-if="titleDesign.visible && title.hasContent" :style="titleStyle">
{{ title.value }}
</h1>Computed Styles
Use computed properties for style objects to ensure reactivity:
<script setup lang="ts">
const titleStyle = computed(() => ({
fontSize: `${titleDesign.size}px`,
fontFamily: titleDesign.font,
color: (titleDesign.color as Color).hex
}));
</script>SSR vs Client-Only Rendering
Sections use server-side rendering (SSR) — the Vue component renders to HTML on the server first, then hydrates in the browser. Browser APIs like window, document, and addEventListener are not available during SSR.
Guard browser values with typeof window:
const windowWidth = ref<number>(typeof window !== 'undefined' ? window.innerWidth : 0);Use onMounted for client-only logic:
Code inside onMounted only runs in the browser after hydration. Use it for event listeners, DOM measurements, animations, and any browser-API-dependent setup:
import { onMounted, onBeforeUnmount } from 'vue';
onMounted(() => {
initializeSlider();
window.addEventListener('resize', onResize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', onResize);
});General rule: Keep <script setup> top-level code SSR-safe — composables, computed properties, and reactive refs with safe defaults. Move all browser-specific logic into onMounted. If a piece of UI should only render on the client, guard it with a ref that flips in onMounted:
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const isMounted = ref(false);
onMounted(() => { isMounted.value = true; });
</script>
<template>
<div v-if="isMounted">
<!-- Client-only content -->
</div>
</template>