Skip to main content
The Layout component provides responsive column layouts commonly used in ERP applications. It handles 1, 2, or 3 column configurations with automatic responsive behavior.

Why Use the Layout Component?

ERP applications often need structured layouts for:
  • Detail pages with a main content area and sidebar
  • Forms with primary fields and metadata panels
  • Three-column views with navigation, content, and properties
The Layout component provides:
  • Container-based responsiveness - Adapts to parent container width, not viewport
  • Consistent spacing - Standardized gaps and padding
  • Header support - Built-in title and action buttons
  • Accessibility - Proper semantic HTML structure

Basic Usage

One Column Layout

Use for simple, full-width content:
import { Layout } from "@tailor-platform/app-shell";

const SimplePage = () => {
  return (
    <Layout columns={1} title="Product Details">
      <Layout.Column>
        <div>Full-width content goes here</div>
      </Layout.Column>
    </Layout>
  );
};

Two Column Layout

Perfect for detail pages with a sidebar:
import { Layout, DescriptionCard } from "@tailor-platform/app-shell";

const OrderDetailPage = ({ order }) => {
  return (
    <Layout columns={2} title="Order Details">
      {/* Main content - flexible width, minimum 560px */}
      <Layout.Column>
        <DescriptionCard
          data={order}
          title="Order Information"
          fields={[
            { key: "orderNumber", label: "Order #" },
            { key: "status", label: "Status", type: "badge" },
            { key: "total", label: "Total", type: "money" },
          ]}
        />
      </Layout.Column>

      {/* Sidebar - fixed 360px width */}
      <Layout.Column>
        <div style={{ padding: "1rem", border: "1px solid hsl(var(--border))" }}>
          <h3>Actions</h3>
          <button>Edit Order</button>
          <button>Cancel Order</button>
        </div>
      </Layout.Column>
    </Layout>
  );
};
Responsive behavior:
  • Desktop (≥1024px): Two columns side-by-side
  • Mobile (<1024px): Columns stack vertically

Three Column Layout

Ideal for complex data entry or property panels:
const ProductEditPage = ({ product }) => {
  return (
    <Layout columns={3} title="Edit Product">
      {/* Left sidebar - fixed 360px */}
      <Layout.Column>
        <div>Categories and Tags</div>
      </Layout.Column>

      {/* Main content - flexible, minimum 550px */}
      <Layout.Column>
        <form>
          <input name="name" placeholder="Product Name" />
          <textarea name="description" />
        </form>
      </Layout.Column>

      {/* Right sidebar - fixed 360px */}
      <Layout.Column>
        <div>Preview and Metadata</div>
      </Layout.Column>
    </Layout>
  );
};
Responsive behavior:
  • Large (≥1200px): All 3 columns side-by-side
  • Medium (960-1199px): Columns 2 & 3 on top row, Column 1 below
  • Small (<960px): All columns stack (order: 3, 2, 1 from top to bottom)

Adding Headers and Actions

The Layout component includes built-in header support with title and action buttons:
import { Layout } from "@tailor-platform/app-shell";

const EditOrderPage = ({ order, onSave, onCancel }) => {
  return (
    <Layout
      columns={2}
      title="Edit Order"
      actions={[
        <Button 
          key="cancel" 
          variant="secondary" 
          onClick={onCancel}
        >
          Cancel
        </Button>,
        <Button 
          key="save" 
          onClick={onSave}
        >
          Save Changes
        </Button>,
      ]}
    >
      <Layout.Column>
        {/* Form fields */}
      </Layout.Column>
      <Layout.Column>
        {/* Metadata */}
      </Layout.Column>
    </Layout>
  );
};
Action buttons automatically appear in the header on desktop and stack responsively on mobile.

Real-World Example: Purchase Order Detail

Here’s a complete example from the App Shell source code:
import { Layout, DescriptionCard, defineResource } from "@tailor-platform/app-shell";

const purchaseOrder = {
  id: "po-2024-0042",
  docNumber: "PO-10000041",
  status: "CONFIRMED",
  billingStatus: "PARTIALLY_BILLED",
  deliveryStatus: "NOT_RECEIVED",
  supplierName: "Acme Industrial Supplies",
  expectedDeliveryDate: "2024-02-15T00:00:00Z",
  createdAt: "2024-01-20T10:30:00Z",
  currency: { code: "USD" },
  total: 13531.25,
  shipToLocation: {
    name: "Main Warehouse",
    address: {
      line1: "1234 Industrial Blvd",
      city: "Austin",
      state: "TX",
      zip: "78701",
    },
  },
};

const PurchaseOrderPage = () => {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
      {/* Status Overview */}
      <DescriptionCard
        data={purchaseOrder}
        title="Status Overview"
        columns={4}
        fields={[
          {
            key: "status",
            label: "Status",
            type: "badge",
            meta: {
              badgeVariantMap: {
                DRAFT: "success",
                CONFIRMED: "success",
                CANCELED: "outline-error",
              },
            },
          },
          {
            key: "billingStatus",
            label: "Billing",
            type: "badge",
            meta: {
              badgeVariantMap: {
                NOT_BILLED: "outline-neutral",
                PARTIALLY_BILLED: "outline-warning",
                BILLED: "outline-success",
              },
            },
          },
        ]}
      />

      {/* Order Details */}
      <DescriptionCard
        data={purchaseOrder}
        title="Order Overview"
        columns={4}
        fields={[
          { key: "docNumber", label: "PO Number", meta: { copyable: true } },
          { key: "supplierName", label: "Supplier" },
          {
            key: "expectedDeliveryDate",
            label: "Expected Delivery",
            type: "date",
            meta: { dateFormat: "medium" },
          },
          { type: "divider" },
          {
            key: "shipToLocation.address",
            label: "Shipping Address",
            type: "address",
          },
        ]}
      />

      {/* Financial Summary */}
      <DescriptionCard
        data={purchaseOrder}
        title="Financial Summary"
        columns={4}
        fields={[
          {
            key: "total",
            label: "Total",
            type: "money",
            meta: { currencyKey: "currency.code" },
          },
        ]}
      />
    </div>
  );
};

export const purchaseOrderResource = defineResource({
  path: "purchase-orders/:id",
  meta: { title: "Purchase Order" },
  component: PurchaseOrderPage,
});

Customizing Gap Spacing

Control spacing between columns with the gap prop:
// Default gap (16px)
<Layout columns={2}>
  <Layout.Column>Content</Layout.Column>
  <Layout.Column>Sidebar</Layout.Column>
</Layout>

// Larger gap (24px)
<Layout columns={2} gap={6}>
  <Layout.Column>Content</Layout.Column>
  <Layout.Column>Sidebar</Layout.Column>
</Layout>

// Extra large gap (32px)
<Layout columns={2} gap={8}>
  <Layout.Column>Content</Layout.Column>
  <Layout.Column>Sidebar</Layout.Column>
</Layout>
The gap prop accepts Tailwind spacing values: 4 (16px), 6 (24px), or 8 (32px).

Combining with DescriptionCard

The DescriptionCard component works perfectly inside Layout columns. Here’s a two-column edit page:
import { Layout, DescriptionCard } from "@tailor-platform/app-shell";

const ProductEditPage = ({ product }) => {
  return (
    <Layout 
      columns={2} 
      title="Edit Product"
      actions={[
        <Button key="save">Save</Button>
      ]}
    >
      {/* Main form */}
      <Layout.Column>
        <DescriptionCard
          data={product}
          title="Basic Information"
          columns={3}
          fields={[
            { key: "name", label: "Product Name" },
            { key: "sku", label: "SKU", meta: { copyable: true } },
            { key: "category", label: "Category" },
            { type: "divider" },
            { 
              key: "description", 
              label: "Description",
              meta: { truncateLines: 3 } 
            },
          ]}
        />
      </Layout.Column>

      {/* Metadata sidebar */}
      <Layout.Column>
        <DescriptionCard
          data={product}
          title="Metadata"
          columns={3}
          fields={[
            { 
              key: "createdAt", 
              label: "Created", 
              type: "date",
              meta: { dateFormat: "relative" } 
            },
            { 
              key: "updatedAt", 
              label: "Updated", 
              type: "date" 
            },
          ]}
        />
      </Layout.Column>
    </Layout>
  );
};

Best Practices

Container Responsiveness: The Layout component responds to its container width, not the viewport. This means it works correctly even when nested inside narrow containers.

When to Use Each Column Count

  • 1 Column: Simple content, documentation, lists
  • 2 Columns: Detail pages with sidebar, forms with metadata
  • 3 Columns: Complex editors, property panels, advanced filters

Content Guidelines

  • Keep the main content in the middle column for 3-column layouts
  • Use fixed-width columns (1st and 3rd) for navigation or metadata
  • Put the most important content in Column 1 for 2-column layouts
  • Avoid nesting Layout components (use DescriptionCard instead)

Accessibility

  • Always provide a meaningful title prop
  • Use semantic HTML inside columns
  • Ensure action buttons have clear labels
  • Test keyboard navigation through all interactive elements

Advanced: Custom Layout Components

If you need a layout that doesn’t fit the 1-2-3 column pattern, you can create your own using the SidebarLayout children prop:
import { AppShell, SidebarLayout } from "@tailor-platform/app-shell";

const App = () => (
  <AppShell modules={modules}>
    <SidebarLayout>
      {({ Outlet }) => (
        <div className="custom-grid">
          <aside>Custom sidebar</aside>
          <main>
            <Outlet />
          </main>
          <aside>Custom panel</aside>
        </div>
      )}
    </SidebarLayout>
  </AppShell>
);
See SidebarLayout component for more details.

Build docs developers (and LLMs) love