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.jsAnything 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, nottitle). - A section marked
'global' => trueisn'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) andrepeater(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 (usee_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#
| Helper | Purpose |
|---|---|
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 --narrowWhen 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.