Skip to content

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.

typescript
import { useInputboxElementContent, useTextElementDesign } from '@lightspeed/crane-api';

Content Composables ​

Content composables provide access to user-editable content defined in settings/content.ts.

Available Composables ​

ComposableEditor TypeReturn TypePurpose
useInputboxElementContentINPUTBOXReactive<InputBoxContent>Single-line text input
useTextareaElementContentTEXTAREAReactive<TextAreaContent>Multi-line text input
useButtonElementContentBUTTONReactive<ButtonContentData>Button with text and link
useImageElementContentIMAGEReactive<ImageContent>Image upload and settings
useToggleElementContentTOGGLEReactive<ToggleContent>Boolean toggle switch
useSelectboxElementContentSELECTBOXReactive<SelectBoxContent>Dropdown selection
useDeckElementContentDECKReactive<DeckContent>Collection of cards
useCategorySelectorElementContentCATEGORY_SELECTORReactive<CategorySelector>Category picker
useProductSelectorElementContentPRODUCT_SELECTORReactive<ProductSelector>Product picker
useLogoElementContentLOGOReactive<LogoContent>Logo image
useMenuElementContentMENUReactive<MenuContent>Menu items
useNavigationMenuElementContentNAVIGATION_MENUReactive<MenuContent>Navigation menu
useTranslation—Translation helperMulti-language support

Return Properties ​

Text Composables (useInputboxElementContent, useTextareaElementContent):

  • hasContent — true if field has non-empty text
  • value — the text string

Button Composable (useButtonElementContent):

  • title — button text label
  • hasTitle — true if button has text
  • hasLink — true if button has a link configured
  • performAction() — 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 — true if an image is uploaded
  • lowResolutionMobileImage — 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 ​

vue
<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 ​

ComposableEditor TypeReturn TypePurpose
useTextElementDesignTEXTReactive<TextDesignData>Text styling (font, size, color)
useTextareaElementDesignTEXTAREAReactive<TextareaDesignData>Textarea styling
useButtonElementDesignBUTTONReactive<ButtonDesignData>Button styling
useBackgroundElementDesignBACKGROUNDReactive<BackgroundDesignData>Background color/image
useImageElementDesignIMAGEReactive<ImageDesignData>Image styling
useToggleElementDesignTOGGLEReactive<ToggleDesignData>Toggle styling
useSelectboxElementDesignSELECTBOXReactive<SelectboxDesignData>Selectbox styling
useLayoutElementDesign—Reactive<LayoutDesignData>Layout settings
useLogoElementDesignLOGOComputedRef<LogoDesignData>Logo styling
useAccordionElementDesignACCORDIONReactive<AccordionDesignData>Accordion nested editors

Return Properties ​

Text Composables (useTextElementDesign, useTextareaElementDesign):

  • visible — true if element should be displayed
  • size — font size as number (append 'px' for CSS)
  • font — font family string
  • color — Color object with .hex, .rgba, .hsl properties
  • bold — true if text should be bold
  • italic — true if text should be italic
  • whiteSpace — (textarea only) white-space CSS value

Background Composable (useBackgroundElementDesign):

  • background.type — 'solid' or 'gradient'
  • background.solid.color — Color object for solid backgrounds
  • background.gradient.fromColor / toColor — Color objects for gradients

Button Composable (useButtonElementDesign):

  • visible — true if button should be displayed
  • appearance — 'solid-button', 'outline-button', or 'text-link'
  • size — 'small', 'medium', or 'large'
  • style — 'pill', 'rectangle', or 'round-corner'
  • color — Color object
  • font — font family string

Example ​

vue
<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>

Accordion Composable ​

The useAccordionElementDesign composable provides access to design editors nested inside an ACCORDION design editor. It returns a reactive object containing items, where each item holds a label and an editors map of nested design editor values.

Usage ​

vue
<script setup lang="ts">
import {
  useAccordionElementDesign,
  SelectboxDesignData,
  ToggleDesignData,
} from '@lightspeed/crane-api';
import type { Design } from './type';

const styling = useAccordionElementDesign<Design>('styling');

const layout = styling.items?.['page_settings']?.editors?.layout as SelectboxDesignData | undefined;
const showFilters = styling.items?.['page_settings']?.editors?.showFilters as ToggleDesignData | undefined;
const cardSize = styling.items?.['card_settings']?.editors?.cardSize as SelectboxDesignData | undefined;
</script>

<template>
  <div :class="['catalog', `layout-${layout?.value}`]">
    <aside v-if="showFilters?.enabled" class="filters">
      <!-- filter panel -->
    </aside>
    <div :class="['products', `card-${cardSize?.value}`]">
      <!-- product grid -->
    </div>
  </div>
</template>

💡 Key Mapping

The first argument to useAccordionElementDesign (e.g., 'styling') must match the key in your design.ts settings file. Access nested editors via items?.['itemKey']?.editors?.editorKey.

Responsive Images ​

The image composable provides multiple resolution variants for responsive designs:

vue
<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:

vue
<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. When a card’s settings include a nested DECK (e.g. slides with link decks), pass EditorTypes.DECK as the editor type to get a reactive deck content object with the same shape (hasContent, cards, getReactiveRef), subject to the max nesting depth of one level.

vue
<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:

vue
<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:

typescript
// 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:

vue
<h1 v-if="titleDesign.visible && title.hasContent" :style="titleStyle">
  {{ title.value }}
</h1>

Computed Styles ​

Use computed properties for style objects to ensure reactivity:

vue
<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:

typescript
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:

typescript
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:

vue
<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>