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

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