Section Definitions
You have a Laioutr app and you want editors to place a new section on pages through Studio. Every section starts with a definition: a TypeScript object that declares the component name, Studio metadata, configurable fields, and slots for blocks.
import type { SectionDefinition } from '@laioutr-core/core-types/frontend';
declare const defineSection: <const T extends SectionDefinition>(definition: T) => T;
// ---cut---
export const definition = defineSection({
component: 'SectionHeroBanner',
studio: {
label: 'Hero Banner',
description: 'A full-width banner with heading, media, and a call-to-action button.',
previewSrc: '/app-my-app/component-previews/SectionHeroBanner.png',
tags: ['Heroes', 'Banner'],
},
slots: [
{
name: 'default',
studio: { label: 'Content' },
},
],
schema: [
{
label: 'Content',
fields: [
{ type: 'text', name: 'heading', label: 'Heading' },
{ type: 'media', name: 'backgroundImage', label: 'Background Image', allowedTypes: ['image'] },
{ type: 'text', name: 'ctaLabel', label: 'Button Label' },
{ type: 'link', name: 'ctaLink', label: 'Button Link' },
],
},
],
});
The platform reads this definition at three points: Studio uses it to build the sidebar editor, Frontend Core uses it to wire up data and props, and your Vue component uses it to define its props.
Required properties
Every section definition needs these properties:
| Property | Type | Purpose |
|---|---|---|
component | string | The globally registered Vue component name. Must match the component's filename (e.g., SectionHeroBanner.vue registers as 'SectionHeroBanner'). See Section and block naming for why the Section prefix matters. |
studio | object | Metadata shown in the Studio UI. At minimum, provide label. |
slots | SectionSlotDefinition[] | Named insertion points for blocks. Pass an empty array if the section has no slots. |
schema is optional but present on almost every section. rendering is optional and controls runtime behavior like stacking context isolation.
Rendering options
The rendering property controls how the section behaves at the CSS level.
import type { SectionDefinition } from '@laioutr-core/core-types/frontend';
declare const defineSection: <const T extends SectionDefinition>(definition: T) => T;
// ---cut---
export const definition = defineSection({
component: 'SectionStickyHeader',
rendering: { isolate: false },
studio: { label: 'Sticky Header', tags: ['Header'] },
slots: [],
schema: [],
});
true, the section gets its own CSS stacking context via isolation: isolate. Z-index values inside the section cannot leak out and affect sibling sections. Set to false for sections with sticky or fixed elements that must remain visible above subsequent sections. See Z-Ordering for details.The studio object
The studio property controls how the section appears in Studio's component picker and sidebar.
import type { SectionDefinition } from '@laioutr-core/core-types/frontend';
declare const defineSection: <const T extends SectionDefinition>(definition: T) => T;
// ---cut---
export const definition = defineSection({
component: 'SectionImageAndContent',
studio: {
label: 'Image and Content',
description: 'Combine large images or videos with any content.',
previewSrc: '/app-ui/component-previews/SectionImageAndContent.png',
tags: ['Banner', 'Content'],
},
slots: [],
schema: [],
});
public/ directory.Well-known tags
Studio recognizes these tags for built-in category filters:
Banner, Brands, Blog, Checkout & Cart, Content, Customer Relations, Featuring Products, Footer, Grids, Header, Heroes, Navigation, Products, Product Detail Page, Product Listing Page, Testimonials, Sliders, Blank Containers
You can also pass any custom string.
Props wizard
A props wizard lets editors choose a pre-configured variant when they add a section. Studio shows the wizard as a step-by-step flow before the section is placed on the page. Each step presents a set of variants, and the selected variant's props are applied to the new section.
import type { SectionDefinition } from '@laioutr-core/core-types/frontend';
declare const defineSection: <const T extends SectionDefinition>(definition: T) => T;
// ---cut---
export const definition = defineSection({
component: 'SectionHeroBanner',
studio: {
label: 'Hero Banner',
propsWizard: {
steps: [
{
type: 'variant',
title: 'Choose a layout',
input: [
{
id: 'centered',
label: 'Centered',
icon: 'layout',
previewSrc: '/app-my-app/previews/hero-centered.png',
props: { layout: 'centered', sectionStyle: 'full-width' },
},
{
id: 'split',
label: 'Split',
icon: 'container',
previewSrc: '/app-my-app/previews/hero-split.png',
props: { layout: 'split', sectionStyle: 'boxed' },
},
],
},
],
},
},
slots: [],
schema: [],
});
The propsWizard object contains a steps array. Currently only variant steps are supported.
Step properties
'variant' is supported.Variant properties
schema.public/ directory.Defining slots
Slots are named insertion points where editors place blocks. They map directly to Vue <slot> elements in your component.
import type { SectionDefinition } from '@laioutr-core/core-types/frontend';
declare const defineSection: <const T extends SectionDefinition>(definition: T) => T;
// ---cut---
export const definition = defineSection({
component: 'SectionProductShowcase',
studio: { label: 'Product Showcase' },
slots: [
{
name: 'default',
studio: { label: 'Content Blocks' },
},
{
name: 'sidebar',
studio: { label: 'Sidebar' },
restrictTo: ['BlockProductCard', 'BlockPromotion'],
prefer: ['BlockProductCard'],
},
],
schema: [],
});
'default' for the main slot. Must match the <slot name="..."> in your Vue template.isStandalone: false need to appear in this list to be usable in this slot.Pass an empty array (slots: []) if your section does not accept child blocks.
Adding a schema
The schema property defines the fields that appear in the Studio sidebar. Fields are grouped into fieldsets (collapsible panels in the sidebar UI).
schema: [
{
label: 'Content',
fields: [
{ type: 'text', name: 'heading', label: 'Heading' },
{ type: 'richtext', name: 'body', label: 'Body Text' },
],
},
{
label: 'Design',
defaultOpen: false,
fields: [
{ type: 'color', name: 'backgroundColor', label: 'Background Color' },
{
type: 'select',
name: 'layout',
label: 'Layout',
default: 'full-width',
options: [
{ label: 'Full Width', value: 'full-width' },
{ label: 'Boxed', value: 'boxed' },
],
},
],
},
]
Each fieldset can have a label, optional helpText, icon, and defaultOpen (defaults to true).
For the full list of available field types and their options, see Schema Fields.
Wiring the definition to a Vue component
The definition and the component live in the same .vue file. Export the definition from a regular <script lang="ts"> block, then use definitionToProps() in <script setup> to derive Vue props from it.
<script lang="ts">
import { defineSection, definitionToProps } from '@laioutr-core/frontend-core/types';
// ---cut---
export const definition = defineSection({
component: 'SectionHeroBanner',
studio: { label: 'Hero Banner' },
slots: [{ name: 'default', studio: { label: 'Content' } }],
schema: [
{
label: 'Content',
fields: [
{ type: 'text', name: 'heading', label: 'Heading' },
{ type: 'richtext', name: 'body', label: 'Body Text' },
],
},
],
});
</script>
<script setup lang="ts">
const props = defineProps(definitionToProps(definition));
</script>
<template>
<section>
<h1>{{ heading }}</h1>
<div v-html="body" />
<slot />
</section>
</template>
definitionToProps keeps your component's props in sync with the schema. You never declare props by hand. For sections, it also adds a slots prop (typed as an object) so you can access block data for each named slot.
File conventions
Place each section as a single .vue file in your app's sections directory:
src/runtime/app/sections/
SectionHeroBanner.vue
SectionImageAndContent.vue
SectionTestimonialCarousel.vue
Register the directory in your module's registerLaioutrApp call:
// module.ts
registerLaioutrApp({
sections: [resolve('./runtime/app/sections')],
// ...
});
Full example
A section with media, design settings, and a content slot (based on the built-in SectionImageAndContent):
<script lang="ts">
import { defineSection, definitionToProps, toMedia } from '#imports';
export const definition = defineSection({
component: 'SectionImageAndContent',
studio: {
label: 'Image and Content',
description: 'Combine large images or videos with any content.',
previewSrc: '/app-ui/component-previews/SectionImageAndContent.png',
tags: ['Banner', 'Content'],
},
slots: [
{
name: 'default',
studio: { label: 'Content' },
},
],
schema: [
{
label: 'Media',
fields: [
{
type: 'media',
name: 'media',
label: 'Media',
allowedTypes: ['image'],
},
{
type: 'select',
name: 'imageSizeMode',
label: 'Image Size Mode',
default: 'keep-ratio',
options: [
{ label: 'Keep Ratio', value: 'keep-ratio' },
{ label: 'Fill', value: 'fill' },
],
},
],
},
{
label: 'Design',
fields: [
{
type: 'toggle_button',
name: 'sectionStyle',
label: 'Style',
default: 'full-width',
options: [
{ label: 'Full', value: 'full-width' },
{ label: 'Boxed', value: 'boxed' },
],
},
{
type: 'color',
name: 'customBackgroundColor',
label: 'Custom Background Color',
},
{
type: 'select',
name: 'proportions',
label: 'Proportions',
default: '50/50',
options: [
{ label: '50/50', value: '50/50' },
{ label: '60/40', value: '60/40' },
{ label: '70/30', value: '70/30' },
],
},
{
type: 'number',
name: 'sectionHeight',
label: 'Section Height',
default: 0,
},
],
},
],
});
</script>
<script setup lang="ts">
const props = defineProps(definitionToProps(definition));
</script>
<template>
<section>
<img v-if="media" :src="toMedia(media).src" :alt="toMedia(media).alt" />
<div>
<slot />
</div>
</section>
</template>