CSS
Element Studio gives an element two stylesheets, on two tabs:
- CSS is a Twig template. It runs against the user's control values, so it's where
every style the user can change is generated. - Default CSS is plain CSS. It runs once, applies to every instance, and holds the
structural styles that make the element work regardless of what anyone sets.
Most elements need both.
The CSS tab

The template is saved as css.twig and starts life with a single empty rule:
.bde-themeless-template-content-area-4881530-120 { }
.bde-themeless-template-content-area-4881530-120 is replaced at render time with a selector matching the one element instance
being styled. It is the only correct way to target your element — writing a bare class
selector would style every instance on the page, including ones the user styled
differently.
Inside the braces, control values interpolate as Twig:
.bde-themeless-template-content-area-4881530-120 {
background-color: {{ design.container.background }};
gap: {{ design.layout.gap }};
}
You can target descendants by extending the selector, which is how Oxygen's own loop
elements style their items:
.bde-themeless-template-content-area-4881530-120 > .bde-loop > .bde-loop-item {
background-color: {{ design.post.background }};
}
Breakpoints are handled for you
Do not write media queries. Oxygen renders the CSS template once per breakpoint and
wraps each pass in that breakpoint's media query itself. On each pass, control values are
already flattened to that breakpoint's value — so {{ design.container.background }} means
"the background at this breakpoint", with inheritance from wider breakpoints resolved.
Writing your own @media block would nest one media query inside another and the rules
would be dropped.
A few extra variables are available if you need to vary output by breakpoint rather than by
value:
| Variable | What it holds |
|---|---|
breakpoint |
The current breakpoint's id |
isBaseBreakpoint |
true on the base (desktop) pass only |
builtinBreakpoints |
The built-in breakpoint definitions |
globalSettings |
The site's global settings |
childElementCount |
How many children the instance has |
isBaseBreakpoint is the useful one: wrap anything that should be emitted exactly once —
a @keyframes block, a CSS custom property definition — so it doesn't get repeated inside
every media query.
{% if isBaseBreakpoint %}
@keyframes my-element-fade { from { opacity: 0 } to { opacity: 1 } }
{% endif %}
Empty values
A control the user hasn't set interpolates as an empty string, which leaves you with
background-color: ; — invalid, and in a strict parser it can take the whole rule with it.
Guard properties that aren't always set:
{% if design.container.background %}
.bde-themeless-template-content-area-4881530-120 { background-color: {{ design.container.background }}; }
{% endif %}
Macros
Oxygen's own CSS templates lean heavily on macros for the composite design controls,
because a border_complex or spacing_complex value is a structure, not a string:
.bde-themeless-template-content-area-4881530-120 {
{{ macros.borders(design.container.borders) }}
{{ macros.spacing_padding_all(design.container.padding) }}
}
Each of those emits several declarations from one control value. If you add a composite
design control to your element, open a first-party element that already uses the same
control type and copy how it renders — that is far more reliable than reconstructing the
value's shape by hand. Macros covers writing your own.
Default CSS
The Default CSS tab is plain CSS with no Twig and no .bde-themeless-template-content-area-4881530-120. It's emitted once
for the element type rather than per instance, so it targets the element's class name —
shown at the top of the tab and set on the Settings tab:
.oxy-svg-icon2 {
display: inline-flex;
--oxy-icon-size: 1em;
justify-content: center;
align-items: center;
width: var(--oxy-icon-size);
height: var(--oxy-icon-size);
}
.oxy-svg-icon2 > svg {
width: 100%;
height: 100%;
object-fit: contain;
}
That's Oxygen's SVG Icon element. Note what's in there: layout the element needs to
function, sane defaults expressed as custom properties, and styling for the internal markup
its template generates. None of it is user-configurable, and none of it belongs in the Twig
template.
New elements get a generated class name like autogenerated-oe-yourelement — the
lowercased element name, prefixed with initials taken from its namespace (oe for
OxygenElements, ee for EssentialElements). You can replace it with something readable
on the Settings tab, but do it early: changing the class name later leaves your old CSS
pointing at a class nothing has any more, and the studio warns you about exactly that.
Which stylesheet gets a rule
| Put it in | When |
|---|---|
| Default CSS | The element needs it to work at all — display mode, positioning context, internal structure, custom-property defaults |
| CSS | A control decides it |
The order matters as well as the intent: default CSS loads before per-instance CSS, so a
value generated from a control always wins over the default you shipped. That's what lets
you set --oxy-icon-size: 1em as a default and have a size control override it cleanly.