> For the complete documentation index, see [llms.txt](https://docs.ecwid.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ecwid.com/site-themes/develop-site-themes/getting-started/make-your-first-section.md).

# Make your first section

In this tutorial, you'll build a **hero banner section** with a background, title, description, image, and a call-to-action button. By the end, you'll understand how Crane sections are structured and how content and design composables work together.

### What You're Building

A hero banner that a store owner can customize through the Lightspeed editor:

* **Background** — solid color or gradient
* **Title** — customizable text, font, size, and color
* **Description** — multi-line text with styling
* **Image** — responsive hero image
* **CTA Button** — configurable label and link

### Download Assets

This tutorial uses sample images for the hero banner and showcase. Download them and keep them ready — you'll copy them into the right directories as you go:

* hero\_webp-2000x1500.jpeg — high-resolution hero image
* hero\_webp-200x150.jpeg — low-resolution hero image
* hero\_banner\_section\_showcase\_1\_preview\.jpeg — showcase preview thumbnail
* reference\_template\_cover\_image.jpeg — template cover image

### Scaffold the Section

From your application root, generate a new section:

```bash
npx @lightspeed/crane@latest init --section hero-banner
```

This creates the following structure inside `sections/hero-banner/`:

```
sections/hero-banner/
├── ExampleSection.vue       # Main Vue component
├── server.ts                # SSR entry point
├── client.ts                # Client hydration entry point
├── type.ts                  # TypeScript type definitions
├── settings/
│   ├── content.ts           # Content editor settings
│   ├── design.ts            # Design editor settings
│   ├── layout.ts            # Layout configuration
│   └── translations.ts      # Translation strings (i18n)
├── component/               # Sub-components used by the section
│   ├── button/
│   ├── image/
│   ├── selectbox/
│   ├── title/
│   └── toggle/
├── entity/                  # Shared types and enums
│   └── color.ts
├── assets/                  # Static assets (images, icons)
└── showcases/               # Showcase configurations (preview presets)
    ├── 1.ts
    ├── 2.ts
    ├── 3.ts
    └── translations.ts
```

Let's walk through the key files.

### Entry Points

#### `server.ts`

The SSR entry point creates the Vue app on the server:

```typescript
import { createVueServerApp } from '@lightspeed/crane-api';
import ExampleSection from './ExampleSection.vue';
import { Content, Design } from './type.ts';

export default createVueServerApp<Content, Design>(ExampleSection);
```

#### `client.ts`

The client entry point hydrates the server-rendered HTML in the browser:

```typescript
import { createVueClientApp } from '@lightspeed/crane-api';
import ExampleSection from './ExampleSection.vue';
import { Content, Design } from './type.ts';

export default createVueClientApp<Content, Design>(ExampleSection);
```

Both entry points pass the `Content` and `Design` types as generics, ensuring full type safety across the SSR pipeline. These files rarely need changes beyond updating the component import.

### Define Content Settings

Edit `settings/content.ts` to define what the store owner can edit. For our hero banner, we need a title, description, image, and button:

```typescript
// settings/content.ts
import { content } from '@lightspeed/crane-api';

export default {
  hero_title: content.inputbox({
    label: '$label.hero_title.label',
    placeholder: '$label.hero_title.placeholder',
  }),
  hero_description: content.textarea({
    label: '$label.hero_description.label',
    placeholder: '$label.hero_description.placeholder',
  }),
  hero_image: content.image({
    label: '$label.hero_image.label',
  }),
  cta_button: content.button({
    label: '$label.cta_button.label',
    defaults: {
      title: '$label.cta_button.defaults.title',
      buttonType: 'HYPER_LINK',
      link: 'https://www.example.com',
    },
  }),
};
```

Each key becomes a content editor field in the Lightspeed admin panel. The `content` builder provides typed factory functions for each editor type. Labels use translation keys (defined in `translations.ts`) for i18n support.

### Define Design Settings

Edit `settings/design.ts` to define the visual customization options:

```typescript
// settings/design.ts
import { design } from '@lightspeed/crane-api';

export default {
  title_design: design.text({
    label: '$label.title_design.label',
    colors: ['#FFFFFF66', '#0000004D', '#00000099'],
    sizes: [18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40],
    defaults: {
      font: 'global.fontFamily.body',
      size: 40,
      bold: true,
      italic: false,
      color: '#313131',
      visible: true,
    },
  }),
  description_design: design.text({
    label: '$label.description_design.label',
    colors: ['#FFFFFF66', '#0000004D', '#00000099'],
    sizes: [12, 13, 14, 15, 16, 17, 18, 20],
    defaults: {
      font: 'global.fontFamily.body',
      size: 18,
      bold: false,
      italic: false,
      color: '#313131',
      visible: true,
    },
  }),
  background_design: design.background({
    label: '$label.background_design.label',
    colors: ['#FFFFFF66', '#0000004D', '#00000099'],
    defaults: {
      style: 'COLOR',
      color: 'global.color.background',
    },
  }),
};
```

The `design` builder provides factory functions for each design editor type. You can specify allowed colors, font sizes, and defaults. These give the store owner control over fonts, colors, and the section background through the Lightspeed editor.

### Define Translations

Edit `settings/translations.ts` to provide the human-readable strings for every `$label.*` key used in your content and design settings. Each language is a separate object keyed by locale code:

```typescript
// settings/translations.ts
import { translation } from '@lightspeed/crane-api';

export default translation.init({
  en: {
    '$label.hero_title.label': 'Title',
    '$label.hero_title.placeholder': 'Enter the hero banner title',
    '$label.hero_description.label': 'Description',
    '$label.hero_description.placeholder': 'Enter a description for the hero banner',
    '$label.hero_image.label': 'Hero Image',
    '$label.cta_button.label': 'Call to Action',
    '$label.cta_button.defaults.title': 'Shop Now',
    '$label.title_design.label': 'Title Style',
    '$label.description_design.label': 'Description Style',
    '$label.background_design.label': 'Background',
  }
});
```

Every `$label.*` key referenced in `content.ts` and `design.ts` must have a corresponding entry here. The `translation.init()` builder takes an object keyed by locale code (`en`, `nl`, `fr`, etc.) — each locale maps every `$label.*` key to its translated string. This is what the store owner sees in the Lightspeed editor UI.

{% hint style="info" %}
**Locales**

The scaffolded section comes with `en`, `nl`, and `fr` locales pre-configured. You can add more locales by adding additional keys to the object.
{% endhint %}

### Define Layout

Edit `settings/layout.ts` to define the available layouts for your section. Each layout is a different arrangement of the section's content — for example, text overlaid on an image vs. text below the image. For a getting-started hero banner, a single layout with empty overrides is sufficient:

```typescript
// settings/layout.ts
import { layout } from '@lightspeed/crane-api';

export default [
  layout.init({
    layoutId: 'Hero_Banner',
    selectedContentSettings: [],
    selectedDesignSettings: [],
  }),
];
```

The file exports an array of layouts created with `layout.init()`. Each layout has:

* **`layoutId`** — a unique identifier for the layout
* **`selectedContentSettings`** — content setting overrides for this layout (empty means use the defaults from `content.ts`)
* **`selectedDesignSettings`** — design setting overrides for this layout (empty means use the defaults from `design.ts`)

{% hint style="info" %}
**Design Overrides**

Layouts let you offer multiple visual variations of the same section. When `selectedDesignSettings` is empty, the layout uses the base design settings as-is. You can add overrides with `layout.designOverride.text()`, `layout.designOverride.background()`, etc. to customize defaults per layout.
{% endhint %}

### Type Definitions

Edit `type.ts` to infer TypeScript types from your settings:

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

{% hint style="info" %}
**Global Types**&#x20;

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

### Build the Component

Now for the main part — edit `ExampleSection.vue` to bring everything together:

```vue
<!-- ExampleSection.vue -->
<script setup lang="ts">
import { computed } from 'vue';
import {
  useInputboxElementContent,
  useTextareaElementContent,
  useImageElementContent,
  useButtonElementContent,
  useTextElementDesign,
  useBackgroundElementDesign,
  Color,
} from '@lightspeed/crane-api';
import { Content, Design } from './type.ts';

// Content composables — reactive data from the editor
const title = useInputboxElementContent<Content>('hero_title');
const description = useTextareaElementContent<Content>('hero_description');
const image = useImageElementContent<Content>('hero_image');
const cta = useButtonElementContent<Content>('cta_button');

// Design composables — style settings from the editor
const titleDesign = useTextElementDesign<Design>('title_design');
const descriptionDesign = useTextElementDesign<Design>('description_design');
const backgroundDesign = useBackgroundElementDesign<Design>('background_design');

// Computed styles
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'
}));

const descriptionStyle = computed(() => ({
  fontSize: `${descriptionDesign.size}px`,
  fontFamily: descriptionDesign.font,
  color: (descriptionDesign.color as Color).hex,
  fontStyle: descriptionDesign.italic ? 'italic' : 'normal',
  fontWeight: descriptionDesign.bold ? 'bold' : 'normal',
}));

// Visibility checks
const showTitle = computed(() => titleDesign.visible && title.hasContent);
const showDescription = computed(() => descriptionDesign.visible && description.hasContent);
</script>

<template>
  <section class="hero-section" :style="backgroundStyle">
    <div class="hero-content">
      <img
        v-if="image.hasContent"
        :src="image.highResolutionDesktopImage"
        class="hero-image"
      />
      <h1 v-if="showTitle" :style="titleStyle">
        {{ title.value }}
      </h1>
      <p v-if="showDescription" :style="descriptionStyle">
        {{ description.value }}
      </p>
      <button
        v-if="cta?.hasTitle && cta?.hasLink"
        class="cta-button"
        @click="cta.performAction"
      >
        {{ cta.title }}
      </button>
    </div>
  </section>
</template>

<style scoped>
.hero-section {
  min-height: 500px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
}

.hero-content {
  max-width: 800px;
  text-align: center;
}

.hero-image {
  max-width: 100%;
  height: auto;
  margin-bottom: 2rem;
}

.cta-button {
  display: inline-block;
  padding: 1rem 2rem;
  text-decoration: none;
  margin-top: 1rem;
  cursor: pointer;
}
</style>
```

#### Key patterns to note

* **Content composables** (`useInputboxElementContent`, `useImageElementContent`, etc.) return reactive data from the store owner's editor input
* **Design composables** (`useTextElementDesign`, `useBackgroundElementDesign`, etc.) return style settings like font, color, and visibility
* **`hasContent`** and **`visible`** — always check these before rendering to handle empty or hidden elements
* **`(titleDesign.color as Color).hex`** — color properties may be global color strings or `Color` objects, so cast when accessing `.hex`
* **Settings use builder functions** — `content.inputbox()`, `design.text()`, etc. imported from `@lightspeed/crane-api`
* **Translation keys** — labels use `$label.*` keys defined in `settings/translations.ts` for i18n support

### Define a Showcase

Showcases are preview presets that demonstrate your section with pre-filled content and design values. They appear in the Lightspeed editor when a store owner browses available sections. You need at least one showcase.

Create `showcases/1.ts`:

```typescript
// showcases/1.ts
import {
  content,
  design,
  showcase,
} from '@lightspeed/crane-api';

export default showcase.init({
  showcaseId: '1',
  previewImage: {
    set: {
      ORIGINAL: {
        url: 'hero_banner_section_showcase_1_preview.jpeg',
        width: 500,
        height: 300,
      },
    },
  },
  blockName: '$label.showcase_1.blockName',
  layoutId: 'Hero_Banner',
  content: {
    hero_title: content.default.inputbox({
      text: '$label.showcase_1.hero_title.text',
    }),
    hero_description: content.default.textarea({
      text: '$label.showcase_1.hero_description.text',
    }),
    hero_image: content.default.image({
      imageData: {
        set: {
          MOBILE_WEBP_LOW_RES: {
            url: 'hero_webp-200x150.jpeg',
          },
          MOBILE_WEBP_HI_RES: {
            url: 'hero_webp-2000x1500.jpeg',
          },
          WEBP_LOW_RES: {
            url: 'hero_webp-200x150.jpeg',
          },
          WEBP_HI_2X_RES: {
            url: 'hero_webp-2000x1500.jpeg',
          },
        },
        borderInfo: {},
      },
    }),
    cta_button: content.default.button({
      title: '$label.showcase_1.cta_button.title',
      buttonType: 'HYPER_LINK',
      link: 'https://www.example.com',
    }),
  },
  design: {
    title_design: design.default.text({
      font: 'global.fontFamily.body',
      size: 40,
      bold: true,
      italic: false,
      color: '#313131',
    }),
    description_design: design.default.text({
      font: 'global.fontFamily.body',
      size: 18,
      bold: false,
      italic: false,
      color: '#313131',
    }),
    background_design: design.default.background({
      style: 'COLOR',
      color: 'global.color.background',
    }),
  },
});
```

Key things to note:

* **`showcase.init()`** — creates a showcase with a unique `showcaseId`
* **`previewImage`** — a preview thumbnail shown in the editor (place the image in `assets/`). Include `width` and `height` to control the preview card's aspect ratio
* **`blockName`** — the display name (uses a translation key)
* **`layoutId`** — must match a layout ID defined in `settings/layout.ts`
* **`content` / `design`** — use `content.default.*()` and `design.default.*()` builders (note: `default` namespace, not the same builders as in settings)
* Content and design keys must match the keys in your `settings/content.ts` and `settings/design.ts`

{% hint style="info" %}
**Cleanup**

You should remove `2.ts` and `3.ts` from the `showcases/` folder. We don't need them for this section.
{% endhint %}

#### Showcase Translations

Create `showcases/translations.ts` with the translated strings for the showcase:

```typescript
// showcases/translations.ts
import { translation } from '@lightspeed/crane-api';

export default translation.init({
  en: {
    '$label.showcase_1.blockName': 'Hero Banner — Default',
    '$label.showcase_1.hero_title.text': 'Welcome to Our Store',
    '$label.showcase_1.hero_description.text': 'Discover our latest collection of products, curated just for you.',
    '$label.showcase_1.cta_button.title': 'Shop Now',
  },
});
```

{% hint style="info" %}
**Translation Scope**

Showcase translations are separate from section translations. Section translations (`settings/translations.ts`) provide labels for the editor UI, while showcase translations (`showcases/translations.ts`) provide the pre-filled content values for each showcase.
{% endhint %}

### Learn more <a href="#next-steps" id="next-steps"></a>

* [Sections](/site-themes/develop-site-themes/sections.md) — Master your knowledge on section components and configurations
* [Section collections](/site-themes/develop-site-themes/sections/section-collections.md) — Create redistributable section packages


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## 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, and the optional `goal` query parameter:

```
GET https://docs.ecwid.com/site-themes/develop-site-themes/getting-started/make-your-first-section.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
