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
useTranslationTranslation helperMulti-language support

Return Properties

Text Composables (useInputboxElementContent, useTextareaElementContent):

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

Button Composable (useButtonElementContent):

  • title — button text label
  • hasTitletrue if button has text
  • hasLinktrue 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):

  • hasContenttrue 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
useLayoutElementDesignReactive<LayoutDesignData>Layout settings
useLogoElementDesignLOGOComputedRef<LogoDesignData>Logo styling

Return Properties

Text Composables (useTextElementDesign, useTextareaElementDesign):

  • visibletrue 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
  • boldtrue if text should be bold
  • italictrue 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):

  • visibletrue 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>

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:

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>