Creating Components
Build custom components for your foundation.
Component Structure
Each section type lives in its own folder under src/sections/:
src/sections/
└── FeatureCard/
├── FeatureCard.jsx # The React component (or index.jsx — both work)
└── meta.js # Content interface declarationThe meta.js file makes a component a section type — selectable via type: in content. Components without meta.js are ordinary React components that live wherever makes sense (typically src/components/).
Minimal Component
meta.js
export default {
title: 'Feature Card',
family: 'features',
}index.jsx
export default function FeatureCard({ content, params }) {
const { title, paragraphs, images } = content
return (
<div className="p-6 border rounded-lg">
{images[0] && <img src={images[0].src} alt={images[0].alt} className="w-16 h-16" />}
<h3 className="text-xl font-bold mt-4">{title}</h3>
{paragraphs[0] && <p className="text-gray-600 mt-2">{paragraphs[0]}</p>}
</div>
)
}Usage in content
---
type: FeatureCard
---

# Fast Performance
Built for speed with optimized loading and caching.Component Props
Every section type receives:
function MyComponent({ content, params, block }) {
// content - Parsed markdown content
// params - Frontmatter parameters (with defaults from meta.js)
// block - Block instance (access to page, website)
}content
The runtime guarantees this structure:
content = {
// Headings
title: '', // H1
pretitle: '', // `#>` label line above the title
subtitle: '', // Heading after H1
// Body
paragraphs: [], // Paragraph text
links: [], // [{ href, label }]
lists: [], // List items
// Media
images: [], // [{ src, alt, title }]
icons: [], // Small images marked as icons
// Structure
items: [], // Content groups from subsequent headings
data: {}, // Tagged blocks + fetched data
// Document order
sequence: [], // All elements in original order
}No null checks needed. Empty arrays/strings are guaranteed.
params
Configuration from frontmatter, with defaults from meta.js:
---
type: Hero
theme: dark
layout: centered
---function Hero({ params, block }) {
const { layout } = params
// layout = 'centered' (from frontmatter)
// Other params have defaults from meta.js
}theme: dark is not among the params: it is one of the section's own settings — with background, grid, vars and sticky — which framework applies around your component. Here the runtime pins the section's dark context, and semantic tokens (text-heading) follow it. When a component needs the context for its own logic, it reads it with useColorContext(block).
block
Access to page and site context:
function MyComponent({ block }) {
const page = block.page
const website = block.website
console.log(page.title)
console.log(website.name)
}Writing meta.js
The meta.js file defines your component's interface.
Naming the Section Type
Every field is optional. These two tell an editor what the section type is:
export default {
title: 'Feature Card', // Display name (inferred from the name when left out)
family: 'features', // The standard section type it belongs to, so an editor
// can show an illustration, an icon and a translated label
}Run uniweb families for the ids, or see Family.
Describing Content
Document what content your component uses:
export default {
title: 'Hero',
family: 'hero',
content: {
pretitle: 'Eyebrow text',
title: 'Headline',
subtitle: 'Secondary headline',
paragraphs: 'Description [1-2]', // [count] is guidance
links: 'CTA buttons [1-2]',
image: 'Hero image [1]',
},
}The count syntax ([1], [1-2], [2+]) documents expected quantity.
Adding Parameters
Define configurable options:
export default {
title: 'Hero',
family: 'hero',
params: {
variant: {
type: 'select',
label: 'Variant',
options: ['plain', 'bold', 'gradient'],
default: 'plain',
},
layout: {
type: 'select',
label: 'Layout',
options: ['centered', 'left', 'split'],
default: 'centered',
},
showPattern: {
type: 'boolean',
label: 'Show Background Pattern',
default: false,
},
},
}Parameter types:
| Type | UI | Value |
|---|---|---|
select | Dropdown | String from options |
boolean | Toggle | true/false |
string | Text input | Any string |
number | Number input | Numeric value |
Creating Presets
Named combinations of parameters, which an editor offers by label — picking one sets those params on the section:
export default {
title: 'Hero',
family: 'hero',
params: {
variant: { type: 'select', options: ['plain', 'bold', 'gradient'], default: 'plain' },
layout: { type: 'select', options: ['centered', 'left', 'split'], default: 'centered' },
},
presets: {
default: {
label: 'Plain Centered',
params: { variant: 'plain', layout: 'centered' },
},
bold: {
label: 'Bold Dark Hero',
params: { variant: 'bold', layout: 'centered', theme: 'dark' }, // a preset may set the section's theme too
},
gradient: {
label: 'Gradient Split',
params: { variant: 'gradient', layout: 'split' },
},
},
}In a file, write the params themselves. uniweb add section Hero --starter --preset bold prints starter content with that preset's params as its frontmatter:
---
type: Hero
variant: bold
layout: centered
theme: dark
---A preset: key in a section's frontmatter has no effect — the build warns and ignores it.
Background Support
Enable background images/videos:
export default {
title: 'Hero',
family: 'hero',
background: true, // Engine handles background rendering
}Content authors can then configure:
---
type: Hero
background:
image: /images/hero-bg.jpg
overlay:
type: dark
opacity: 0.5
---Handling Items
When content has multiple headings after the main content, they become items:
---
type: Features
---
# Our Features
The best features for your needs.
## Fast
Lightning quick performance.
## Secure
Enterprise-grade security.
## Scalable
Grows with your needs.The component receives:
content.title = "Our Features"
content.paragraphs = ["The best features for your needs."]
content.items = [
{ title: "Fast", paragraphs: ["Lightning quick performance."] },
{ title: "Secure", paragraphs: ["Enterprise-grade security."] },
{ title: "Scalable", paragraphs: ["Grows with your needs."] },
]Render items:
function Features({ content }) {
const { title, paragraphs, items } = content
return (
<section>
<h2>{title}</h2>
<p>{paragraphs[0]}</p>
<div className="grid grid-cols-3 gap-6">
{items.map((item, i) => (
<div key={i}>
<h3>{item.title}</h3>
<p>{item.paragraphs[0]}</p>
</div>
))}
</div>
</section>
)
}Using Tagged Blocks
For structured data that doesn't fit markdown, use tagged data blocks:
---
type: PricingTable
---
# Pricing
Choose the plan that's right for you.
```yaml:plans
- name: Starter
price: 9
features: [5 projects, 1GB storage]
- name: Pro
price: 29
features: [Unlimited projects, 10GB storage, Priority support]
```Access in component:
function PricingTable({ content }) {
const { title, paragraphs } = content
const plans = content.data.plans || []
return (
<section>
<h2>{title}</h2>
<p>{paragraphs[0]}</p>
<div className="grid grid-cols-2 gap-6">
{plans.map(plan => (
<div key={plan.name}>
<h3>{plan.name}</h3>
<p className="text-3xl">${plan.price}/mo</p>
<ul>
{plan.features.map(f => <li key={f}>{f}</li>)}
</ul>
</div>
))}
</div>
</section>
)
}Define the schema in meta.js:
export default {
title: 'Pricing Table',
family: 'pricing',
data: {
plans: {
name: 'string',
price: 'number',
features: { type: 'array', items: { type: 'string' } },
},
},
}Accessing Context
Use hooks from @uniweb/kit to access site context — navigation, routing, theming, and locale:
import { useWebsite, useActiveRoute } from '@uniweb/kit'
function Header({ content }) {
const { website } = useWebsite()
const { isActiveOrAncestor } = useActiveRoute()
const pages = website.getPageHierarchy({ for: 'header' })
return (
<header>
<span>{website.name}</span>
<nav>
{pages.map(page => (
<a
key={page.id}
href={page.route}
className={isActiveOrAncestor(page) ? 'active' : ''}
>
{page.label}
</a>
))}
</nav>
</header>
)
}See Kit Reference for all available hooks.
Building the Foundation
After creating components:
cd foundation
pnpm buildThis generates:
dist/entry.js— Bundled componentsdist/meta/schema.json— Component metadata for editors
Generating Documentation
Generate COMPONENTS.md for content authors:
cd foundation
uniweb docsThis creates documentation with each component's:
- Description and family
- Content expectations
- Parameters and defaults
- Available presets
Best Practices
- Graceful degradation — Handle missing content without errors
- Sensible defaults — Every param should have a good default
- Intent over implementation —
variant: featurednotbackgroundColor: #1a1a1a - Minimal required content — Components should render with minimal input
- Consistent naming — Use the standard content element names
See Also
- Receiving Content — Consuming the parsed structure well: guarantees, multi-line slots, items, the two views
- Component Metadata — Full meta.js reference
- Content Structure — How content is parsed
- Kit Reference — Hooks, components, and utilities
- Foundation Configuration — CSS variables and Layout