Engine Primitives
Box, Stack, Grid, Text, Heading, Card, Spacer, Divider, Link, Section, Slot. These are the foundational schema node types available in every page.
Shared Base Props
Shared Base Props
All nodes accept these props in addition to their type-specific props:
Prop | Type | Description |
|---|---|---|
|
| HTML id attribute |
|
| CSS class names |
|
| Inline style overrides |
|
| CSS custom properties set inline — cascade to all children |
|
| Skip lazy loading entirely |
|
| Force lazy (true) or eager (false) |
| `ResponsiveValue<string\ | number>` |
| `ResponsiveValue<string\ | number>` |
| `ResponsiveValue<string\ | number>` |
| `ResponsiveValue<string\ | number>` |
|
| Background color |
|
| Text color |
|
| CSS border shorthand |
| `ResponsiveValue<string\ | number>` |
|
| Box shadow |
|
| Long-form box shadow alias |
|
| CSS transition |
|
| Background image controls |
|
| CSS backdrop-filter controls |
|
| CSS position |
|
| Z-index |
|
| Opacity |
|
| Overflow behaviour |
| `(1\ | 2\ |
| `string\ | number` |
| `"margin"\ | "padding"` |
|
| Handler name from |
|
| Wraps node in an |
Numbers in spacing/sizing props are treated as pixels divided by 16 (so 16 → 1rem, 32 → 2rem).
vars — CSS Custom Properties
vars — CSS Custom Properties
Set CSS variables directly on any element. They cascade to all children through the standard CSS variable system.
{
type: "box",
props: {
vars: {
"--card-bg": "#1a1a2e",
"--accent": "#9bcf3a",
"--radius": "12px",
},
bg: "var(--card-bg)",
}
}Keys without a leading -- are auto-prefixed, so "accent" and "--accent" both work.
sides — Per-Side Spacing
sides — Per-Side Spacing
The sides system applies a distance to specific sides of an element using CSS margin (default) or padding. In a CSS grid, adding margin to a cell pushes adjacent cells outward.
Side numbering:
1 (top)
┌───┐
2 (left) │ │ 3 (right)
└───┘
4 (bottom)Diamond effect on a 3×3 grid — give the centre cell outward margin on all sides:
{
type: "grid",
props: { columns: 3, gap: "1rem" },
children: [
// ... 4 outer cells ...
{
type: "box",
props: {
sides: [1, 2, 3, 4], // all four sides
sideDistance: "32px", // push 32px outward on every side
sideType: "margin", // margin = pushes neighbours (default)
bg: "#9bcf3a",
}
},
// ... 4 outer cells ...
]
}Top and bottom push only — vertical stretch effect:
props: {
sides: [1, 4], // top and bottom
sideDistance: "64px",
}Padding variant — push inner content instead of neighbours:
props: {
sides: [2, 3], // left and right sides only
sideDistance: "2rem",
sideType: "padding", // pushes content inward
}box
box
Generic container. Maps to a <div> by default.
Prop | Type | Description |
|---|---|---|
|
| CSS display |
|
| flex-direction |
|
| align-items |
|
| justify-content |
|
| flex-wrap |
| `ResponsiveValue<string\ | number>` |
|
| Override the HTML tag |
stack
stack
Vertical or horizontal flex stack with optional dividers.
Prop | Type | Default |
|---|---|---|
| `"vertical" \ | "horizontal" \ |
| `ResponsiveValue<string\ | number>` |
|
| — |
|
| — |
|
|
|
|
|
|
grid
grid
CSS Grid container.
Prop | Type | Description |
|---|---|---|
| `ResponsiveValue<number\ | string>` |
| `ResponsiveValue<number\ | string>` |
| `ResponsiveValue<string\ | number>` |
| `ResponsiveValue<string\ | number>` |
|
|
|
|
| Min column width for autoFit mode |
|
| Alignment |
text
text
Renders text with a variant system.
Variant | HTML Tag | Style |
|---|---|---|
|
| Fluid type scale using |
|
| 1rem, line-height 1.6 |
|
| 0.875rem |
|
| 1.25rem, lighter weight |
|
| 0.75rem, muted colour |
|
| Uppercase, tracked |
|
| Monospace with code background |
|
| Small caps, wide tracking |
Additional text props: size, weight, align, lineHeight, letterSpacing, truncate, italic, underline, gradient, content, as.
The gradient prop applies a CSS gradient as a text fill (clip + transparent fill).
truncate: true = single line ellipsis. truncate: 3 = three-line clamp.
parts — inline text with embedded links
Use parts instead of content when you need a mix of plain text and hyperlinks within a single text node. When parts is provided, content and child nodes are ignored.
{
type: "text",
props: {
variant: "caption",
parts: [
{ text: "built with love by " },
{
text: "Kastrick",
href: "https://kastricks.com",
target: "_blank",
style: { color: "#555", textDecoration: "none" },
},
],
},
}Each entry in parts is a TextPart:
Field | Type | Description |
|---|---|---|
|
| The visible text for this segment (required) |
|
| If set, renders an |
| `"_blank" \ | "_self" \ |
|
| Rel attribute. Auto-set to |
|
| Per-segment inline style. Plain segments without style render as bare text (no wrapper element) |
Plain segments (no href, no style) render as raw text nodes — no <span> wrapper is added, keeping the DOM clean.
heading
heading
Shorthand combining a heading text node with an optional subheading.
{
type: "heading",
props: {
level: 1,
content: "Main title",
subheading: "Supporting line underneath",
}
}section
section
Page section with a centered max-width content column.
Prop | Type | Default |
|---|---|---|
| `ResponsiveValue<string\ | number>` |
|
|
|
|
|
|
| `"start" \ | "center" \ |
card
card
A styled container with full layout control.
Prop | Type | Default | Description |
|---|---|---|---|
| `"elevated"\ | "outlined"\ | "filled"\ |
|
|
| Hover lift + pointer cursor |
| `"vertical"\ | "horizontal"` |
|
|
| — | Cover image URL |
|
|
| Alt text for the cover image |
|
|
| CSS aspect-ratio of the cover area |
|
|
| CSS object-fit for the cover image |
|
|
| Cover column width when |
|
|
| Padding on the content area |
Any children passed to the card render inside the content area below (or beside) the cover.
// Simple card with cover image
{
type: "card",
props: {
cover: "/images/hero.jpg",
coverAlt: "Mountains at sunset",
coverRatio: "3/2",
variant: "elevated",
interactive: true,
},
children: [
{ type: "text", props: { variant: "h3", content: "Alpine Trek" } },
{ type: "text", props: { content: "A 5-day route through the high passes." } },
]
}
// Horizontal card (media left, content right)
{
type: "card",
props: {
direction: "horizontal",
cover: "/images/thumb.jpg",
coverAlt: "Article thumbnail",
coverWidth: "35%",
variant: "outlined",
innerPadding: "1.5rem",
},
children: [
{ type: "text", props: { variant: "h4", content: "Getting Started" } },
{ type: "text", props: { content: "Everything you need to know." } },
]
}spacer / divider
spacer
Inserts whitespace. axis: "y" (default) or axis: "x".
divider
A <hr> with configurable orientation, color, thickness, and style.
divider
divider
A <hr> with configurable orientation, color, thickness, and style.
slot
slot
Renders a named piece of React content injected via createPage({ slots: { … } }). Useful for dynamic content that can't be expressed as a schema node.
// In schema:
{ type: "slot", props: { name: "userWidget", fallback: { type: "text", props: { content: "Loading…" } } } }
// In createPage:
createPage({
schema: MySchema,
slots: { userWidget: <UserWidget /> }
})link
link
The primary routing primitive. It handles external URLs automatically and integrates with the next-view-transitions pipeline.
Prop | Type | Description |
|---|---|---|
|
| Destination URL. Defaults to |
|
| HTML anchor target (e.g., |
| `string \ | function` |
|
| Text content (alternative to schema children) |
|
| Behavior config: `{ transition: "page-to-page" |
Transition Pipeline:
page-to-page: Uses the View Transitions API for seamless full-page animations.instant: Standard high-speed client-side navigation.
Routing pipeline: EngineLink delegates all anchor rendering to renderEngineAnchor exported from EngineNav. The three-strategy routing (external / animated / native) lives in one place.
Note: EngineLink merges styles from cprop.styles and cprop.link.styles before processing.
button
Renders a <button> or <a> depending on whether href is set.
Use type: "link" for navigation — "button" is for actions.
Prop | Type | Default | Description |
|---|---|---|---|
|
| — | Button text content |
| `"solid" \ | "outline" \ | "ghost" \ |
| `"xs" \ | "sm" \ | "md" \ |
|
|
| Override the accent colour for this button only |
|
| — | Icon string (emoji or icon font class) |
| `"left" \ | "right"` |
|
|
|
| Disables the button |
|
|
| Shows a spinner and disables the button |
|
|
| Stretches to full container width |
| `"button" \ | "submit" \ | "reset"` |
|
| — | When set, renders as |
|
| — | Handler name from |
Variant styles:
Variant | Look |
|---|---|
| Filled with accent colour |
| Transparent with accent border |
| Transparent, no border |
| Solid with drop shadow glow |
| Underlined text, no padding |
// Action button
{ type: "button", props: { label: "Save", variant: "solid", size: "md", onClick: "handleSave" } }
// Submit button inside a form
{ type: "button", props: { label: "Sign in", variant: "solid", type: "submit", fullWidth: true } }
// Link button
{ type: "button", props: { label: "View docs", variant: "outline", href: "/docs", size: "lg" } }custom-select
A fully custom dropdown select — keyboard navigable, searchable, clearable.
Replaces native <select> where React 19 rejects <div> children.
Prop | Type | Default | Description |
|---|---|---|---|
|
| required | Field name — used for |
|
| — | Visible label rendered above the dropdown |
|
| required | Array of |
|
| — | Text shown when no option is selected |
|
| — | Initially selected value |
|
|
| Adds a search input inside the dropdown |
|
|
| Shows a clear (×) button to deselect |
| `"sm" \ | "md" \ | "lg"` |
|
| — | Handler name called with the selected value |
{
type: "custom-select",
props: {
name: "plan",
label: "Pricing plan",
placeholder: "Choose a plan...",
searchable: true,
clearable: true,
size: "md",
onChange: "handlePlanChange",
options: [
{ value: "free", label: "Free" },
{ value: "pro", label: "Pro — $9/mo" },
{ value: "team", label: "Team — $29/mo" },
{ value: "legacy", label: "Legacy", disabled: true },
],
},
}option / optgroup
Native <option> and <optgroup> for use inside as="select" on a box.
These are necessary because React 19 rejects unknown children inside <select>.
// A native select using box + option
{
type: "box",
props: { as: "select", name: "country", style: { padding: ".5rem" } },
children: [
{ type: "option", props: { value: "", label: "Choose a country", disabled: true } },
{ type: "option", props: { value: "us", label: "United States" } },
{ type: "option", props: { value: "gb", label: "United Kingdom" } },
{ type: "option", props: { value: "au", label: "Australia" } },
],
}
// Grouped options using optgroup
{
type: "box",
props: { as: "select", name: "city" },
children: [
{
type: "optgroup",
props: { label: "United States" },
children: [
{ type: "option", props: { value: "nyc", label: "New York" } },
{ type: "option", props: { value: "la", label: "Los Angeles" } },
],
},
{
type: "optgroup",
props: { label: "United Kingdom" },
children: [
{ type: "option", props: { value: "ldn", label: "London" } },
{ type: "option", props: { value: "mcr", label: "Manchester" } },
],
},
],
}option props: value (string), label (string — the visible text),
disabled (boolean), selected (boolean).
optgroup props: label (string — the group heading), disabled (boolean).
Prefer custom-select over native box as="select" when you need search,
clear, or consistent cross-browser styling.
raw
Escape hatch — renders an arbitrary React component directly inside the schema
tree. Use when a component can't be expressed as schema nodes and registering
it via registerComponent isn't appropriate (e.g. a third-party widget).
import { RawComponentRenderer } from "@/engine";
// Register once at module level
import MyThirdPartyChart from "some-chart-library";
{
type: "raw",
props: {
component: MyThirdPartyChart,
// All other props are forwarded to the component
data: [...],
width: "100%",
height: 400,
theme: "dark",
},
}raw bypasses the registry lookup entirely — component must be a valid
React component. For reusable custom nodes, use registerComponent instead
so your component is addressable by type string everywhere in the schema.