Theme chrome
Some pages belong to core rather than to your theme: the account-delete confirmation, the credits wallet, the buy and orders screens. Core uses your theme’s view if you ship one. If you do not, it needs somewhere to put the page.
Theme chrome is the pair of views that opens and closes a page on your site:
the one printing <!doctype html> through the site header, and the one closing
</body>. Tell core where they are, and those core-owned pages render inside
your layout, with your header, your footer, your typography.
You probably do not need to do anything
Section titled “You probably do not need to do anything”Core finds chrome on its own, first hit wins:
- what your theme declared (below)
header.php+footer.phpin the theme rootcommon/header.php+common/footer.php
Each is tried in your theme first, then in the parent theme when your
index.php names one. A child theme that ships no chrome inherits its
parent’s. The bundled fallback theme is deliberately not in that walk: it knows
nothing about your site, so core renders its own page instead.
Both halves must exist. A header with no footer is not chrome: core would leave the page unclosed, so it falls through to its own standalone page instead.
If your theme uses either conventional pair, it already works. Declare only when your layout does not match one.
Declaring
Section titled “Declaring”In your theme’s functions.php:
osc_add_theme_support('chrome', array( 'header' => 'parts/site-header.php', 'footer' => 'parts/site-footer.php',));Both paths are relative to your theme directory. An absolute path, or one
containing .., is refused and core falls back to the probes.
Add 'account' => true when your chrome also draws the account area, so core’s
credits pages (wallet, buy, orders) render inside it rather than through your
user-custom.php:
osc_add_theme_support('chrome', array( 'header' => 'parts/site-header.php', 'footer' => 'parts/site-footer.php', 'account' => true,));Rendering chrome yourself
Section titled “Rendering chrome yourself”osc_get_header(); // true when the theme has chrome, false otherwiseosc_get_footer();Both return false rather than printing anything when there is no chrome, so a
caller can fall through to something else:
if (!osc_get_header()) { // no chrome on this theme, so render a self-contained page instead}osc_theme_has_chrome() answers the same question without rendering, and
osc_theme_chrome() returns the resolved pair as absolute paths (or null)
if you need to know which files answered rather than just whether any did.
$chrome = osc_theme_chrome(); // ['header' => '/…/common/header.php', 'footer' => …]What core puts between them
Section titled “What core puts between them”Core prints its page markup wrapped in .oe-page and .oe-doc, and injects one
small stylesheet through the header hook: the same hook your <head> already
runs for enqueued scripts and styles. Every selector in it is .oe-* prefixed,
so it cannot reach your own markup on the same page.
Text colour and typography are deliberately not set on that path: the page inherits yours, so it reads as part of your theme rather than a panel dropped into it.
Styling core pages yourself
Section titled “Styling core pages yourself”Core styles its fallback pages and the comments block (osc_show_item_comments())
unless your theme says it will. To style the .oe-* classes in your own CSS and
drop core’s sheets:
osc_add_theme_support('core-styles', false);Core then prints neither sheet on a page rendered inside your chrome. Core’s own
standalone page, shown when a theme has no chrome, keeps its styles either way.
osc_core_styles_enabled() tells you which applies. Older cores ignore the
declaration and keep printing their styles.
Theme supports in general
Section titled “Theme supports in general”chrome is one feature. The registry is generic:
osc_add_theme_support(string $feature, mixed $args = true): voidmixed osc_theme_supports(string $feature); // the args, or falseosc_remove_theme_support(string $feature): void;Call osc_add_theme_support() from functions.php, which core loads before it
renders anything. A feature nobody declared reads as false, and core does what
it did before: declaring is always optional.
Core reads these features:
| Feature | Value | Effect |
|---|---|---|
comments |
false |
Settings, Comments shows a note that the theme shows no comments. Unset or true shows nothing. |
category-depth |
integer, 1 or more | How many levels of categories the theme shows. See below. |
Category depth
Section titled “Category depth”osc_add_theme_support('category-depth', 2);This says the theme shows two levels of categories: top-level ones and their children. It is the only limit. Without it, categories nest without limit.
When it is declared:
- The admin Categories screen refuses a drag, key move, Move to…, Parent choice or Add subcategory that would go deeper, and shows Too deep.
- The REST API refuses the same create and move requests. Core keeps a copy of the number in the
category_depthpreference, refreshed whenever an admin page loads, because the API does not load the theme. - Categories already deeper stay where they are and are marked; nothing is moved. They may move sideways or up, never deeper.
Declare the number your templates really show. Menus have their own depth per place (below).
Declaring extra views
Section titled “Declaring extra views”A static page’s internal name becomes a URL segment, so core keeps a list of
names a page may not take. Otherwise a page slugged contact would shadow the
contact route. That list is core’s own view vocabulary, and a theme adds to it:
osc_add_theme_support('views', array( 'user-wishlist', 'template-promo',));Names may be written with or without .php. A declaration only ever adds:
core’s own names stay reserved whatever you declare, and a theme that declares
nothing behaves exactly as before.
osc_theme_view_names() returns the whole reserved set (core’s names plus
anything the active theme declared) as names without a directory and without
.php. The admin page editor uses it to refuse a colliding slug; a plugin that
offers its own page-naming UI should check against the same list rather than
hardcoding one.
in_array('contact', osc_theme_view_names(), true); // true, reserved by coreDeclaring widget zones
Section titled “Declaring widget zones”A theme has always listed its widget zones on the Widgets: line of
index.php. That line gives core a slug and nothing else, so the admin screen
labels the zone footer and cannot say what it is or where it renders.
Declare them instead:
osc_add_theme_support('widget_locations', array( 'header' => array( 'label' => __('Masthead', 'mytheme'), 'description' => __('Below the navigation.', 'mytheme'), ), 'footer' => array( 'label' => __('Colophon', 'mytheme'), 'description' => __('Above the copyright line.', 'mytheme'), ),));The declared order is the order the admin shows them in. description is
optional; a zone with no label falls back to its slug. A bare list
(array('header', 'footer')) and a slug => label map are both accepted.
Declare from the init hook rather than the top of functions.php if your
labels are translated: core requires functions.php before the translation
layer is initialised.
A theme that declares nothing keeps its Widgets: line, with each slug
standing in as its own label, exactly what it does today.
osc_widget_locations() returns the resolved map, and passes through the
widget_locations filter, which is how a plugin contributes a zone of its own.
Anything in that map is placeable: the admin builds its drop zones from it and
refuses a move into a zone that is not in it.
Rendering a zone is unchanged:
osc_show_widgets('footer');It prints nothing when the zone is empty, which is most zones on most sites, so buffer it if your wrapper would otherwise render as an empty box.
The owner builds menus in Appearance → Menus. Read one with osc_menu():
foreach (osc_menu('main') as $link) { echo '<a href="' . osc_esc_html($link['url']) . '">' . osc_esc_html($link['label']) . '</a>'; // $link['children'] holds the items below it, same shape, when there are any.}mainandfooteralways exist. Until the owner saves them they show your place’sdefault(below), else the top categories and the linked pages.- Other ids are menus the owner made. An unknown id gives an empty array.
- Labels are plain text in the visitor’s language. Escape them when printing.
- Items nest up to three levels. Show as many as your design has room for.
Declare the places your theme shows menus, so the editor can name them and warn about items that will not show:
osc_add_theme_support('menus', [ 'main' => ['label' => __('Top bar', 'mytheme'), 'depth' => 1], 'footer' => ['label' => __('Footer', 'mytheme'), 'depth' => 1, 'hint' => __('Shows in one row; keep it short.', 'mytheme')],]);The key is a menu id. label is required; depth is how many levels you show, 1 to 3 (default 3). hint is an optional plain-text note the editor shows under the place. A declared menu the owner has not made yet is still listed in the editor. Entries that do not fit are skipped. Older cores ignore the key.
A place can declare default: the menu shown until the owner saves their own. The editor starts from it, so the owner sees what the site shows and can change it. Use the automatic menu goes back to it.
osc_add_theme_support('menus', [ 'main' => ['label' => __('Header', 'mytheme'), 'depth' => 1, 'default' => [ ['type' => 'builtin', 'ref' => 'home'], ['type' => 'builtin', 'ref' => 'search'], ['type' => 'builtin', 'ref' => 'contact', 'label' => __('Help', 'mytheme')], ]],]);Items have the stored shape: type is builtin, category, page (with an id in ref) or link (with url), plus optional label and children. In a default, label may be one string, used for every language; otherwise it is [locale => text]. A default that would not pass the editor’s save checks is ignored.
The builtin refs are home, search, contact, post, login and register. Core builds their URLs on each request, so they follow the permalink setting, and gives them its own translated labels unless the item has one. login and register drop out when users are off (register also when sign-up is off), and post when guests may not post or the site is a shop. Owners add them from Built-in pages in the editor.
A link to this site is stored as a path and printed as a full URL; links to other sites are kept as typed.
The core.menu widget prints one menu as <nav class="osc-menu"> with nested <ul> lists and an optional <h2 class="osc-menu-title">. Core adds no CSS for it; style .osc-menu in your theme. osc_menu_choices() lists the menus as id => name.
The API serves the same menus at GET /menus, with children on nested items. See the API changelog.