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 declaration

The 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
---

![](/icons/rocket.svg)

# 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:

TypeUIValue
selectDropdownString from options
booleanToggletrue/false
stringText inputAny string
numberNumber inputNumeric 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 build

This generates:

  • dist/entry.js — Bundled components
  • dist/meta/schema.json — Component metadata for editors

Generating Documentation

Generate COMPONENTS.md for content authors:

cd foundation
uniweb docs

This creates documentation with each component's:

  • Description and family
  • Content expectations
  • Parameters and defaults
  • Available presets

Best Practices

  1. Graceful degradation — Handle missing content without errors
  2. Sensible defaults — Every param should have a good default
  3. Intent over implementation — variant: featured not backgroundColor: #1a1a1a
  4. Minimal required content — Components should render with minimal input
  5. Consistent naming — Use the standard content element names

See Also