Detail page layout
Header, optional tabs, then a two-column body: main content and a sidebar.
vue
<script setup lang="ts">
import DetailPageLayout from '@shared/components/common/pages/DetailPageLayout.vue'
import { Button } from '@shared/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@shared/components/ui/card'
import SaListGroup from '@shared/components/common/list/SaListGroup.vue'
import SaListGroupItem from '@shared/components/common/list/SaListGroupItem.vue'
import { Tabs, TabsList, TabsTrigger } from '@shared/components/ui/tabs'
</script>
<template>
<DetailPageLayout title="Northwind">
<template #description>
Customer since March 2023.
</template>
<template #actions>
<Button size="sm" variant="outline">
Edit
</Button>
</template>
<template #tabs>
<Tabs default-value="overview">
<TabsList>
<TabsTrigger value="overview">
Overview
</TabsTrigger>
<TabsTrigger value="orders">
Orders
</TabsTrigger>
<TabsTrigger value="notes">
Notes
</TabsTrigger>
</TabsList>
</Tabs>
</template>
<template #main>
<Card>
<CardHeader>
<CardTitle>Recent activity</CardTitle>
</CardHeader>
<CardContent class="p-0">
<SaListGroup variant="flush">
<SaListGroupItem title="Invoice INV-2043 paid" description="3 March 2026" />
<SaListGroupItem title="Order placed" description="28 February 2026" />
<SaListGroupItem title="Contact added" description="14 February 2026" />
</SaListGroup>
</CardContent>
</Card>
</template>
<template #sidebar>
<Card>
<CardHeader>
<CardTitle>Details</CardTitle>
</CardHeader>
<CardContent class="space-y-2 text-sm">
<p><span class="text-muted-foreground">Owner</span> Amina Haddad</p>
<p><span class="text-muted-foreground">Segment</span> Enterprise</p>
<p><span class="text-muted-foreground">Balance</span> $12,480</p>
</CardContent>
</Card>
</template>
</DetailPageLayout>
</template>Usage
main takes two of three columns and sidebar the third, from the lg breakpoint up. With no sidebar, main takes all three, so a page without one does not sit in two thirds of the screen.
Below lg the two stack, sidebar last.
The main column carries min-w-0. A grid item's automatic minimum size is its min-content width, so one wide child, such as a table with a minimum width, would push the item past its own track and the card would run off the screen instead of the table scrolling inside it.
API
Source: src/shared/components/common/pages/DetailPageLayout.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | — | Forwarded to the page header. |
Emits
None.
Slots
| Slot | Description |
|---|---|
breadcrumb | Forwarded to the page header. |
description | Forwarded to the page header. |
actions | Forwarded to the page header. |
tabs | Between the header and the body. |
main | The wide column. |
sidebar | The narrow column, rendered as an <aside>. |
Accessibility
- The sidebar is a real
<aside>, so it is announced as complementary content rather than as more of the page. - The main column can shrink, so a wide table scrolls inside it instead of the layout scrolling sideways.