Styling
How Forms and Popups are styled: the theme.json tokens Forms reads, the custom properties Popups exposes, the class names both emit, and how to switch the shipped CSS off.
Both front-end plugins ship a stylesheet you are meant to re-skin rather than replace. Neither raises specificity beyond a single class, and both drive every value they care about through a CSS custom property. Setting those properties is the supported way to make a form or a popup look like the rest of the site.
They read them from different places, so they are covered separately.
Forms
The token pipeline
Forms ships its defaults as a styles.json at
dist/packages/forms/styles.json. On every request the plugin merges that file
into the site's global styles under settings.custom.xp-form, and WordPress then
emits the merged result as CSS custom properties named
--wp--custom--xp-form--<token>.
That is the whole mechanism, and it has one useful consequence: a token you set
in theme.json and a token the plugin ships end up in exactly the same place, so
your value simply wins.
{
"settings": {
"custom": {
"xp-form": {
"primary-color": "var(--wp--preset--color--primary)",
"input-brd-radius": "0.25rem",
"input-brd-color": "var(--wp--preset--color--border)"
}
}
}
}Nested tokens flatten with a double dash, so input-padding.top is read as
--wp--custom--xp-form--input-padding--top.
The tokens
74 in total, grouped by what they style. Every value below is the shipped default; anything you leave out keeps it.
| Token | Default |
|---|---|
spacer | var(--wp--style--block-gap, 1.5rem) |
primary-color | #2563eb |
error-color | #b42318 |
warning-color | #b54708 |
success-color | #067647 |
primary-color is the focus ring and accent. The three status colours are
referenced by the input, validation and alert tokens, so changing one of
them re-colours every state that uses it in one edit.
| Token | Default |
|---|---|
label-color | currentColor |
label-font-size | 1em |
label-font-style | normal |
label-font-weight | 600 |
label-line-height | 1.35 |
label-margin | object of top, right, bottom, left |
info-color | currentColor |
info-font-size | 0.875em |
info-font-style | normal |
info-font-weight | 400 |
info-line-height | 1.4 |
info-margin | object of top, right, bottom, left |
validation-txt-color | var(--wp--custom--xp-form--error-color) |
validation-txt-font-size | 0.875em |
validation-txt-font-style | normal |
validation-txt-font-weight | 400 |
validation-txt-line-height | 1.4 |
info-* styles a field's description. The font sizes are in em, so they
follow whatever the surrounding theme sets rather than fixing a pixel size.
label-margin and info-margin are applied with !important, to hold
off themes that set a blanket margin on every label and p. Plain CSS
of your own will lose to them — change these two through the token.
The base set:
| Token | Default |
|---|---|
input-padding | object of top, right, bottom, left |
input-font-size | 16px |
input-font-weight | 400 |
input-line-height | 1.5 |
input-color | #111827 |
input-bg-color | #fff |
input-brd-width | 1px |
input-brd-color | #64748b |
input-brd-radius | 0.5em |
input-box-shadow | none |
input-placeholder-color | #64748b |
Then one variant per state — disabled, hover, focus, error,
error-focus, success and success-focus — each with its own color,
bg-color, brd-color and box-shadow, named
input-<state>-<property>. For example input-focus-brd-color and
input-error-focus-box-shadow.
Most of them default to a var() pointing back at the base or at a status
colour, which is why setting primary-color alone already moves the focus
border. The three focus rings — input-focus-box-shadow,
input-error-focus-box-shadow and input-success-focus-box-shadow — are
0 0 0 2px var(--wp--custom--xp-form--primary-color|error-color|success-color)
and now follow the status colours too. Only input-hover-brd-color
(#475569) still carries a literal that has to be re-coloured by hand.
input-font-size is 16px on purpose. Below 16px, iOS Safari zooms the
page when a field takes focus and does not zoom back out.
| Token | Default |
|---|---|
checkbox-label-color | currentColor |
checkbox-label-font-size | 1em |
checkbox-label-font-style | normal |
checkbox-label-font-weight | 700 |
checkbox-label-line-height | 1.1 |
checkbox-label-margin | 0 0 0.2em 0 |
These style the label beside a single control, not the group's own label,
which uses the label-* tokens.
The success and error messages a form shows after submitting.
| Token | Default |
|---|---|
alert-brd-radius | 0.5em |
alert-success-color | #05603a |
alert-success-bg-color | #ecfdf3 |
alert-success-brd-color | #abefc6 |
alert-error-color | #b42318 |
alert-error-bg-color | #fef3f2 |
alert-error-brd-color | #fecdca |
Where a value can come from
Five layers can set the same token. Highest first:
- The Fields panel on one form's Form builder block. It saves the values in
the block's
fieldStyleattribute and prints them as custom properties in the builder's ownstyleattribute, so they reach that form's fields only. - The form editor's Styles sidebar, which writes into the global styles user layer. These values are site-wide, not per-form.
settings.custom.xp-formin the active theme'stheme.json.- The plugin's shipped
styles.json. - Nothing — the property is not emitted and the stylesheet's own fallback applies.
Layers 2 to 4 are merged into global styles, which WordPress prints on :root, so
a state token that defaults to another token, such as input-hover-color, already
holds a finished value by the time it reaches a form. A Fields setting therefore
writes every state that follows it:
| Fields setting | Tokens it sets on the builder |
|---|---|
| Text | input-color, plus the hover, focus, error, error-focus, success and success-focus text colours |
| Background | input-bg-color, plus the same six states |
| Border | input-brd-color, input-hover-brd-color |
| Focus border | input-focus-brd-color |
| Placeholder | input-placeholder-color |
| Label | label-color |
| Description | info-color |
| Border width | input-brd-width |
| Radius | input-brd-radius |
| Label size | label-font-size |
Palette colours and font sizes are kept as presets, for example
var(--wp--preset--color--accent), so a palette change still reaches the form. A
builder with no Fields settings saves no extra markup.
Switching the shipped CSS off
Three layers can disable it, and they are asked in this order:
- The
xpac_forms_disable_default_stylesfilter, which XPAC → Forms → Settings → Appearance feeds at priority 1.truehere is final: the style handle is deregistered on the front end and in wp-admin, and the tokens are never merged into global styles at all. settings.custom.xp-form.defaultStylesin the global styles user layer, written by the Styles panel. Present decides either way, so a site that turned it back on overrides a theme that ships it off.- The same key in the theme's
theme.json.
Turning it off at layer 1 takes the custom properties with it, because the
filter that merges them is never added. Anything of yours written in terms of
--wp--custom--xp-form--* stops resolving. Layers 2 and 3 drop the stylesheet
but keep the tokens.
Class names
Stable, and reported at runtime by
form.xpacForm.utils so JavaScript
never has to hard-code them.
| Class | On |
|---|---|
xp-block-form | The <form> element |
xp-is-loading | The form, while a submission is in flight |
xp-form__response | The message wrapper, plus xp-form__response-success or -error |
xp-block-form-field | Every field wrapper |
xp-block-form-field--<type> | The same wrapper, once per field type — --text, --email, --dropdown |
xp-block-form-field__label | The field label |
xp-block-form-field__control | The control, or the group wrapping several |
xp-block-form-field__description | The description text |
xp-block-form-field__validation | The validation message |
is-xp-invalid | A control that failed validation |
A field can carry more than one type modifier. Dynamic Select renders
xp-block-form-field--dynamic-select alongside --dropdown or --checklist
depending on how it is configured, so style the generic one and let the specific
one override.
Popups
Popups does not use theme.json. It declares its properties on :root in its
own stylesheet, so you override them with ordinary CSS — anywhere that loads
after it.
:root {
--xpac-pp-width: 32rem;
--xpac-pp-overlay-bg: rgba(0, 0, 0, 0.6);
--xpac-pp-container-padd: 2rem;
}| Property | Default | What it sets |
|---|---|---|
--xpac-pp-width | var(--wp--style--global--content-size, 45rem) | Popup width |
--xpac-pp-zindex | 999999 | Popup stacking order |
--xpac-pp-overlay-zindex | 999998 | Overlay stacking order |
--xpac-pp-overlay-bg | 32% of the theme's contrast colour | Overlay colour |
--xpac-pp-space-x / -y | clamp(1rem, 4vw, 3rem) / clamp(1rem, 4vh, 3rem) | Gap between the popup and the viewport edge |
--xpac-pp-shadow | var(--wp--preset--shadow--natural, …) | Popup shadow |
--xpac-pp-container-bg | the theme's base colour | Popup background |
--xpac-pp-container-color | the theme's contrast colour | Popup text colour |
--xpac-pp-container-padd | 0 | Padding inside the popup |
--xpac-pp-close-top / -right | 0.75rem | Close button position |
--xpac-pp-close-width / -height | 2.75rem | Close button hit area |
--xpac-pp-close-icon-size | 1.125rem | The cross itself |
--xpac-pp-close-size | 2px | Stroke width of the cross |
--xpac-pp-close-color / -hover-color | 55% of currentColor / currentColor | Close button colour |
--xpac-pp-close-outline-color / -width / -offset | currentColor / 2px / 2px | Its focus ring |
The defaults read from the theme wherever there is a sensible token to read — content width, shadow preset, base and contrast colours — so a popup usually looks close to right before you set anything.
Several properties are re-declared per popup type. A floating bar zeroes the viewport gap and moves the close button to the vertical centre; an inline popup drops the shadow. Override inside the matching type class if you need a different value for one type only.
Structure and class names
A popup is built from its template element when it opens, not rendered with the page:
<div class="xpac-popup has-xpac-type-lightbox" data-parent="…">
<div class="xpac-popup-wrap">
<div class="xpac-popup-container" role="dialog" aria-modal="true" tabindex="-1">
<button class="xpac-popup-close"></button>
<div class="xpac-popup-inner"><!-- your blocks --></div>
</div>
</div>
</div>
<div class="xpac-popup-overlay" data-parent="…"></div>| Class | On |
|---|---|
xpac-popup-template | The source element in the page. Never visible |
xpac-popup | The generated popup |
xpac-popup--open | The same element, while it is open |
xpac-popup-overlay | The backdrop, a sibling rather than a child |
xpac-popup-container | The dialog. Carries the ARIA role and takes focus |
xpac-popup-inner | Where your blocks are moved to |
xpac-popup-close | The generated close button |
xpac-popup-cta-close | Any element of yours that should close the popup |
has-xpac-type-lightbox, has-xpac-type-floating, has-xpac-type-inlineloc | The popup, by type |
has-xpac-place-<position> | The popup, by configured position — top, bottom and so on |
has-xpac-pos-sticky | A floating bar configured to stick |
The overlay is a sibling of the popup rather than a parent, so it can be positioned independently. Do not write descendant selectors that assume it wraps anything.
The blocks inside a popup are moved into .xpac-popup-inner on open, and
moved back on close. A selector anchored to where the template sits in the
page will not match while the popup is open, and any JavaScript holding a
reference to a node inside it sees that node re-parented.