The component contract themes target — what core/plugin components emit, and what the theme is responsible for adding around them.
For the template syntax that invokes components ({component(...)},
{slot(...)}) see
theme-template-reference.md. For
introductory orientation see
theme-development.md.
The principle
Components render the smallest useful semantic HTML. Themes decide the chrome — cards, borders, spacing, colors.
This rule is what lets components be placed into any slot — sidebar, footer column, header-right, a custom after_content region — without the component fighting the surrounding context.
1. What components emit
A well-behaved component template looks like this:
{* search-form.latte — emitted by core *}
<form class="search-form" action="{$action}" method="get" role="search">
<label for="search-input" class="sr-only">Search</label>
<input type="search" id="search-input" name="q"
value="{$query}" placeholder="{$placeholder}">
<button type="submit" class="search-submit" aria-label="Search">...</button>
</form>
No card. No padding. No background. Just the form and its role.
Other built-ins follow the same pattern:
latest_posts→<ul class="post-list"><li>…</li></ul>category_list→<ul class="category-list">…</ul>menu→<ul class="menu-list">…</ul>link_list→<nav class="link-list">…</nav>- Source list cards such as
post-card,job-card, orproperty-card→ one semantic item root with stable child classes for title, excerpt, media, meta, and tags
Each component carries a stable class on its root element so themes can target it. Beyond that, nothing.
Core component class contract:
| Component | Root class | Stable internal classes | Params that change structure |
|---|---|---|---|
search_form |
.search-form |
.sr-only, .search-submit |
placeholder changes text only. |
latest_posts |
.widget.widget-latest-posts |
.widget-title, .post-list, .post-list-item, .post-list-item-thumb, .post-list-item-body |
title adds/removes the heading. count changes item count only. |
category_list |
.widget.widget-category-list |
.widget-title, .category-list, .count |
title adds/removes the heading. .count appears only for non-empty categories. |
tag_cloud |
.widget.widget-tag-cloud |
.widget-title, .tag-cloud, .tag-cloud-item |
title adds/removes the heading. limit changes item count only. |
related_posts |
.related-posts |
.related-posts-title, .widget-title, .related-posts-grid, .related-post-card, .related-post-thumb |
title adds/removes the heading. count changes item count only. Requires post context. |
author_profile |
.author-profile |
.author-avatar, .author-info, .author-name, .author-bio, .author-links |
No params. Optional author fields add/remove child elements. |
menu |
.menu-list |
.menu-item, .has-children, .sub-menu |
location selects the menu. Nested items add .has-children and .sub-menu. |
link_list |
.link-list, .link-list--horizontal, .link-list--vertical |
.link-list__item |
links controls anchors. layout changes the root modifier class. |
Plugin components follow the same contract. They may ship admin UI CSS for their iframe settings screens, but their frontend output should stay semantic and minimally styled. The active theme owns the visible chrome.
Common bundled plugin component classes:
| Plugin | Frontend classes themes should expect |
|---|---|
| Form | .td-form, .td-form-field, .td-form-success, .td-form-error, .td-form-required, .td-form-submit, .td-form-thanks |
| Social | .td-social-share, .td-social-share-list, .td-social-share-item, .td-social-share-copy, .td-social-follow, .td-social-follow-list, .td-social-follow-item |
External Source list components opt into the Source mapping UI through
theme.json (source_list.compatible: true). Their frontend output
still follows the same bare-component rule; the declaration only tells
the admin which fields can be mapped into the component.
2. What the theme does
The theme applies chrome. Prefer slot-scoped rules for furniture that changes by placement, and component-class rules for base affordances that should remain recognizable anywhere.
Example from Kinari's stylesheet:
/* Every direct sidebar child becomes a card — consistent widths,
consistent padding, regardless of which component landed there. */
.sidebar > * {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 1.25rem;
}
/* Header slot is compact and transparent — reset anything the
component template brought in, then size it for the header. */
.site-header-actions { width: 220px; }
.site-header-actions > * {
background: transparent;
border: 0;
padding: 0;
}
/* Footer columns: clear background, no border. */
.footer-column > .widget,
.footer-column > ul,
.footer-column > div {
background: transparent;
border: 0;
padding: 0;
}
The same <form class="search-form"> element renders as a padded card in
the sidebar, a compact icon group in the header, and a bare form in the
footer — all driven by the theme's slot-scoped selectors.
For plugin components, keep the same split. The theme can style the basic control states once, then let slot rules decide whether the whole component sits in a card, a footer column, or inline content:
/* Base affordance: inputs and submit buttons should be usable anywhere. */
.td-form input,
.td-form textarea,
.td-form select {
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 0.65rem 0.75rem;
}
.td-form-submit,
.td-social-share a,
.td-social-share-copy,
.td-social-follow a {
background: var(--color-accent);
color: var(--color-on-accent);
border-radius: 6px;
}
/* Layout: the slot decides how the component fits the page. */
.sidebar > .td-form,
.sidebar > .td-social-share,
.sidebar > .td-social-follow {
background: var(--color-surface);
border: 1px solid var(--color-border);
padding: 1.25rem;
}
3. Why this matters
- Portability. Any component (core or plugin) works in any slot your theme declares, without the theme author having to style every combination.
- Accessibility utilities ship with the theme. Things like
.sr-only/.skip-linkare theme responsibilities — components just use the classes assuming they exist. Declare them in your theme stylesheet. - Plugin compatibility. A third-party
sns_linkscomponent you've never seen will still look correct because the chrome comes from your slot CSS, not from the plugin. - Theme settings compatibility. Plugin components automatically inherit the site's colours, borders, spacing, and type scale when their chrome uses the theme's semantic CSS variables.
4. Frontend CSS responsibility for plugins
Frontend CSS for plugin components belongs in themes, not in plugin assets, unless the CSS is purely functional and cannot reasonably be owned by a theme. A plugin may provide stable classes, ARIA attributes, data attributes, and minimal structure. It should not impose cards, brand colours, large spacing, shadows, or typography on the public site.
Bundled themes should style bundled plugin components well enough that enabling a plugin never creates an unstyled first-run experience. Third party themes should at least cover stable classes from the components they expect site operators to use, especially forms and social links.
5. Convention: mandatory utility classes
Every theme MUST define:
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.skip-link { /* visible on :focus, invisible otherwise */ }
Components assume these exist. The component API treats them as part of
the theme contract — just like <body> existing or CSS being loaded.
Source: https://github.com/typedock/core/blob/main/docs/theme-components.md