Skip to content

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

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

PropTypeDefaultDescription
classNamestring-Additional CSS classes for container
styleCSSProperties-Inline styles for the layout container
childrenLayout.Column[]RequiredLayout.Header and/or Layout.Column components
fillbooleanfalseFill the available height instead of growing with content (see Fill Mode)
columns1 | 2 | 3-(Deprecated) Auto-detected from Layout.Column children count when omitted
titlestring-(Deprecated) Use <Layout.Header title="..."> instead
actionsReact.ReactNode[]-(Deprecated) Use <Layout.Header actions={[...]}> instead
gapnumber4(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.

PropTypeDefaultDescription
titlestring-Page title (left side, rendered as <h1>)
actionsReact.ReactNode[]-Action buttons or nodes (right side)
childrenReact.ReactNode-Full-width content below the title/actions row (e.g. tabs)

Layout.Column Props

PropTypeDefaultDescription
classNamestring-Additional CSS classes
styleCSSProperties-Inline styles for the column
area"left" | "main" | "right"-Column area role for explicit width control (see Area Mode)
childrenReact.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))

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:

AreaWidth
"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.

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 Size1 Column2 Columns3+ Columns
Mobile (< 1024px)Full widthStackedStacked
Desktop (1024px - 1280px)Full widthSide-by-sideStacked
Large (≥ 1280px)Full widthSide-by-sideSide-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