Layout
Guidelines to create consistent layouts across Studio pages using a set of page components.
The Page pattern consists of three main components that work together to create consistent page layouts: PageContainer, PageHeader, and PageSection. These components provide a structured approach to building pages with consistent spacing, max-widths, and content organization.
Layout Types
Settings
Settings pages are used for configuration and preference management. They follow a single-column layout with default widths to keep content focused and readable. Examples include project settings or auth sessions.
- Use
PageHeaderwithsize="default" - Use
PageContainerwithsize="default" - Use
PageSectionfor organizing settings into logical groups
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import {
Button,
Card,
CardContent,
CardFooter,
FormControl_Shadcn_,
FormField_Shadcn_,
Form_Shadcn_,
Input_Shadcn_,
PrePostTab,
Switch,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageHeader,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import {
PageSection,
PageSectionContent,
PageSectionDescription,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import * as z from 'zod'
const RefreshTokenSchema = z.object({
REFRESH_TOKEN_ROTATION_ENABLED: z.boolean(),
SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: z.coerce.number().min(0),
})
const UserSessionsSchema = z.object({
SESSIONS_TIMEBOX: z.coerce.number().min(0),
SESSIONS_INACTIVITY_TIMEOUT: z.coerce.number().min(0),
SESSIONS_SINGLE_PER_USER: z.boolean(),
})
function HoursOrNeverText({ value }: { value: number }) {
if (value === 0) {
return 'never'
} else if (value === 1) {
return 'hour'
} else {
return 'hours'
}
}
export function PageLayoutSettings() {
const refreshTokenForm = useForm<z.infer<typeof RefreshTokenSchema>>({
resolver: zodResolver(RefreshTokenSchema),
defaultValues: {
REFRESH_TOKEN_ROTATION_ENABLED: false,
SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: 10,
},
})
const userSessionsForm = useForm<z.infer<typeof UserSessionsSchema>>({
resolver: zodResolver(UserSessionsSchema),
defaultValues: {
SESSIONS_TIMEBOX: 0,
SESSIONS_INACTIVITY_TIMEOUT: 0,
SESSIONS_SINGLE_PER_USER: false,
},
})
return (
<div className="w-full">
<PageHeader size="default">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>User Sessions</PageHeaderTitle>
<PageHeaderDescription>
Configure settings for user sessions and refresh tokens
</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
</PageHeader>
<PageContainer size="default">
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Refresh Tokens</PageSectionTitle>
<PageSectionDescription>
Configure refresh token rotation and security settings.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Form_Shadcn_ {...refreshTokenForm}>
<form className="space-y-4">
<Card>
<CardContent className="pt-6">
<FormField_Shadcn_
control={refreshTokenForm.control}
name="REFRESH_TOKEN_ROTATION_ENABLED"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Detect and revoke potentially compromised refresh tokens"
description="Prevent replay attacks from potentially compromised refresh tokens."
>
<FormControl_Shadcn_>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField_Shadcn_
control={refreshTokenForm.control}
name="SECURITY_REFRESH_TOKEN_REUSE_INTERVAL"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Refresh token reuse interval"
description="Time interval where the same refresh token can be used multiple times to request for an access token. Recommendation: 10 seconds."
>
<FormControl_Shadcn_>
<PrePostTab postTab="seconds">
<Input_Shadcn_ type="number" min={0} {...field} />
</PrePostTab>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
<CardFooter className="justify-end space-x-2">
{refreshTokenForm.formState.isDirty && (
<Button type="default" onClick={() => refreshTokenForm.reset()}>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
disabled={!refreshTokenForm.formState.isDirty}
>
Save changes
</Button>
</CardFooter>
</Card>
</form>
</Form_Shadcn_>
</PageSectionContent>
</PageSection>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>User Sessions</PageSectionTitle>
<PageSectionDescription>
Configure session timeout and single session enforcement settings.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Form_Shadcn_ {...userSessionsForm}>
<form className="space-y-4">
<Card>
<CardContent>
<FormField_Shadcn_
control={userSessionsForm.control}
name="SESSIONS_SINGLE_PER_USER"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Enforce single session per user"
description="If enabled, all but a user's most recently active session will be terminated."
>
<FormControl_Shadcn_>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField_Shadcn_
control={userSessionsForm.control}
name="SESSIONS_TIMEBOX"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Time-box user sessions"
description="The amount of time before a user is forced to sign in again. Use 0 for never."
>
<div className="flex items-center">
<FormControl_Shadcn_>
<PrePostTab postTab={<HoursOrNeverText value={field.value || 0} />}>
<Input_Shadcn_ type="number" min={0} {...field} />
</PrePostTab>
</FormControl_Shadcn_>
</div>
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField_Shadcn_
control={userSessionsForm.control}
name="SESSIONS_INACTIVITY_TIMEOUT"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Inactivity timeout"
description="The amount of time a user needs to be inactive to be forced to sign in again. Use 0 for never."
>
<div className="flex items-center">
<FormControl_Shadcn_>
<PrePostTab postTab={<HoursOrNeverText value={field.value || 0} />}>
<Input_Shadcn_ type="number" {...field} />
</PrePostTab>
</FormControl_Shadcn_>
</div>
</FormItemLayout>
)}
/>
</CardContent>
<CardFooter className="justify-end space-x-2">
{userSessionsForm.formState.isDirty && (
<Button type="default" onClick={() => userSessionsForm.reset()}>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
disabled={!userSessionsForm.formState.isDirty}
>
Save changes
</Button>
</CardFooter>
</Card>
</form>
</Form_Shadcn_>
</PageSectionContent>
</PageSection>
</PageContainer>
</div>
)
}List
List pages display collections of objects like tables, triggers, or functions. These pages use larger widths to accommodate wide content like data tables. Examples include database triggers, database functions or org team members.
- Use
PageHeaderwithsize="large" - Use
PageContainerwithsize="large" - Use
PageSectionto wrap list content
Table and List Actions:
- With filters or search: If the table has filters or search, place table actions aligned with the filters on the right side. Do not use
PageSectionAsideorPageHeaderAsidefor table actions when filters are present. - Without filters: For simple lists without filters or search, add primary list actions to
PageHeaderAsideorPageSectionAsideas appropriate.
import { Search } from 'lucide-react'
import React from 'react'
import {
Button,
Card,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Input,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageHeader,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
export function PageLayoutList(): React.JSX.Element {
const functions = [
{
id: 1,
name: 'get_user_profile',
arguments: 'user_id uuid',
return_type: 'jsonb',
security: 'Definer',
},
{
id: 2,
name: 'update_user_settings',
arguments: 'user_id uuid, settings jsonb',
return_type: 'void',
security: 'Invoker',
},
{
id: 3,
name: 'calculate_total',
arguments: 'amount numeric, tax_rate numeric',
return_type: 'numeric',
security: 'Definer',
},
]
return (
<div className="w-full">
<PageHeader size="large">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>Database Functions</PageHeaderTitle>
<PageHeaderDescription>Manage your database functions</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
</PageHeader>
<PageContainer size="large">
<PageSection>
<PageSectionContent>
<div className="w-full space-y-4">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-2 flex-wrap">
<div className="flex flex-col lg:flex-row lg:items-center gap-2">
<Input
placeholder="Search for a function"
size="tiny"
icon={<Search />}
className="w-full lg:w-52"
/>
</div>
<Button type="primary">Create a new function</Button>
</div>
<Card>
<Table className="table-fixed overflow-x-auto">
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead className="table-cell">Arguments</TableHead>
<TableHead className="table-cell">Return type</TableHead>
<TableHead className="table-cell w-[100px]">Security</TableHead>
<TableHead className="w-1/6"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{functions.map((fn) => (
<TableRow key={fn.id}>
<TableCell className="font-medium">{fn.name}</TableCell>
<TableCell>{fn.arguments}</TableCell>
<TableCell>{fn.return_type}</TableCell>
<TableCell>{fn.security}</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="text" size="small">
Actions
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive">
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
</div>
</PageSectionContent>
</PageSection>
</PageContainer>
</div>
)
}import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageHeader,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
PageHeaderDescription,
PageHeaderAside,
} from 'ui-patterns/PageHeader'
import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
import {
Button,
Card,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
export function PageLayoutListSimple() {
const items = [
{ id: 1, name: 'Project Alpha', status: 'Active', members: 12 },
{ id: 2, name: 'Project Beta', status: 'Active', members: 8 },
{ id: 3, name: 'Project Gamma', status: 'Inactive', members: 5 },
]
return (
<div className="w-full">
<PageHeader size="large">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>Projects</PageHeaderTitle>
<PageHeaderDescription>
Manage and view all your projects in one place.
</PageHeaderDescription>
</PageHeaderSummary>
<PageHeaderAside>
<Button type="primary" size="small">
Create Project
</Button>
</PageHeaderAside>
</PageHeaderMeta>
</PageHeader>
<PageContainer size="large">
<PageSection>
<PageSectionContent>
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Status</TableHead>
<TableHead>Members</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow key={item.id}>
<TableCell className="font-medium">{item.name}</TableCell>
<TableCell>{item.status}</TableCell>
<TableCell>{item.members}</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="text" size="small">
Actions
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Duplicate</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
</PageSectionContent>
</PageSection>
</PageContainer>
</div>
)
}Data and Full Page Experiences
Full-page experiences like the table editor, cron jobs, and edge functions require maximum screen real-estate and so make use of "full" size containers.
- Use
PageHeaderwithsize="full" - Use
PageContainerwithsize="full" - Content spans the full width of the viewport
Detail Pages
Detail pages display dense or lengthy content split into multiple sections. The horizontal orientation allows for better information hierarchy and context. Examples include organisation billing or project infrastructure.
- Use
PageHeaderwithsize="large" - Use
PageContainerwithsize="large" - Use
PageSectionwithorientation="horizontal"to show summary alongside content - Multiple sections can stack vertically with horizontal layouts within each
import { Button, Card, CardContent } from 'ui'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageHeader,
PageHeaderDescription,
PageHeaderMeta,
PageHeaderSummary,
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import {
PageSection,
PageSectionContent,
PageSectionDescription,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
export function PageLayoutDetail() {
return (
<div className="w-full">
<PageHeader size="large">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>Billing</PageHeaderTitle>
<PageHeaderDescription>
Manage your organization's billing and subscription settings.
</PageHeaderDescription>
</PageHeaderSummary>
</PageHeaderMeta>
</PageHeader>
<PageContainer size="large">
<PageSection orientation="horizontal">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Subscription</PageSectionTitle>
<PageSectionDescription>
View and manage your current subscription plan and billing cycle.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Card>
<CardContent className="p-6">
<div className="space-y-4">
<div>
<p className="text-xs text-foreground-lighter mb-1">Current Plan</p>
<p className="text-sm font-medium">Pro Plan</p>
</div>
<div>
<p className="text-xs text-foreground-lighter mb-1">Billing Cycle</p>
<p className="text-sm">Monthly</p>
</div>
<div>
<p className="text-xs text-foreground-lighter mb-1">Next Billing Date</p>
<p className="text-sm">March 15, 2024</p>
</div>
<div className="pt-2">
<Button type="default" size="small">
Change Plan
</Button>
</div>
</div>
</CardContent>
</Card>
</PageSectionContent>
</PageSection>
<PageSection orientation="horizontal">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Cost control</PageSectionTitle>
<PageSectionDescription>
Set spending limits and alerts to manage your organization's costs.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Card>
<CardContent className="p-6">
<div className="space-y-4">
<div>
<p className="text-xs text-foreground-lighter mb-1">Monthly Spending Limit</p>
<p className="text-sm">$500.00</p>
</div>
<div>
<p className="text-xs text-foreground-lighter mb-1">Current Month Spend</p>
<p className="text-sm">$234.50</p>
</div>
<div className="pt-2">
<Button type="default" size="small">
Configure Limits
</Button>
</div>
</div>
</CardContent>
</Card>
</PageSectionContent>
</PageSection>
<PageSection orientation="horizontal">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Payment Methods</PageSectionTitle>
<PageSectionDescription>
Manage payment methods and billing information for your organization.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Card>
<CardContent className="p-6">
<div className="space-y-4">
<div>
<p className="text-xs text-foreground-lighter mb-1">Primary Payment Method</p>
<p className="text-sm">•••• •••• •••• 4242</p>
</div>
<div>
<p className="text-xs text-foreground-lighter mb-1">Expires</p>
<p className="text-sm">12/2025</p>
</div>
<div className="pt-2">
<Button type="default" size="small">
Update Payment Method
</Button>
</div>
</div>
</CardContent>
</Card>
</PageSectionContent>
</PageSection>
</PageContainer>
</div>
)
}Components
- PageContainer - Container component providing consistent max-width and padding based on size variants
- PageHeader - Compound component for building page headers with breadcrumbs, icons, titles, descriptions, actions, and navigation
- PageSection - Compound component for organizing page content into distinct sections with title, description, and action areas