# 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 <a href="#content-composables" id="content-composables"></a>

Content composables provide access to user-editable content defined in [Content Editors](/site-themes/develop-site-themes/sections/settings/content-editors.md).

#### Available Composables <a href="#available-composables" id="available-composables"></a>

<table><thead><tr><th width="302.1796875">Composable</th><th>Editor Type</th><th width="147.09765625">Return Type</th><th>Purpose</th></tr></thead><tbody><tr><td><code>useInputboxElementContent</code></td><td>INPUTBOX</td><td><code>Reactive&#x3C;InputBoxContent></code></td><td>Single-line text input</td></tr><tr><td><code>useTextareaElementContent</code></td><td>TEXTAREA</td><td><code>Reactive&#x3C;TextAreaContent></code></td><td>Multi-line text input</td></tr><tr><td><code>useButtonElementContent</code></td><td>BUTTON</td><td><code>Reactive&#x3C;ButtonContentData></code></td><td>Button with text and link</td></tr><tr><td><code>useImageElementContent</code></td><td>IMAGE</td><td><code>Reactive&#x3C;ImageContent></code></td><td>Image upload and settings</td></tr><tr><td><code>useToggleElementContent</code></td><td>TOGGLE</td><td><code>Reactive&#x3C;ToggleContent></code></td><td>Boolean toggle switch</td></tr><tr><td><code>useSelectboxElementContent</code></td><td>SELECTBOX</td><td><code>Reactive&#x3C;SelectBoxContent></code></td><td>Dropdown selection</td></tr><tr><td><code>useDeckElementContent</code></td><td>DECK</td><td><code>Reactive&#x3C;DeckContent></code></td><td>Collection of cards</td></tr><tr><td><code>useCategorySelectorElementContent</code></td><td>CATEGORY_SELECTOR</td><td><code>Reactive&#x3C;CategorySelector></code></td><td>Category picker</td></tr><tr><td><code>useProductSelectorElementContent</code></td><td>PRODUCT_SELECTOR</td><td><code>Reactive&#x3C;ProductSelector></code></td><td>Product picker</td></tr><tr><td><code>useLogoElementContent</code></td><td>LOGO</td><td><code>Reactive&#x3C;LogoContent></code></td><td>Logo image</td></tr><tr><td><code>useAccordionElementDesign</code></td><td>ACCORDION</td><td><code>Reactive&#x3C;AccordionDesignData></code></td><td>Accordion nested editors</td></tr><tr><td><code>useMenuElementContent</code></td><td>MENU</td><td><code>Reactive&#x3C;MenuContent></code></td><td>Menu items</td></tr><tr><td><code>useNavigationMenuElementContent</code></td><td>NAVIGATION_MENU</td><td><code>Reactive&#x3C;MenuContent></code></td><td>Navigation menu</td></tr><tr><td><code>useTranslation</code></td><td>—</td><td>Translation helper</td><td>Multi-language support</td></tr></tbody></table>

#### Return Properties <a href="#return-properties" id="return-properties"></a>

**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 <a href="#example" id="example"></a>

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

{% hint style="info" icon="sliders" %}
**Key Mapping**

The string passed to each composable (e.g., `'title'`, `'hero_image'`) must match a key in your `content.ts` settings file.
{% endhint %}

### Design Composables <a href="#design-composables" id="design-composables"></a>

Design composables provide access to styling settings defined in [Design Editors](/site-themes/develop-site-themes/sections/settings/design-editors.md)

#### Available Composables <a href="#available-composables-1" id="available-composables-1"></a>

<table><thead><tr><th width="239.68359375">Composable</th><th width="139.6328125">Editor Type</th><th width="221.91796875">Return Type</th><th>Purpose</th></tr></thead><tbody><tr><td><code>useTextElementDesign</code></td><td>TEXT</td><td><code>Reactive&#x3C;TextDesignData></code></td><td>Text styling (font, size, color)</td></tr><tr><td><code>useTextareaElementDesign</code></td><td>TEXTAREA</td><td><code>Reactive&#x3C;TextareaDesignData></code></td><td>Textarea styling</td></tr><tr><td><code>useButtonElementDesign</code></td><td>BUTTON</td><td><code>Reactive&#x3C;ButtonDesignData></code></td><td>Button styling</td></tr><tr><td><code>useBackgroundElementDesign</code></td><td>BACKGROUND</td><td><code>Reactive&#x3C;BackgroundDesignData></code></td><td>Background color/image</td></tr><tr><td><code>useImageElementDesign</code></td><td>IMAGE</td><td><code>Reactive&#x3C;ImageDesignData></code></td><td>Image styling</td></tr><tr><td><code>useToggleElementDesign</code></td><td>TOGGLE</td><td><code>Reactive&#x3C;ToggleDesignData></code></td><td>Toggle styling</td></tr><tr><td><code>useSelectboxElementDesign</code></td><td>SELECTBOX</td><td><code>Reactive&#x3C;SelectboxDesignData></code></td><td>Selectbox styling</td></tr><tr><td><code>useLayoutElementDesign</code></td><td>—</td><td><code>Reactive&#x3C;LayoutDesignData></code></td><td>Layout settings</td></tr><tr><td><code>useLogoElementDesign</code></td><td>LOGO</td><td><code>ComputedRef&#x3C;LogoDesignData></code></td><td>Logo styling</td></tr></tbody></table>

#### Return Properties <a href="#return-properties-1" id="return-properties-1"></a>

**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 <a href="#example-1" id="example-1"></a>

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

{% hint style="info" icon="tag" %}
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`.
{% endhint %}

### Responsive Images <a href="#responsive-images" id="responsive-images"></a>

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:

```typescript
<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 <a href="#working-with-deck" id="working-with-deck"></a>

The `DECK` composable returns a collection of cards. Use `getReactiveRef` to access individual card fields. When a card’s settings include a nested `DECK` – 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.

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

{% hint style="info" %}
**Type Casting**

Use the `as unknown as Type` casting pattern for `getReactiveRef` results, as shown above.
{% endhint %}

### Translation <a href="#translation" id="translation"></a>

Use `useTranslation` for static multi-language text defined in your [translations file](/site-themes/develop-site-themes/sections/settings/translations.md):

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

{% hint style="info" %}
**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`.
{% endhint %}

### TypeScript Types <a href="#typescript-types" id="typescript-types"></a>

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.

{% hint style="info" %}
**Global Types**

`InferContentType` and `InferDesignType` are globally available — no import needed.
{% endhint %}

### Best Practices <a href="#best-practices" id="best-practices"></a>

#### Conditional Rendering <a href="#conditional-rendering" id="conditional-rendering"></a>

Always check `hasContent` or `visible` before rendering elements:

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

#### Computed Styles <a href="#computed-styles" id="computed-styles"></a>

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 <a href="#ssr-vs-client-only-rendering" id="ssr-vs-client-only-rendering"></a>

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

<br>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.ecwid.com/site-themes/develop-site-themes/sections/ui-composables.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
