---
title: Layout
description: Responsive column layout component for organizing page content in 1, 2, or 3 columns
---

# Layout

`Layout` is a responsive column layout component that helps you organize page content into 1, 2, or 3 columns with automatic responsive behavior. Perfect for detail pages, dashboards, and forms.

[Live preview in the UI Catalogue →](https://ui.tailor.tech/components/layout)

## Import

```tsx
import { Layout } from "@tailor-platform/app-shell";
```

## Basic Usage

The column count is auto-detected from the number of `Layout.Column` children:

```tsx
<Layout>
  <Layout.Column>
    <h2>Main Content</h2>
    <p>Your main content goes here...</p>
  </Layout.Column>
  <Layout.Column>
    <h2>Sidebar</h2>
    <p>Additional info or actions...</p>
  </Layout.Column>
</Layout>
```

## Props

### Layout Props

| Prop        | Type                | Default      | Description                                                                             |
| ----------- | ------------------- | ------------ | --------------------------------------------------------------------------------------- |
| `className` | `string`            | -            | Additional CSS classes for container                                                    |
| `style`     | `CSSProperties`     | -            | Inline styles for the layout container                                                  |
| `children`  | `Layout.Column[]`   | **Required** | `Layout.Header` and/or `Layout.Column` components                                       |
| `fill`      | `boolean`           | `false`      | Fill the available height instead of growing with content (see [Fill Mode](#fill-mode)) |
| `columns`   | `1 \| 2 \| 3`       | -            | **(Deprecated)** Auto-detected from `Layout.Column` children count when omitted         |
| `title`     | `string`            | -            | **(Deprecated)** Use `<Layout.Header title="...">` instead                              |
| `actions`   | `React.ReactNode[]` | -            | **(Deprecated)** Use `<Layout.Header actions={[...]}>` instead                          |
| `gap`       | `number`            | `4`          | **(Deprecated)** Use `className` (e.g. `className="gap-6"`) instead                     |

### Layout.Header Props

`Layout.Header` is a compound sub-component for page-level headers. Place it as a direct child of `<Layout>`, above any `<Layout.Column>` children.

| Prop       | Type                | Default | Description                                                |
| ---------- | ------------------- | ------- | ---------------------------------------------------------- |
| `title`    | `string`            | -       | Page title (left side, rendered as `<h1>`)                 |
| `actions`  | `React.ReactNode[]` | -       | Action buttons or nodes (right side)                       |
| `children` | `React.ReactNode`   | -       | Full-width content below the title/actions row (e.g. tabs) |

### Layout.Column Props

| Prop        | Type                          | Default | Description                                                               |
| ----------- | ----------------------------- | ------- | ------------------------------------------------------------------------- |
| `className` | `string`                      | -       | Additional CSS classes                                                    |
| `style`     | `CSSProperties`               | -       | Inline styles for the column                                              |
| `area`      | `"left" \| "main" \| "right"` | -       | Column area role for explicit width control (see [Area Mode](#area-mode)) |
| `children`  | `React.ReactNode`             | -       | Column content                                                            |

## Column Configurations

### 1 Column

Full-width layout, always stacks vertically:

```tsx
<Layout>
  <Layout.Column>
    <DescriptionCard data={data} fields={fields} />
  </Layout.Column>
</Layout>
```

**Responsive behavior:**

- All screens: 1 column (full width)

### 2 Columns

Main content with sidebar:

```tsx
<Layout>
  <Layout.Column>
    {/* Main content - flexible width */}
    <DescriptionCard data={orderData} fields={orderFields} />
  </Layout.Column>
  <Layout.Column>
    {/* Sidebar - fixed 280px on desktop */}
    <Card>Quick Actions</Card>
    <Card>History</Card>
  </Layout.Column>
</Layout>
```

**Responsive behavior:**

- Mobile (< 1024px): Stacks vertically
- Desktop (≥ 1024px): First column flexible, second column 280px

**Use cases:**

- Detail pages with sidebar
- Forms with preview
- Main content + related items

### 3 Columns

Left sidebar, main content, right sidebar:

```tsx
<Layout>
  <Layout.Column>
    {/* Left sidebar - fixed 320px */}
    <Navigation />
  </Layout.Column>
  <Layout.Column>
    {/* Main content - flexible */}
    <DescriptionCard data={data} fields={fields} />
  </Layout.Column>
  <Layout.Column>
    {/* Right sidebar - fixed 280px */}
    <ActivityFeed />
  </Layout.Column>
</Layout>
```

**Responsive behavior:**

- Mobile (< 1280px): Stacks vertically
- Desktop (≥ 1280px): Left 320px, middle flexible, right 280px

**Use cases:**

- Dashboards with multiple panels
- Complex detail pages
- Editor with navigation and preview

### 4+ Columns

When more than 3 `Layout.Column` children are provided, all columns share equal width:

```tsx
<Layout>
  <Layout.Column>{/* 25% */}</Layout.Column>
  <Layout.Column>{/* 25% */}</Layout.Column>
  <Layout.Column>{/* 25% */}</Layout.Column>
  <Layout.Column>{/* 25% */}</Layout.Column>
</Layout>
```

**Responsive behavior:**

- Mobile (< 1280px): Stacks vertically
- Desktop (≥ 1280px): Equal-width columns (`repeat(N, 1fr)`)

## Header

Use `Layout.Header` to add a page-level header with title, actions, and optional full-width content:

```tsx
<Layout>
  <Layout.Header
    title="Order #12345"
    actions={[
      <Button key="edit">Edit</Button>,
      <Button key="delete" variant="destructive">
        Delete
      </Button>,
    ]}
  />
  <Layout.Column>{/* ... */}</Layout.Column>
  <Layout.Column>{/* ... */}</Layout.Column>
</Layout>
```

The header displays:

- **Title** on the left (rendered as `<h1>`, large and bold)
- **Actions** on the right (buttons, dropdowns, etc.)
- **Children** (optional) full-width below the title/actions row

### Header with Tabs

```tsx
<Layout>
  <Layout.Header title="Purchase Orders" actions={[<Button key="create">Create</Button>]}>
    <Tabs value={tab} onValueChange={setTab}>
      <TabsList>
        <TabsTrigger value="all">All</TabsTrigger>
        <TabsTrigger value="open">Open</TabsTrigger>
      </TabsList>
    </Tabs>
  </Layout.Header>
  <Layout.Column>…</Layout.Column>
</Layout>
```

## Area Mode

By default, column widths are determined by position (first, second, third). You can use the `area` prop on `Layout.Column` to explicitly assign roles:

| Area      | Width          |
| --------- | -------------- |
| `"left"`  | Fixed 320px    |
| `"main"`  | Flexible (1fr) |
| `"right"` | Fixed 280px    |
| _(unset)_ | Flexible (1fr) |

If any `Layout.Column` has an `area` prop, all columns switch to area-based widths. Columns without an `area` default to flexible (`1fr`).

```tsx
<Layout>
  <Layout.Column area="left">
    <nav>Sidebar</nav>
  </Layout.Column>
  <Layout.Column area="main">
    <h2>Content</h2>
  </Layout.Column>
</Layout>
```

Columns are rendered in source order — place them in the visual order you want.

## Fill Mode

By default, `<Layout>` grows with its content and the AppShell content area scrolls. Set `fill` to instead stretch the layout to the available height and bound its column row, so a child component can scroll **internally** while the page chrome stays pinned:

```tsx
<Layout fill>
  <Layout.Header title="Products" />
  <Layout.Column>
    <DataTable.Root value={table}>
      <DataTable.Toolbar>
        <DataTable.Filters />
      </DataTable.Toolbar>
      <DataTable.Table />
      <DataTable.Footer>
        <DataTable.Pagination />
      </DataTable.Footer>
    </DataTable.Root>
  </Layout.Column>
</Layout>
```

With `fill`, the `Layout.Header` (title/actions), the `DataTable` toolbar, its column header row, and its footer all remain visible regardless of row count — only the table's rows region scrolls vertically. When the content is short enough to fit, nothing stretches and no scrollbar appears.

Use `fill` for pages whose main content manages its own scrolling (typically a `DataTable`). Leave it off for pages that should flow and scroll naturally (forms, articles, dashboards). It is intended for single-row layouts; when multiple columns stack vertically on mobile, the content area scrolls as usual.

> Because the shell is viewport-bounded, page content scrolls inside the content area rather than on the document. If you have code that read `window`/document scroll, reach for the content scroll container instead — see [Accessing the content scroll container](./sidebar-layout.md#accessing-the-content-scroll-container).

## Gap Spacing

Use `className` to control the space between columns:

```tsx
<Layout className="astw:gap-6">
  <Layout.Column>{/* ... */}</Layout.Column>
  <Layout.Column>{/* ... */}</Layout.Column>
</Layout>
```

## Examples

### Order Detail Page

```tsx
function OrderDetailPage() {
  const { id } = useParams();
  const order = useOrder(id);

  return (
    <Layout>
      <Layout.Header
        title={`Order #${order.orderNumber}`}
        actions={[
          <Button key="print" variant="outline">
            Print
          </Button>,
          <Button key="edit">Edit Order</Button>,
        ]}
      />
      <Layout.Column>
        <DescriptionCard
          data={order}
          fields={[
            { key: "customer.name", label: "Customer" },
            { key: "orderDate", label: "Order Date", type: "date" },
            { key: "status", label: "Status", type: "badge" },
            { type: "divider" },
            { key: "total", label: "Total", type: "money" },
            { key: "shippingAddress", label: "Shipping", type: "address" },
          ]}
        />
      </Layout.Column>
      <Layout.Column>
        <Card>
          <h3>Quick Actions</h3>
          <Button fullWidth>Mark as Shipped</Button>
          <Button fullWidth>Send Invoice</Button>
        </Card>
        <Card>
          <h3>Order History</h3>
          <Timeline events={order.history} />
        </Card>
      </Layout.Column>
    </Layout>
  );
}
```

### Dashboard with 3 Columns

```tsx
function Dashboard() {
  return (
    <Layout>
      <Layout.Header title="Dashboard" />
      <Layout.Column>
        <Card>
          <h3>Navigation</h3>
          <nav>
            <a href="#overview">Overview</a>
            <a href="#sales">Sales</a>
            <a href="#products">Products</a>
          </nav>
        </Card>
      </Layout.Column>
      <Layout.Column>
        <Card>
          <h2>Sales Overview</h2>
          <Chart data={salesData} />
        </Card>
        <Card>
          <h2>Recent Orders</h2>
          <OrdersTable orders={recentOrders} />
        </Card>
      </Layout.Column>
      <Layout.Column>
        <Card>
          <h3>Activity Feed</h3>
          <ActivityList activities={activities} />
        </Card>
      </Layout.Column>
    </Layout>
  );
}
```

### Form with Live Preview

```tsx
function ProductEditor() {
  const [product, setProduct] = useState(initialProduct);

  return (
    <Layout>
      <Layout.Header
        title="Edit Product"
        actions={[
          <Button key="cancel" variant="outline">
            Cancel
          </Button>,
          <Button key="save">Save Changes</Button>,
        ]}
      />
      <Layout.Column>
        <Card>
          <h3>Product Details</h3>
          <Input
            label="Name"
            value={product.name}
            onChange={(e) => setProduct({ ...product, name: e.target.value })}
          />
          <Input
            label="Price"
            type="number"
            value={product.price}
            onChange={(e) => setProduct({ ...product, price: e.target.value })}
          />
          <Textarea
            label="Description"
            value={product.description}
            onChange={(e) => setProduct({ ...product, description: e.target.value })}
          />
        </Card>
      </Layout.Column>
      <Layout.Column>
        <Card>
          <h3>Preview</h3>
          <ProductCard product={product} />
        </Card>
      </Layout.Column>
    </Layout>
  );
}
```

### Area-Based Layout

```tsx
function EditorPage() {
  return (
    <Layout>
      <Layout.Header title="Editor" />
      <Layout.Column area="left">
        <FileTree />
      </Layout.Column>
      <Layout.Column area="main">
        <CodeEditor />
      </Layout.Column>
      <Layout.Column area="right">
        <PropertiesPanel />
      </Layout.Column>
    </Layout>
  );
}
```

### Single Column with Header

```tsx
<Layout>
  <Layout.Header title="Settings" actions={[<Button key="save">Save Changes</Button>]} />
  <Layout.Column>
    <Card>
      <h3>Account Settings</h3>
      <SettingsForm />
    </Card>
  </Layout.Column>
</Layout>
```

## Responsive Behavior

The Layout component automatically handles responsive breakpoints:

### Breakpoints

| Screen Size               | 1 Column   | 2 Columns    | 3+ Columns   |
| ------------------------- | ---------- | ------------ | ------------ |
| Mobile (< 1024px)         | Full width | Stacked      | Stacked      |
| Desktop (1024px - 1280px) | Full width | Side-by-side | Stacked      |
| Large (≥ 1280px)          | Full width | Side-by-side | Side-by-side |

### Column Widths

**2 Columns (Desktop):**

- Column 1: Flexible (takes remaining space)
- Column 2: Fixed 280px

**3 Columns (Desktop):**

- Column 1: Fixed 320px
- Column 2: Flexible (takes remaining space)
- Column 3: Fixed 280px

**4+ Columns (Desktop):**

- All columns: Equal width (`repeat(N, 1fr)`)

## Validation

The Layout component validates its children:

- Only `Layout.Header` and `Layout.Column` are recognized as direct children; other elements are silently filtered out
- If multiple `Layout.Header` children are provided, only the first one is rendered
- When the deprecated `columns` prop is set and doesn't match the actual `Layout.Column` child count, a console warning is emitted

## Styling

### Custom Styling

Add custom classes to the layout container:

```tsx
<Layout className="astw:bg-gray-50 astw:p-8 astw:rounded-lg">{/* ... */}</Layout>
```

Add custom classes to individual columns:

```tsx
<Layout>
  <Layout.Column className="astw:bg-white astw:shadow-sm">
    {/* Main content with background */}
  </Layout.Column>
  <Layout.Column className="astw:space-y-4">{/* Sidebar with extra spacing */}</Layout.Column>
</Layout>
```

## Best Practices

### Do:

- ✅ Use 2 columns for detail pages with sidebar
- ✅ Use 3 columns for dashboards and complex layouts
- ✅ Add meaningful titles to pages
- ✅ Group related actions in the header
- ✅ Keep sidebars focused (5-7 items max)

### Don't:

- ❌ Use 3 columns for mobile-first designs (too complex)
- ❌ Mix Layout with other layout systems (conflicts)
- ❌ Put too many actions in header (use dropdown menus)
- ❌ Forget to handle responsive behavior

## Accessibility

- Semantic HTML structure
- Proper heading hierarchy
- Keyboard navigation support
- Screen reader friendly

## Related Components

- [Grid](./grid.md) - Content-level grid for arranging items into columns
- [DescriptionCard](./description-card.md) - Display data in columns
- [Badge](./badge.md) - Status indicators
- [SidebarLayout](./sidebar-layout.md) - App-level layout

## Related Concepts

- [Styling and Theming](../concepts/styling-theming.md) - Customize appearance
- [Modules and Resources](../concepts/modules-and-resources.md) - Page structure
