HTML
The HTML tab holds your element's markup. It's a Twig
template, saved as html.twig next to element.php, and it renders on every page the
element appears on — in the builder canvas and on the front end alike.
What you write here is the inside of the element. Oxygen supplies the wrapper tag itself,
along with its class and any attributes, from the Settings and
HTML Attributes tabs. A template that starts with a <div> gets you
two nested divs.
Reading control values

Control values arrive as a Twig object shaped exactly like your control tree — tab, then
section, then control slug:
{{ content.content.text }}
That's Oxygen's Text Link element reading a control called text, in a section called
content, on the Content tab. Design-tab controls sit under design by the same rule:
design.spacing.margin_top.
Standard Twig applies, so escaping, defaults, filters and conditionals all work:
{{ content.content.text|default('Click Here') }}
{% if content.card.heading %}
<h3 class="card__heading">{{ content.card.heading }}</h3>
{% endif %}
{% for item in content.card.items %}
<li>{{ item.label }}</li>
{% endfor %}
Twig escapes output by default, which is what you want for anything a user typed. Reach for
|raw only on values that are already HTML — the value of a richtext control, for
instance.
Placeholders
Alongside Twig, Oxygen substitutes a handful of %%NAME%% placeholders after the template
renders. They exist for the things Twig can't know:
— where nested elements render. An element without this placeholder
can't contain anything; an element with it becomes a container. Oxygen's Container element
is one line long:
%%SSR%% — where the output of ssr.php goes. See below.
.bde-themeless-template-content-area-4881530-120 — the CSS selector for this specific element instance. It's mainly a
CSS and Dependencies placeholder, used when a script needs to
target the one instance it belongs to.
%%BREAKPOINT%% — stands in for the active breakpoint inside a property path, so a
single path expression covers all of them. It appears in property paths rather than in
markup.
Server-side rendering with ssr.php
Twig alone can't call WordPress. When your element needs to run a query, call a plugin's
API, or process a shortcode, put that PHP in a file named ssr.php in the element's folder
and drop %%SSR%% into the template where its output belongs.
Oxygen's Shortcode element is the whole pattern in two files. html.twig:
%%SSR%%
ssr.php:
/**
* @var array $propertiesData
*/
echo do_shortcode($propertiesData['content']['shortcode']['full_shortcode'] ?? '');
The file is included with output buffering on, so echo, don't return. $propertiesData
holds the same control values the Twig template sees, as a plain PHP array with the same
content / design → section → slug nesting. Guard every lookup with ?? — a control the
user hasn't touched isn't there.
Two things worth knowing about ssr.php:
- It is not loaded with the rest of the element. Oxygen's element scan deliberately
skips files namedssr.phpand includes yours only at render time. - It runs in the builder too. Every keystroke in a control that the element re-renders
on runs this code again, so keep it cheap — and tell Oxygen exactly which controls should
trigger a re-render, on the Property Paths tab. Anssr.phpdoing
real work with no re-render paths configured makes the builder feel broken.
Macros
When several elements need the same chunk of markup, move it into a
macro and call it from each template rather than copying it. Macros
are Twig macros, created from the studio's New macro entry, and they live in their own
saving location.
Keeping the markup simple
Two habits pay off:
Let Oxygen own the outer element. Your wrapper tag is configurable by the user through
the Settings tab's HTML Tag options — someone can make your card a <section> or an
<article> without touching code. Hard-coding a wrapper in the template takes that away.
Put classes in the template only for internal structure. The wrapper's class is
generated (.oxy-shortcode, .autogenerated-oe-yourelement) and is what your
CSS tab targets through .bde-themeless-template-content-area-4881530-120. Classes you write in the template are for
the parts inside — card__heading, card__meta — and they need matching rules in
Default CSS.