| title | Layout DSL |
|---|---|
| description | Declarative layout description language for pages, sections, and responsive grids |
import { Layout, Grid, Columns, Box, Monitor, Smartphone, Tablet, MousePointer } from 'lucide-react';
The Layout DSL (Domain-Specific Language) is ObjectUI's declarative syntax for defining page structure, sections, and responsive grids. It abstracts away CSS Grid, Flexbox, and platform-specific layout engines.
Instead of giving you infinite layout freedom (and infinite ways to break responsive design), ObjectUI provides a constrained, opinionated layout system:
- 12-Column Grid: All layouts use a 12-column responsive grid
- Section-Based: Pages are composed of sections, sections contain fields/widgets
- Auto-Responsive: Breakpoints applied automatically (no media queries)
- Platform-Agnostic: Same DSL renders as CSS Grid (web), Auto Layout (iOS), ConstraintLayout (Android)
Why Constraints?
- Consistency: All forms look professionally designed
- Accessibility: Keyboard navigation and screen readers work automatically
- Maintainability: Designers can't create 47 different button sizes
A page's kind selects how it is authored. This document describes the
structured mode (regions + the 12-column grid below) — but for layouts the
fixed schema can't express, two source-authoring tiers let an author (or AI)
write the page body as a string. Pick by what the page needs:
kind |
You write | JavaScript | Use when |
|---|---|---|---|
full / slotted (this doc) |
structured regions / slots |
— | record, detail, home, and app layouts from the component catalogue |
html |
constrained JSX = registered components + safe native HTML, parsed, never executed | none | free-form layout, landing pages, or dashboards that compose blocks — no interactivity |
react |
real React (hooks, .map, onClick, expressions) |
executed in the app | complex interactive business UIs — master/detail, wizards, state-driven filters |
For html / react, the page body is the source string (it is the
source-of-truth; regions is ignored). html is safe by construction (it
is parsed into the same SDUI tree this document describes, never executed).
react executes author code, so it is gated by a host capability that is ON
by default and disabled per-deployment with the OS_PAGE_REACT=off env
variable. Both tiers can embed the platform's real data components
(<ObjectTable>, <ObjectForm>, charts, metrics).
Learn more: ADR-0080 (the
htmltier — parse ≠ execute; the registry is the contract) and ADR-0081 (the trustedreacttier). The rest of this page covers the structured mode.
Page
├─ Header (Title, Actions, Breadcrumbs)
├─ Main Region
│ ├─ Section 1
│ │ ├─ Field Group
│ │ │ ├─ Field A (span: 6)
│ │ │ └─ Field B (span: 6)
│ │ └─ Widget
│ └─ Section 2
│ └─ Related List
└─ Sidebar Region
├─ Widget: Quick Stats
└─ Widget: Activity Feed
Top-level layout structures that define macro organization.
context: record
object: account
template: standard
regions:
- name: header
components:
- type: page:header
properties:
title: Customer Details
actions: [edit, delete, share]
- name: main
components:
- type: page:section
properties:
label: Contact Information
fields: [name, email, phone]
- name: sidebar
components:
- type: widget
properties:
component: activity_feedVisual Layout:
┌────────────────────────────────────────────────────┐
│ Header: Customer Details [Edit] [Delete] │
├──────────────────────────────┬─────────────────────┤
│ │ │
│ Main Region │ Sidebar │
│ │ │
│ ┌────────────────────────┐ │ ┌───────────────┐ │
│ │ Contact Information │ │ │ Activity Feed │ │
│ │ Name: ____________ │ │ │ • Called │ │
│ │ Email: ___________ │ │ │ • Emailed │ │
│ │ Phone: ___________ │ │ └───────────────┘ │
│ └────────────────────────┘ │ │
│ │ │
└──────────────────────────────┴─────────────────────┘
Breakpoint Behavior:
- Desktop (≥1024px): Sidebar on right (25% width)
- Tablet (768-1023px): Sidebar below main (full width)
- Mobile (less than 768px): Sidebar below main (full width)
High-density layout for power users (like Salesforce Service Console).
context: app
template: console
regions:
- name: left
width: small
components:
- type: record:list
properties:
object: case
- name: right
width: full
components:
- type: record:details
properties:
tabs: [details, related, activity]Visual Layout:
┌──────────────┬─────────────────────────────────────┐
│ │ Case #12345 │
│ Case List │ ┌───┬─────────┬──────────┬────────┐│
│ │ │Det│ Related │ Activity │ ││
│ □ Case #123 │ └───┴─────────┴──────────┴────────┘│
│ ■ Case #124 │ │
│ □ Case #125 │ Subject: Email not working │
│ □ Case #126 │ Priority: High │
│ │ Status: In Progress │
│ │ │
│ │ [Resolve] [Escalate] │
└──────────────┴─────────────────────────────────────┘
Use Cases:
- Customer service (Case management)
- Sales (Opportunity pipeline + details)
- Helpdesk (Ticket queue + ticket details)
Multi-step flow with progress indicator.
# page_template: wizard
steps:
- name: account_info
label: Account Information
fields: [company_name, industry, size]
- name: contact_details
label: Contact Details
fields: [name, email, phone]
- name: preferences
label: Preferences
fields: [newsletter, notifications]
- name: review
label: Review & Submit
type: summaryVisual Layout:
┌────────────────────────────────────────────────────┐
│ New Customer Setup │
│ ● Account Info → ○ Contact → ○ Preferences → ○ │
├────────────────────────────────────────────────────┤
│ │
│ Account Information │
│ │
│ Company Name: _______________________________ │
│ Industry: [Select Industry ▼] │
│ Company Size: ○ 1-10 ○ 11-50 ○ 51-200 │
│ │
│ [Cancel] [Next Step →] │
└────────────────────────────────────────────────────┘
All layouts use a responsive grid that divides space into 12 columns.
section:
label: Contact Information
columns: 2 # Each field takes 6/12 columns (50% width)
fields:
- name # Column 1-6
- email # Column 7-12
- phone # Column 1-6 (new row)
- company # Column 7-12Rendered Grid:
┌──────────────────────┬──────────────────────┐
│ Name: ______________ │ Email: _____________ │
├──────────────────────┼──────────────────────┤
│ Phone: _____________ │ Company: ___________ │
└──────────────────────┴──────────────────────┘
section:
label: Product Details
layout:
- field: product_name
span: 12 # Full width
- field: price
span: 4 # 33% width
- field: quantity
span: 4 # 33% width
- field: total
span: 4 # 33% width
- field: description
span: 12 # Full widthRendered Grid:
┌─────────────────────────────────────────────────┐
│ Product Name: _________________________________│
├───────────────┬───────────────┬────────────────┤
│ Price: $_____ │ Qty: ________ │ Total: $______ │
├───────────────┴───────────────┴────────────────┤
│ Description: │
│ ____________________________________________ │
│ ____________________________________________ │
└─────────────────────────────────────────────────┘
Grid automatically collapses on smaller screens:
section:
columns: 3 # Desktop: 3 columns, Tablet: 2 columns, Mobile: 1 column
fields: [field_a, field_b, field_c, field_d, field_e, field_f]Desktop (≥1024px): 3 columns
┌──────────┬──────────┬──────────┐
│ Field A │ Field B │ Field C │
├──────────┼──────────┼──────────┤
│ Field D │ Field E │ Field F │
└──────────┴──────────┴──────────┘
Tablet (768-1023px): 2 columns
┌──────────────┬──────────────┐
│ Field A │ Field B │
├──────────────┼──────────────┤
│ Field C │ Field D │
├──────────────┼──────────────┤
│ Field E │ Field F │
└──────────────┴──────────────┘
Mobile (less than 768px): 1 column
┌────────────────────────┐
│ Field A │
├────────────────────────┤
│ Field B │
├────────────────────────┤
│ Field C │
├────────────────────────┤
│ Field D │
├────────────────────────┤
│ Field E │
├────────────────────────┤
│ Field F │
└────────────────────────┘
Sections are collapsible containers for related fields.
sections:
- label: Contact Information
collapsible: true
collapsed: false # Expanded by default
fields:
- name
- email
- phoneRendered:
┌─ Contact Information ──────────────────────────▼─┐
│ │
│ Name: __________________________________________│
│ Email: __________________________________________│
│ Phone: __________________________________________│
│ │
└──────────────────────────────────────────────────┘
Show sections based on field values or permissions:
sections:
- label: Basic Info
fields: [name, email]
- label: Billing Information
visible:
field: account_type
value: premium # Only show for premium accounts
fields: [payment_method, billing_address]
- label: Admin Settings
visible:
permission: admin # Only show to admins
fields: [api_key, rate_limit]sections:
- label: Quick Summary
variant: compact # Reduced padding
columns: 4
fields: [status, priority, assignee, due_date]
- label: Description
variant: spacious # Extra padding
fields: [long_description]
- label: Danger Zone
variant: danger # Red border, warning icon
fields: [archive, delete]Group related fields on the same row:
sections:
- label: Name
layout:
- type: field_group
inline: true
fields:
- field: first_name
span: 6
placeholder: First
- field: last_name
span: 6
placeholder: LastRendered:
Name
┌──────────────────────┬──────────────────────┐
│ First: _____________ │ Last: ______________ │
└──────────────────────┴──────────────────────┘
- type: field_group
label: Address
fields:
- field: street
span: 12
- field: city
span: 6
- field: state
span: 3
- field: postal_code
span: 3Rendered:
Address
┌─────────────────────────────────────────────────┐
│ Street: _______________________________________│
├──────────────────────┬───────────┬────────────┤
│ City: _______________ │ State: __ │ ZIP: _____ │
└──────────────────────┴───────────┴────────────┘
Organize large forms into tabbed sections.
layout:
mode: tabbed
tabs:
- name: details
label: Details
icon: file-text
sections:
- label: Basic Info
fields: [name, email, phone]
- name: address
label: Address
icon: map-pin
sections:
- label: Primary Address
fields: [street, city, state, zip]
- name: preferences
label: Preferences
icon: settings
sections:
- label: Notifications
fields: [email_notifications, sms_notifications]Rendered:
┌──────────────────────────────────────────────────┐
│ [Details] [Address] [Preferences] │
├──────────────────────────────────────────────────┤
│ │
│ Basic Info │
│ Name: __________________________________________│
│ Email: __________________________________________│
│ Phone: __________________________________________│
│ │
└──────────────────────────────────────────────────┘
Load tab content only when clicked (performance optimization):
tabs:
- name: details
label: Details
lazy: false # Load immediately
- name: history
label: History (1,234 records)
lazy: true # Load when tab clicked
source: /api/customers/123/historytabs:
- name: details
label: Details
- name: tasks
label: Tasks
badge: 5 # Show "5" badge
badgeVariant: danger # Red badge
- name: notes
label: Notes
badge: { count: 12, variant: info }fields:
- name: detailed_description
span: 12
visible:
desktop: true # Show on desktop
tablet: true # Show on tablet
mobile: false # Hide on mobile
- name: short_summary
span: 12
visible:
desktop: false # Hide on desktop
tablet: false # Hide on tablet
mobile: true # Show on mobilesection:
columns:
portrait: 1 # 1 column in portrait mode
landscape: 2 # 2 columns in landscape mode
fields: [field_a, field_b, field_c]Display child records within a parent record's page.
sections:
- type: related_list
label: Contacts
object: contact
relationField: account_id # contact.account_id → account.id
columns:
- name
- email
- phone
actions:
- type: standard_new
label: New ContactRendered:
┌─ Contacts ─────────────────────────────── [+ New Contact] ─┐
│ │
│ Name Email Phone │
│ ──────────────── ───────────────── ───────────────── │
│ John Doe john@acme.com 555-1234 │
│ Jane Smith jane@acme.com 555-5678 │
│ │
│ Showing 2 of 2 contacts │
└─────────────────────────────────────────────────────────────┘
sections:
- type: related_list
label: Invoice Line Items
object: invoice_line
relationField: invoice_id
mode: inline_edit # Edit cells directly
columns:
- field: product
type: lookup
object: product
- field: quantity
type: number
editable: true
- field: unit_price
type: currency
editable: true
- field: total
type: formula
formula: quantity * unit_price
editable: false
actions:
- type: add_row
label: Add Line ItemWidgets are pre-built UI components that display data or provide functionality.
sections:
- type: widget
component: metric
config:
title: Open Opportunities
value: 47
trend: +12%
trendDirection: up
icon: trending-up
color: successRendered:
┌─────────────────────┐
│ Open Opportunities │
│ │
│ 47 ↑ +12% │
│ │
└─────────────────────┘
sections:
- type: widget
component: activity_feed
config:
object: activity
filter: { related_to: '{recordId}' }
limit: 10
showFilters: truesections:
- type: widget
component: custom.approval_timeline
config:
recordId: '{recordId}'
showComments: truelayout:
type: split_view
orientation: horizontal # or 'vertical'
split: 60 # Left: 60%, Right: 40%
left:
type: list_view
object: opportunity
view: my_opportunities
right:
type: detail_view
object: opportunity
recordId: '{selectedRecordId}'layout:
type: card_grid
columns:
desktop: 3
tablet: 2
mobile: 1
cards:
- type: metric_card
title: Revenue
value: $1.2M
- type: metric_card
title: Deals
value: 47
- type: chart_card
title: Pipeline
chartType: funnellayout:
type: kanban
object: project_task
groupBy: status
columns:
- value: todo
label: To Do
color: gray
- value: in_progress
label: In Progress
color: blue
- value: done
label: Done
color: green
cardFields:
- title
- assignee
- due_date// Source: packages/spec/src/ui/page.zod.ts (PageSchema)
interface Page {
name: string; // lowercase snake_case
label: string;
description?: string;
icon?: string;
type?: 'record' | 'home' | 'app' | 'utility' | 'list'
| 'dashboard' | 'form' | 'record_detail'
| 'record_review' | 'overview' | 'blank'; // default 'record'
object?: string; // bound object for record pages
template?: string; // layout template name, default 'default'
regions: PageRegion[]; // named regions, each holding components
}
interface PageRegion {
name: string; // e.g. "header", "main", "sidebar"
width?: 'small' | 'medium' | 'large' | 'full';
components: PageComponent[];
}// Source: packages/spec/src/ui/view.zod.ts (FormSectionSchema)
interface FormSection {
name?: string; // stable snake_case id for i18n
label?: string;
description?: string;
collapsible?: boolean; // default false
collapsed?: boolean; // default false
columns?: 1 | 2 | 3 | 4; // default 1
visibleWhen?: string; // CEL visibility predicate; hides section when false
fields: (string | FormField)[];
}// Source: packages/spec/src/ui/view.zod.ts (FormFieldSchema)
interface FormField {
field: string; // Field name (snake_case)
type?: FieldType; // auto-infers widget when omitted
colSpan?: number; // Column span within the section grid (1-4)
label?: string; // Override label
placeholder?: string;
helpText?: string;
required?: boolean;
readonly?: boolean;
hidden?: boolean;
widget?: string; // custom widget override
visibleWhen?: string; // CEL visibility predicate
}// Source: packages/spec/src/ui/responsive.zod.ts (BreakpointColumnMapSchema)
// Grid columns (1-12) per named breakpoint. All entries optional.
interface ResponsiveColumns {
xs?: number; // ~480px
sm?: number; // ~640px
md?: number; // ~768px
lg?: number; // ~1024px
xl?: number; // ~1280px
'2xl'?: number; // ~1536px
}Visibility is a single CEL expression string, not a structured rule object.
Since ADR-0089 the one canonical key is visibleWhen across every
layer — data fields, view form sections/fields, and page components — aligning with
the readonlyWhen / requiredWhen family. The element is shown only when the
expression evaluates truthy.
// e.g. on a PageComponent:
visibleWhen: "record.account_type == 'premium'"
// e.g. on a FormSection / FormField:
visibleWhen: "record.status != 'closed' && user.hasRole('admin')"The predicate's binding root is set by the layer, not the key:
| Layer | Predicate binds |
|---|---|
Runtime record forms & pages (*.view.ts, *.page.ts) |
record + current_user (pages also expose page.<var>) |
Metadata-editing forms (*.form.ts) |
data — the row under edit |
The legacy spellings visibleOn (view) and visibility (page) are @deprecated
aliases: still accepted and folded into visibleWhen at parse time, so existing
metadata keeps working. Author new metadata with visibleWhen.
Since @objectstack/spec 15 (ADR-0089 D3a) the form field/section and page
component schemas are strict: a key they do not declare — a visibleWhen
typo, a page-only visibility pasted onto a view field (or vice-versa), or a
stale key — is now a loud parse error instead of being silently dropped. The
error names the offending key and, when it looks like a visibility predicate,
points you back at visibleWhen.
Breakpoint-based show/hide is handled separately via the component's
responsive.hiddenOn array (e.g. hiddenOn: ['xs', 'sm']), see
packages/spec/src/ui/responsive.zod.ts.
name: customer_360
object: customer
template: standard
regions:
header:
- type: title
field: name
- type: actions
buttons: [edit, delete, share, convert]
main:
- type: section
label: Overview
columns: 2
fields:
- name
- status
- industry
- employee_count
- website
- phone
- type: section
label: Key Contacts
columns: 1
component:
- type: related_list
object: contact
relationField: account_id
columns: [name, title, email, phone]
- type: section
label: Open Opportunities
component:
- type: related_list
object: opportunity
relationField: account_id
filter: { stage: { $ne: 'closed_won' } }
columns: [name, amount, close_date, stage]
sidebar:
- type: widget
component: metric
config:
title: Total Revenue
value: $1.2M
- type: widget
component: activity_feed
config:
limit: 10name: employee_onboarding
object: employee
template: wizard
steps:
- name: personal_info
label: Personal Information
sections:
- label: Basic Details
columns: 2
fields:
- first_name
- last_name
- email
- phone
- date_of_birth
- ssn
- name: employment
label: Employment Details
sections:
- label: Position
fields:
- job_title
- department
- manager
- start_date
- employment_type
- name: compensation
label: Compensation
sections:
- label: Salary & Benefits
fields:
- base_salary
- bonus_eligible
- equity_grant
- benefits_plan
- name: review
label: Review & Submit
type: summary
sections:
- type: summary_view
showAllFields: trueLoad sections/tabs only when visible:
sections:
- label: Details
lazy: false # Load immediately
- label: History (10,000 records)
lazy: true # Load when section expanded
source: /api/customers/123/historyFor long lists of fields:
section:
label: Product Catalog (1,000 items)
virtualScroll: true
itemHeight: 60 # px
fields: [...] # Large arrayRender above-the-fold content first:
layout:
renderStrategy: progressive
priority:
- sections[0] # Render first section immediately
- sections[1] # Render second section after 100ms
- sections[2] # Render third section after 200ms- Page Reference - Page, region, and component schemas
- View Reference - Form, list, kanban, and calendar views
- Responsive Reference - Responsive and performance configuration