Front end

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.

theme.json
{
	"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.

Where a value can come from

Five layers can set the same token. Highest first:

  1. The Fields panel on one form's Form builder block. It saves the values in the block's fieldStyle attribute and prints them as custom properties in the builder's own style attribute, so they reach that form's fields only.
  2. The form editor's Styles sidebar, which writes into the global styles user layer. These values are site-wide, not per-form.
  3. settings.custom.xp-form in the active theme's theme.json.
  4. The plugin's shipped styles.json.
  5. 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 settingTokens it sets on the builder
Textinput-color, plus the hover, focus, error, error-focus, success and success-focus text colours
Backgroundinput-bg-color, plus the same six states
Borderinput-brd-color, input-hover-brd-color
Focus borderinput-focus-brd-color
Placeholderinput-placeholder-color
Labellabel-color
Descriptioninfo-color
Border widthinput-brd-width
Radiusinput-brd-radius
Label sizelabel-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:

  1. The xpac_forms_disable_default_styles filter, which XPAC → Forms → Settings → Appearance feeds at priority 1. true here 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.
  2. settings.custom.xp-form.defaultStyles in 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.
  3. 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.

ClassOn
xp-block-formThe <form> element
xp-is-loadingThe form, while a submission is in flight
xp-form__responseThe message wrapper, plus xp-form__response-success or -error
xp-block-form-fieldEvery field wrapper
xp-block-form-field--<type>The same wrapper, once per field type — --text, --email, --dropdown
xp-block-form-field__labelThe field label
xp-block-form-field__controlThe control, or the group wrapping several
xp-block-form-field__descriptionThe description text
xp-block-form-field__validationThe validation message
is-xp-invalidA 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;
}
PropertyDefaultWhat it sets
--xpac-pp-widthvar(--wp--style--global--content-size, 45rem)Popup width
--xpac-pp-zindex999999Popup stacking order
--xpac-pp-overlay-zindex999998Overlay stacking order
--xpac-pp-overlay-bg32% of the theme's contrast colourOverlay colour
--xpac-pp-space-x / -yclamp(1rem, 4vw, 3rem) / clamp(1rem, 4vh, 3rem)Gap between the popup and the viewport edge
--xpac-pp-shadowvar(--wp--preset--shadow--natural, …)Popup shadow
--xpac-pp-container-bgthe theme's base colourPopup background
--xpac-pp-container-colorthe theme's contrast colourPopup text colour
--xpac-pp-container-padd0Padding inside the popup
--xpac-pp-close-top / -right0.75remClose button position
--xpac-pp-close-width / -height2.75remClose button hit area
--xpac-pp-close-icon-size1.125remThe cross itself
--xpac-pp-close-size2pxStroke width of the cross
--xpac-pp-close-color / -hover-color55% of currentColor / currentColorClose button colour
--xpac-pp-close-outline-color / -width / -offsetcurrentColor / 2px / 2pxIts 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>
ClassOn
xpac-popup-templateThe source element in the page. Never visible
xpac-popupThe generated popup
xpac-popup--openThe same element, while it is open
xpac-popup-overlayThe backdrop, a sibling rather than a child
xpac-popup-containerThe dialog. Carries the ARIA role and takes focus
xpac-popup-innerWhere your blocks are moved to
xpac-popup-closeThe generated close button
xpac-popup-cta-closeAny element of yours that should close the popup
has-xpac-type-lightbox, has-xpac-type-floating, has-xpac-type-inlinelocThe popup, by type
has-xpac-place-<position>The popup, by configured position — top, bottom and so on
has-xpac-pos-stickyA 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.

On this page