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.
Import
import { Layout } from "@tailor-platform/app-shell";Basic Usage
The column count is auto-detected from the number of Layout.Column children:
<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) |
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) |
children | React.ReactNode | - | Column content |
Column Configurations
1 Column
Full-width layout, always stacks vertically:
<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:
<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:
<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:
<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:
<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
<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).
<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:
<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.
Gap Spacing
Use className to control the space between columns:
<Layout className="astw:gap-6">
<Layout.Column>{/* ... */}</Layout.Column>
<Layout.Column>{/* ... */}</Layout.Column>
</Layout>Examples
Order Detail Page
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
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
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
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
<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.HeaderandLayout.Columnare recognized as direct children; other elements are silently filtered out - If multiple
Layout.Headerchildren are provided, only the first one is rendered - When the deprecated
columnsprop is set and doesn't match the actualLayout.Columnchild count, a console warning is emitted
Styling
Custom Styling
Add custom classes to the layout container:
<Layout className="astw:bg-gray-50 astw:p-8 astw:rounded-lg">{/* ... */}</Layout>Add custom classes to individual columns:
<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 - Content-level grid for arranging items into columns
- DescriptionCard - Display data in columns
- Badge - Status indicators
- SidebarLayout - App-level layout
Related Concepts
- Styling and Theming - Customize appearance
- Modules and Resources - Page structure