Skip to content

Layout Templates

Layouts define how block regions are arranged inside the shell content area. The template field in the LayoutDescriptor selects which layout to use.

Architecture

All layouts share a common foundation: the BlockRegion component. BlockRegion handles per-block horizontal padding (px-6 by default), respects meta.fullBleed on individual blocks, and provides consistent spacing between blocks via gap.

Built-in layouts are presets that compose BlockRegion in different arrangements. Custom layouts can use BlockRegion the same way, or ignore it entirely for full control.

BlockRegion (shared utility)
  - Per-block horizontal padding (px-6)
  - Skips padding when block has meta.fullBleed: true
  - Consistent gap between blocks (space-y-4)

StackLayout     = BlockRegion(header?) + BlockRegion(content) + BlockRegion(footer?)
SidebarLayout   = BlockRegion(header?) + grid[ BlockRegion(main) | aside ]
DashboardLayout = StatRow(metrics) + BlockRegion(content) + BlockRegion(aside?)
WizardLayout    = StepBar + BlockRegion(content) + ActionsBar

Padding Model

Padding is per-block, per-region — not global. The shell provides zero padding on its content area. Each layout applies vertical padding (py-6) and uses BlockRegion for horizontal per-block padding:

  • Default: each block gets px-6 horizontal padding.
  • Full bleed: blocks with meta.fullBleed: true skip horizontal padding (edge-to-edge rendering).
  • Grid regions: some regions (e.g., DashboardLayout metrics) render blocks without per-block padding because they use a grid where blocks fill cells.
ts
// Example: DenseTable with full bleed in a stack layout
{
    template: 'stack',
    regions: {
        content: [
            { type: 'status_strip', key: 'status', data: { ... } },
            { type: 'dense_table', key: 'users', data: { ... }, meta: { fullBleed: true } },
        ],
    },
}
// Result: status_strip has px-6 padding, dense_table spans edge-to-edge

Overview

TemplateRegionsUse Case
stackcontent, header?, footer?Lists, tables, single-column pages
sidebarmain, aside, header?Detail pages with sidebar panel (320px)
dashboardmetrics, header?, content, aside?KPI overview with metrics row
wizardcontent (+ meta.steps, meta.actions)Multi-step forms with progress indicator

stack

Single column, blocks stacked vertically. The simplest and most common layout.

Regions: content (required), header?, footer?

ts
{
    template: 'stack',
    regions: {
        content: [
            { type: 'status_strip', key: 'status', data: { ... } },
            { type: 'dense_table', key: 'users', data: { ... }, meta: { fullBleed: true } },
        ],
    },
}

Use cases: User lists, settings pages, single-focus views, form pages.

Blocks receive px-6 horizontal padding by default. Set meta.fullBleed: true on a block to render it edge-to-edge (useful for dense_table).

Two-column layout with a main content area and an aside panel.

Regions: main (required), aside (required), header?

ts
{
    template: 'sidebar',
    regions: {
        main: [
            { type: 'detail_panel', key: 'details', data: { ... } },
            { type: 'activity_timeline', key: 'timeline', data: { ... } },
        ],
        aside: [
            { type: 'link_list', key: 'related', data: { ... } },
            { type: 'metric_card', key: 'stats', data: { ... } },
        ],
    },
}

Use cases: Entity detail pages, profile pages, connector settings, documentation with ToC.

The aside panel is 320px wide with a left border and subtle background (border-l border-border bg-muted/30), styled via semantic tokens so themes can override it. It collapses below the main content on mobile.

When the header region is present, it renders as a full-width row above the two columns:

ts
{
    template: 'sidebar',
    regions: {
        header: [
            { type: 'status_strip', key: 'status', data: { ... } },
        ],
        main: [
            { type: 'dense_table', key: 'list', data: { ... }, meta: { fullBleed: true } },
        ],
        aside: [
            { type: 'detail_panel', key: 'selected', data: { ... } },
        ],
    },
}

dashboard

Grid layout with a metrics row at the top and a body area for content blocks.

Regions: metrics (metric cards row), header?, content, aside?

ts
{
    template: 'dashboard',
    regions: {
        metrics: [
            { type: 'metric_card', key: 'users', data: { ... } },
            { type: 'metric_card', key: 'courses', data: { ... } },
            { type: 'metric_card', key: 'completions', data: { ... } },
            { type: 'metric_card', key: 'revenue', data: { ... } },
        ],
        content: [
            { type: 'dense_table', key: 'recent', data: { ... } },
            { type: 'activity_timeline', key: 'activity', data: { ... } },
        ],
    },
}

Use cases: Dashboard overview pages, analytics summaries, admin home.

The metrics region renders as a responsive 4-column grid via StatRow. Blocks in this region do not receive per-block padding (they fill grid cells directly). The content and aside regions use standard BlockRegion padding.

wizard

Multi-step form layout with a progress indicator and action buttons.

Regions: content (blocks for the current step)

Meta: steps (step definitions), actions (navigation buttons)

ts
{
    template: 'wizard',
    regions: {
        content: [
            { type: 'form_panel', key: 'step1-form', data: { ... } },
        ],
    },
    meta: {
        steps: [
            { key: 'basic', label: 'Basic Information', status: 'current' },
            { key: 'config', label: 'Configuration', status: 'upcoming' },
            { key: 'review', label: 'Review', status: 'upcoming' },
        ],
        actions: [
            { id: 'back', label: 'Back', intent: 'ghost', href: '/wizard/back' },
            { id: 'next', label: 'Next', intent: 'primary', href: '/wizard/next' },
        ],
    },
}

Use cases: Onboarding flows, multi-step configuration, setup wizards.

Pair with immersive shell

For focused wizard experiences, use shell: 'immersive' to remove sidebar and navigation chrome entirely. The wizard layout provides its own step indicator and action buttons.

Custom Layouts

Register custom layout components that compose BlockRegion for consistent padding behavior:

tsx
import { registerLayout } from '@middag-io/react';
import type { LayoutProps } from '@middag-io/react';

function MyCustomLayout({ layout, renderBlock }: LayoutProps) {
    const main = layout.regions.main ?? [];
    const aside = layout.regions.aside ?? [];

    return (
        <div className="py-6 flex flex-col gap-5">
            <div className="grid grid-cols-3 gap-4 px-6">
                <div className="col-span-2 space-y-4">
                    {main.map(renderBlock)}
                </div>
                <div className="space-y-4">
                    {aside.map(renderBlock)}
                </div>
            </div>
        </div>
    );
}

registerLayout('my_custom', MyCustomLayout);

Then reference it in the contract:

ts
{
    template: 'my_custom',
    regions: {
        main: [...],
        aside: [...],
    },
}

BlockRegion

Custom layouts can import and use the BlockRegion component from @middag-io/react for automatic per-block padding with fullBleed support, or handle padding manually as shown above.

MIDDAG © 2015-2026