Skip to content

Layout extensions

A layout controls how items in a collection are browsed in the app. CairnCMS ships built-in layouts (Tabular, Cards, Calendar, Map); a layout extension adds a new one.

A layout is the most structurally complex of the app extensions. It has four Vue components — a main component plus three slots — and a setup function that builds shared state for all of them. All five live inside a single npm package created by the extensions toolchain.

The package’s source entrypoint exports a configuration object built with defineLayout:

import { ref } from 'vue';
import { defineLayout } from '@cairncms/extensions-sdk';
import LayoutComponent from './layout.vue';
import LayoutOptions from './options.vue';
import LayoutSidebar from './sidebar.vue';
import LayoutActions from './actions.vue';
export default defineLayout({
id: 'custom',
name: 'Custom',
icon: 'box',
component: LayoutComponent,
slots: {
options: LayoutOptions,
sidebar: LayoutSidebar,
actions: LayoutActions,
},
setup() {
const name = ref('Custom Layout');
return { name };
},
});

defineLayout is generic in two type parameters, Options and Query, that describe the user-saved options shape and the layout-specific query parameters (sort, limit, page, fields, and so on). Use them to get full TypeScript inference inside the components:

import { defineLayout } from '@cairncms/extensions-sdk';
import type { LayoutOptions, LayoutQuery } from './types';
export default defineLayout<LayoutOptions, LayoutQuery>({
// ...
});

The fields available on a layout configuration object:

  • id — unique key. Scope proprietary layouts with an author or organization prefix.
  • name — display name shown in the layout selector.
  • icon — icon name from the Material icon set or one of CairnCMS’s custom icons.
  • component — the main Vue component, rendered in the page’s content area.
  • slots — an object with three Vue components that render alongside the main component:
    • options — appears in the Layout Options section of the page sidebar
    • sidebar — appears as additional sections in the page sidebar
    • actions — appears in the page header next to the standard action buttons
    • All three are required. Use () => undefined for any slot you do not need.
  • setup — a function that builds shared state for all four components. Receives the standard layout props as the first argument and a context object (with emit) as the second. Returns an object whose properties are merged into every component’s props.
  • smallHeader — when true, the page header is rendered in a more compact style.
  • headerShadow — when false, the drop shadow under the header is suppressed. Useful when the layout has its own header treatment.

setup is where layouts do most of their work. It runs once when the layout mounts and returns reactive state shared with all four components.

setup(props, { emit }) {
const selection = useSync(props, 'selection', emit);
const layoutOptions = useSync(props, 'layoutOptions', emit);
const layoutQuery = useSync(props, 'layoutQuery', emit);
const { collection, filter, search } = toRefs(props);
const { info, primaryKeyField } = useCollection(collection);
const { items, loading, error, totalPages } = useItems(collection, /* query */);
return { selection, layoutOptions, layoutQuery, items, loading, error };
}

Whatever setup returns is bound to all four components as props. Components that need shared state simply declare a matching prop.

The context argument exposes one method:

  • emit(event, ...args) — emit one of update:selection, update:layoutOptions, or update:layoutQuery to write back to the parent. Use useSync to wrap this in a writable computed ref.

Every layout component receives the following props in addition to whatever setup returns:

  • collection — the collection key currently being browsed (or null while loading)
  • selection — array of currently-selected item primary keys
  • layoutOptions — the user’s saved options for this layout (typed as Options from the generic)
  • layoutQuery — the user’s layout query (sort, limit, fields, page; typed as Query from the generic)
  • filter — combined active filter (user + system)
  • filterUser — the user-applied filter
  • filterSystem — system-applied filters (such as those from a preset or shared link)
  • search — the current search string
  • selectMode — whether the layout is in select mode (multi-select via long-press, for example)
  • showSelect — whether selection is allowed: 'none', 'one', or 'multiple'
  • readonly — whether the layout should refuse mutations (during share-link viewing, for example)
  • resetPreset — function that resets the user’s saved preset for this layout
  • clearFilters — function that clears the active filters

The main component emits these events to update writable layout state. setup typically wraps each in useSync:

  • update:selection — change the selected items
  • update:layoutOptions — persist options changes
  • update:layoutQuery — persist query changes

For values returned from setup, an update:<key> event is also available for two-way binding from the slot components.

The SDK ships a few composables tailored to layout development:

  • useSync(props, key, emit) — wraps a prop into a writable ref that emits update:<key> on write. Use this for selection, layoutOptions, and layoutQuery so child components can mutate them directly.
  • useCollection(collectionRef) — returns reactive metadata for a collection: info, fields, primaryKeyField, sortField, and so on.
  • useItems(collectionRef, query) — fetches items from the API with a reactive query. Returns items, itemCount, totalCount, totalPages, loading, error, and helpers like changeManualSort, getItems, getTotalCount.
  • useFilterFields(fields, filter) — utilities for working with filterable field definitions.
  • useLayout(layoutId) — takes a reactive layout ID and returns the wrapper component used to render that layout. This is framework plumbing for route-level rendering; rarely needed in extension code.

These come from @cairncms/extensions-sdk alongside the more general composables.

The SDK exports three composables for reaching internal systems from a layout component:

  • useApi() — an axios instance pre-configured to talk to the CairnCMS API as the current user
  • useStores() — the Pinia stores the app uses internally
  • useExtensions() — the registered-extension catalog
import { useApi, useStores } from '@cairncms/extensions-sdk';
export default {
setup() {
const api = useApi();
const { useCollectionsStore } = useStores();
const collectionsStore = useCollectionsStore();
// ...
},
};

A layout that renders items as a simple list of names. Just a main component with no extra slot work.

src/index.js:

import { defineLayout, useCollection, useItems, useSync } from '@cairncms/extensions-sdk';
import { computed, toRefs } from 'vue';
import LayoutComponent from './layout.vue';
export default defineLayout({
id: 'simple-list',
name: 'Simple list',
icon: 'list',
component: LayoutComponent,
slots: {
options: () => undefined,
sidebar: () => undefined,
actions: () => undefined,
},
setup(props, { emit }) {
const layoutQuery = useSync(props, 'layoutQuery', emit);
const { collection, filter, search } = toRefs(props);
const { primaryKeyField } = useCollection(collection);
const { items, loading } = useItems(collection, {
sort: computed(() => layoutQuery.value?.sort ?? [primaryKeyField.value?.field ?? 'id']),
limit: computed(() => 50),
fields: computed(() => ['*']),
filter,
search,
page: computed(() => 1),
});
return { items, loading };
},
});

src/layout.vue:

<template>
<div class="simple-list">
<p v-if="loading">Loading…</p>
<ul v-else>
<li v-for="item in items" :key="item.id">{{ item.name ?? item.id }}</li>
</ul>
</div>
</template>
<script setup>
defineProps({
items: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
});
</script>

Build with npm run build, then install or symlink the package into a CairnCMS instance. The new layout becomes available in the layout selector for any collection.