Adding an element
This tutorial adds a Notice element, a colored message box that visitors can close, from a child theme. The same folder works in a plugin. The full format of each file is in the builder internals.
Where elements live
Each element is one folder with a config.json. BlueTheme looks in three places. A later place wins when two folders have the same name, so a child theme can replace a theme element by copying its folder.
bluetheme/elements/: the theme's own elements.bluetheme-child/elements/: the active child theme's elements.- Any folder added with the
bluetheme_builder_element_dirsfilter, for plugins:
add_filter( 'bluetheme_builder_element_dirs', function ( array $dirs ): array {
$dirs[ plugin_dir_path( __FILE__ ) . 'elements' ] = plugin_dir_url( __FILE__ ) . 'elements';
return $dirs;
} );
The Notice element goes in bluetheme-child/elements/notice/:
elements/notice/
config.json the element's name, category and settings
render.php prints its markup
style.css loaded only on pages that use it
script.js the same, deferred, in the footer
1. config.json
notice/config.json
{
"title": "Notice",
"description": "A colored message box with an icon, which visitors can close.",
"category": "boxes",
"icon": "bi:megaphone",
"keywords": [
"alert",
"message"
],
"summary": "title",
"textdomain": "bluetheme-child",
"fields": [
{
"type": "select",
"id": "kind",
"label": "Kind",
"default": "info",
"choices": {
"info": "Information",
"success": "Success",
"warning": "Warning"
}
},
{
"type": "icon",
"id": "icon",
"label": "Icon",
"default": "bi:info-circle"
},
{
"type": "text",
"id": "title",
"label": "Title",
"default": "Good to know",
"translate_default": true
},
{
"type": "textarea",
"id": "text",
"label": "Text",
"default": ""
},
{
"type": "switch",
"id": "dismissible",
"label": "Visitors can close it",
"default": true
},
{
"type": "color",
"id": "accent",
"label": "Accent color",
"default": "",
"tab": "style",
"css": {
"selector": "{{WRAPPER}} .notice-box",
"var": "--notice-accent"
}
},
{
"type": "number",
"id": "space",
"label": "Space inside",
"default": "",
"unit": "px",
"responsive": true,
"tab": "style",
"css": {
"selector": "{{WRAPPER}} .notice-box",
"property": "padding",
"unit": "px"
}
}
],
"style_selectors": {
"typography": "{{WRAPPER}} .notice-box__text"
}
}
- title, description, category, icon, keywords: how the element appears in the builder's Elements tab. The description is the tooltip. The category is one of
typography,media,boxes,interactive,sliders,dynamic,post,woocommerce,product,headerorother. The icon is anypack:namethe icon picker knows. - summary: the field the backend builder shows on the element's card.
- textdomain: the text domain that translates the labels. Leave it out for the theme's own
bluetheme. - fields: the element's settings, using the Theme Options field types. Fields go on the Content tab unless
tabsaysstyleoradvanced.responsive: truestores one value per device.translate_default: truetranslates the default text.cssturns the value into CSS:{{WRAPPER}}is the element's own class, and the rule sets either apropertyor a custom property (var). CSS is written only for values that differ from the default, into the page's generated stylesheet, per device.
- style_selectors: points the shared Style settings (here Typography) at a part of the element instead of its outer box.
Every element also gets the shared Style and Advanced settings (spacing, background, border, animation, display conditions and the rest), so don't add those yourself.
2. render.php
notice/render.php
<?php
/**
* Notice element: a colored message box with an icon, which visitors can
* close.
*
* @package BlueTheme_Child
*
* @var array<string, mixed> $settings Element settings.
*/
defined( 'ABSPATH' ) || exit;
use BlueTheme\Builder\Elements\Parts;
use BlueTheme\Builder\Render\Renderer;
use BlueTheme\Icons\Registry as Icons;
$bluetheme_child_title = (string) $settings['title'];
$bluetheme_child_text = (string) $settings['text'];
// Nothing to show: a hint in the builder, nothing on the site.
if ( '' === $bluetheme_child_title && '' === $bluetheme_child_text ) {
if ( Renderer::$editing ) {
echo Parts::hint( __( 'Write a title or a text for the notice.', 'bluetheme-child' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped by Parts.
}
return;
}
$bluetheme_child_kind = in_array( $settings['kind'], array( 'info', 'success', 'warning' ), true ) ? $settings['kind'] : 'info';
echo '<div class="notice-box notice-box--' . esc_attr( $bluetheme_child_kind ) . '" role="note">';
if ( '' !== (string) $settings['icon'] ) {
echo '<span class="notice-box__icon" aria-hidden="true">' . Icons::render( (string) $settings['icon'] ) . '</span>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Icon markup is built by the theme.
}
echo '<div class="notice-box__body">';
if ( '' !== $bluetheme_child_title ) {
echo '<p class="notice-box__title">' . esc_html( $bluetheme_child_title ) . '</p>';
}
echo Parts::text( $bluetheme_child_text, 'notice-box__text' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped by Parts.
echo '</div>';
if ( $settings['dismissible'] ) {
echo '<button type="button" class="notice-box__close" aria-label="' . esc_attr__( 'Close the notice', 'bluetheme-child' ) . '">×</button>';
}
echo '</div>';
$settingsholds the saved values over the defaults. A per-device value is a map such as{ "desktop": 24, "mobile": 12 }, but you don't need it here: thecsskey handles it.- Print the element's inner markup only. The theme wraps it in
<div class="bt-el bt-el-notice bt-n-{id}">. - Escape everything.
Partshas escaped helpers for images, headings, text with line breaks, links, buttons and phone links;BlueTheme\Icons\Registry::render()returns the markup of an icon field. Renderer::$editingis true in the live builder. Use it to show a hint instead of an empty space when required content is missing.
3. style.css
notice/style.css
/* Notice element. Logical properties, so it works in both directions. */
.notice-box {
--notice-accent: var(--bt-color-primary);
display: flex;
gap: 0.75rem;
align-items: flex-start;
padding: 1rem 1.25rem;
border-inline-start: 4px solid var(--notice-accent);
border-radius: var(--bt-radius);
background: color-mix(in srgb, var(--notice-accent) 10%, transparent);
}
.notice-box--success {
--notice-accent: #0f7b46;
}
.notice-box--warning {
--notice-accent: #a15c00;
}
.notice-box__icon {
flex: none;
color: var(--notice-accent);
font-size: 1.25rem;
line-height: 1.4;
}
.notice-box__body {
flex: 1;
min-inline-size: 0;
}
.notice-box__title {
margin: 0 0 0.25rem;
color: var(--bt-color-heading);
font-weight: 700;
}
.notice-box__text > :last-child {
margin-block-end: 0;
}
.notice-box__close {
flex: none;
padding: 0 0.25rem;
border: 0;
background: none;
color: inherit;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
}
.notice-box.is-closed {
display: none;
}
- Use logical properties (
margin-inline-start,border-inline-start,inset-inline-end) so the element works in right-to-left and left-to-right languages without a second stylesheet. The theme'spnpm lint:cssrejectsleftandright. - Use the theme's custom properties (
--bt-color-primary,--bt-color-heading,--bt-radius) so the element follows Theme Options → Colors. - Put the default look here; the
cssfields only add what a user changes.
4. script.js
notice/script.js
/**
* Notice element: the close button hides the box. One listener on the
* document also covers boxes the live builder adds later.
*/
document.addEventListener( 'click', ( event ) => {
const button = event.target.closest( '.notice-box__close' );
// In the live builder a click selects the element instead.
if ( ! button || document.body.classList.contains( 'bt-canvas' ) ) {
return;
}
button.closest( '.notice-box' ).classList.add( 'is-closed' );
} );
- Plain JavaScript, not bundled; no jQuery.
- The live builder replaces sections while someone edits. Listen on the document, as here, or set elements up again on the
bluetheme:renderevent, which fires on the replaced part. - In the live builder
bodyhas thebt-canvasclass. Skip actions there that would leave the page or hide the element.
5. Translate it
The theme translates labels with the domain in textdomain. WordPress's wp i18n make-pot doesn't read JSON, so list the texts in a PHP file that is never loaded, and translate that domain as usual:
<?php
// elements/strings.php: texts from config.json, for translators only.
__( 'Notice', 'bluetheme-child' );
__( 'A colored message box with an icon, which visitors can close.', 'bluetheme-child' );
__( 'Kind', 'bluetheme-child' );
// …every label, description and choice.
The theme's own elements get this file from tools/element-strings.mjs (pnpm i18n).
6. Try it
- Open a page in the live builder. Notice is in the Elements tab under Boxes.
- Drag it onto the page and change its settings. Pick Tablet or Mobile in the device switcher and change Space inside: only that device changes.
- Clear the title and the text. The builder shows the hint; the published page shows nothing.
- Save and view the page as a visitor: the close button hides the notice. The element's
style.cssandscript.jsload only on pages that use it.
Tip This example is tested: it lives in
docs/dev/examples/elements/notice/, andpnpm lint:js,pnpm lint:cssandcomposer lintcheck it with the theme's own code.
More
- Libraries:
"requires": [ "swiper" ]loads a bundled library before the element's script. See libraries. - Plugin elements:
"needs": "woocommerce"hides the element while the plugin is off. - Repeaters and galleries, choices from the site, and assets shared with another element: builder internals.
- The page data format an element is saved in: document.