Timberdocs

Developers

Building themes

A Timber theme is a folder of PHP templates plus a stylesheet. Its **schema.php* declares the homepage sections and fields — and that file *generates the editor in the Playground. No admin code to write.

Theme folder#

themes/{slug}/
  theme.json      name, description, optional "provides": ["tailwind"|"bootstrap"]
  schema.php      sections → fields (+ default placeholder copy)
  layout.php      the full HTML document
  home.php        loops the version's visible sections
  sections/*.php  one file per (non-global) schema section
  parts/header.php, parts/footer.php   built-in header and footer
  functions.php   optional helpers, loaded once per request
assets/themes/{slug}/theme.css, theme.js

Anything a theme doesn't provide falls back to templates/: page, post, blog, post-cards, page-hero, form, request-page, 404, maintenance and the head/scripts hooks. Override any of them by adding a file with the same name.

Start from a starter#

Copy themes/birch and assets/themes/birch to a new slug (or press Fork theme in the Playground), edit theme.json, schema.php, sections/*.php and theme.css, then New version in the Playground, choose your theme and preview.

schema.php#

return ['sections' => [
    'chrome' => ['label' => 'Header & footer', 'global' => true, 'fields' => [
        'nav_cta_label' => ['label' => 'Header button', 'type' => 'text', 'default' => ''],
    ]],
    'hero' => ['label' => 'Hero', 'fields' => [
        'hero_title' => ['label' => 'Headline', 'type' => 'textarea', 'rows' => 2, 'default' => 'A *simple* home for your ideas.'],
        'hero_image' => ['label' => 'Image', 'type' => 'media', 'default' => 'assets/img/placeholders/rings-warm.svg'],
        'items' => ['label' => 'Items', 'type' => 'repeater',
            'fields' => ['title' => ['label' => 'Title'], 'text' => ['label' => 'Text', 'type' => 'textarea']],
            'default' => [['title' => 'One', 'text' => '…']]],
    ]],
]];
  • Field names are global across sections — prefix them (hero_title, not title).
  • A section marked 'global' => true isn't a homepage section; its fields are for the header/footer and read anywhere.
  • Types: text, textarea, html, url, media, video, color, toggle, select (options), lines (one per line → array) and repeater (fields, whose sub-fields can be text, textarea, select, color, media, video).
  • Every field takes label, default, help.
  • In headlines, *word* renders as emphasis and <br> as a line break (use e_rich()).

A version saves only the values that differ from your defaults, so improving a theme's defaults still flows through.

layout.php#

<head> <?php partial('head-meta', get_defined_vars()) ?> … your CSS … <?php partial('head-end') ?> </head>
<body>
  <?php partial('admin-bar') ?> <?php partial('body-start') ?>
  <?php region('header', get_defined_vars()) ?>
  <main id="main"><?= $content ?></main>
  <?php region('footer', get_defined_vars()) ?>
  <?php partial('scripts') ?>
  <script src="<?= theme_asset('theme.js') ?>" defer></script>
</body>

region() renders the Global Header/Footer block when one is active, otherwise parts/header.php / parts/footer.php. Keep both so nothing breaks when a block is removed.

Helpers#

HelperPurpose
vc('field', $default)Value of a field for the version being rendered (repeaters are arrays of arrays)
e($s) · e_br($s) · e_rich($s)Escape · escape but keep <br> · also *word* → <em>
href('/blog')Page link; keeps /v/{slug}/ and the share key while previewing
url() · asset() · theme_asset('theme.css') · media_url()Static files; asset() adds ?v=filemtime
menu_html('header', ['class' => 'nav'])A menu with dropdown/mega-menu support (style: list, inline, stack)
timber_icon('bolt', 22)An icon from the built-in set
site_logo_html()The logo image, or the site name as text
social_icon('x'), lines(), e_bold()Small formatters
settings('site.name') · posts_sorted() · current_version() · is_preview()Site data

The CSS contract#

The CMS emits fixed class names; style them (or link the shared assets/themes/_base/base.css, which all three starter themes do):

.container .page-hero .eyebrow .sub .page-body .prose .article .related .post-grid .post-card .post-card-img .post-meta .post-title .post-excerpt .btn .btn-primary .btn-ghost · forms: .form .field .half .check .chip-check .field-error .hp (the honeypot — must be visually hidden) · .request .request-card · .pf-menu-list · .skip-link.

base.css is driven by tokens on :root:

--bg --surface --fg --muted --line --accent --accent-fg --accent-2 --radius --shadow --font --font-head --max --narrow

When the owner picks brand colours, Timber emits --brand, --brand-2 and --brand-fg. Define your accent as --accent: var(--brand, #yourdefault) and your theme keeps its look until a brand colour is chosen.

Shortcodes in content#

[form id="contact"], [menu id="footer" style="stack"], and any Block with a tag. See Menus & Blocks.

Timber is open source. Built by indies, for indies. © 2026