Skip to content

Styling and theming

OctaForms ships a deliberately neutral stylesheet so a form looks tidy the moment you add it. This page shows how to make forms and the callback widget match your theme, from a one-line colour change to a full custom look.

There are two ways to style, and you will usually mix them:

  1. Set a few CSS variables. The fastest path. Change colours, borders, radius and spacing without writing a single override rule.
  2. Write your own CSS rules. For layout or anything the variables do not cover. The plugin is built to make this easy and hard to break, as long as you follow two small rules below.

Every default rule is wrapped in :where(), which means it carries zero specificity. The styles still apply, but any selector you write beats them. A plain class is enough:

.my-form-input { border: 2px solid #2563eb; }

No need to repeat .octa-forms__field input or stack extra classes to win. There are two deliberate exceptions. The submit button keeps real specificity so a bare button {} reset in your theme cannot wash it out to a blank control. More on that below. The frame of a section card and of a radio or checkbox group (both are a <fieldset>) carries one class of specificity, so a theme rule on the bare fieldset element does not give every group its own border and spacing. Any rule of yours with a class still wins.

OctaForms renders the same markup everywhere (the shortcode, the helper, and the widget popup), so one set of styles covers all of them.

<form class="octa-forms" data-octa-forms="{slug}">
<div class="octa-forms__field octa-forms__field--text octa-forms__field--required" data-octa-forms-field="name">
<label for="…">Name<span class="octa-forms__required" aria-hidden="true">*</span></label>
<input type="text" … required>
</div>
<fieldset class="octa-forms__field octa-forms__field--radio" data-octa-forms-field="business">
<legend>Do you run a business?</legend>
<div class="octa-forms__options">
<label class="octa-forms__option"><input type="radio" …><span>Yes</span></label>
<label class="octa-forms__option"><input type="radio" …><span>No</span></label>
</div>
</fieldset>
<div class="octa-forms__field octa-forms__field--checkbox">
<label>
<input type="checkbox" value="on" …>
<span class="octa-forms__consent">I agree to the privacy policy</span>
</label>
</div>
<div class="octa-forms__actions">
<button type="submit" class="octa-forms__submit">Send</button>
</div>
</form>

Each field is a .octa-forms__field wrapper with a type modifier: --text, --email, --tel, --number, --date, --select, --radio, --textarea, --file, --checkbox, --checkboxes. A required field adds --required and a star in .octa-forms__required. Every wrapper carries data-octa-forms-field with the field’s name, which is the hook to target one field.

Three layouts matter when you write CSS:

  • Regular fields (text, email, tel, number, date, select, textarea, file) put the <label> above the control. The wrapper is a vertical flex column.
  • The checkbox is different. Its <label> wraps the box and the consent text side by side. The <input type="checkbox"> is a small native box, not a full-width control. This distinction is the single most common source of styling bugs. See Do not style bare input.
  • Choice groups (radio, checkboxes) are a <fieldset> whose <legend> is the label, with one .octa-forms__option line per option.

A form with sections wraps each group of fields in a card:

<fieldset class="octa-forms__section" data-octa-forms-section="1">
<legend class="octa-forms__section-title">Contact details</legend>
<p class="octa-forms__section-text">How we can reach you.</p>
<div class="octa-forms__section-fields">…fields…</div>
<p class="octa-forms__section-note">We reply within a day.</p>
</fieldset>

.octa-forms__section-fields is where to lay fields out in columns. OctaForms keeps one column on purpose, so the layout stays your theme’s call:

@media (min-width: 768px) {
.octa-forms__section-fields {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
/* Long answers and choice groups take the full width. */
.octa-forms__section-fields > :is(.octa-forms__field--textarea, .octa-forms__field--checkboxes, .octa-forms__field--radio) {
grid-column: 1 / -1;
}
}

A form shown as steps adds a step bar inside .octa-forms__actions: .octa-forms__steps holds .octa-forms__step-progress (“Step 2 of 5”), .octa-forms__step-back and .octa-forms__step-next. Hidden steps, hidden conditional fields and the step buttons use the hidden attribute, and the stylesheet makes .octa-forms [hidden] always win, even over a theme’s own display rule.

For the most common changes you do not need an override rule. Set a few values on .octa-forms:

.octa-forms {
--octa-forms-submit-bg: #2563eb;
--octa-forms-submit-bg-hover: #1d4ed8;
--octa-forms-control-radius: 12px;
--octa-forms-control-border: 1px solid #cbd5e1;
--octa-forms-focus-color: #2563eb;
}

Because you set them on the container, you can scope different looks to different places:

.site-footer .octa-forms { --octa-forms-submit-bg: #fff; --octa-forms-submit-color: #111; }
Variable Controls Default
--octa-forms-gap Space between fields 1rem
--octa-forms-max-width Max width of the form 640px
--octa-forms-label-size Label font size 0.9375rem
--octa-forms-label-weight Label font weight 600
--octa-forms-control-bg Input / select / textarea background #fff
--octa-forms-control-color Control text colour inherit
--octa-forms-placeholder-color Placeholder text colour #b3b3bb
--octa-forms-control-padding Control padding 0.55rem 0.75rem
--octa-forms-control-height Minimum height of controls and buttons 2.75rem
--octa-forms-control-border Control border 1px solid #c7c7cf
--octa-forms-control-border-hover Control border colour on hover #8e8e99
--octa-forms-control-radius Control corner radius 6px
--octa-forms-control-disabled-bg Background of a disabled control and of the file button #f4f4f6
--octa-forms-focus-color Focus border and outline #111
--octa-forms-accent Colour of ticked checkboxes and radio buttons #111
--octa-forms-label-gap Space between a label and its control 0.375rem
--octa-forms-options-direction column for one option per line, row to line short options up side by side column
--octa-forms-options-gap Space between options 0.5rem 1.5rem
--octa-forms-muted Secondary text: section text and notes, the thank-you text #5f5f6b
--octa-forms-submit-bg Submit button background #111
--octa-forms-submit-bg-hover Submit button hover background #000
--octa-forms-submit-color Submit button text colour #fff
--octa-forms-submit-padding Submit button padding 0.6rem 1.4rem
--octa-forms-submit-radius Submit button corner radius 6px
--octa-forms-error-color Validation error colour #b91c1c
--octa-forms-error-bg Background of the error box at the top of the form #fef2f2
--octa-forms-required-color The required star #b91c1c
--octa-forms-section-bg Section card background #fff
--octa-forms-section-border Section card border 1px solid #e5e5ea
--octa-forms-section-radius Section card corner radius 8px
--octa-forms-section-padding Section card padding 1.5rem
--octa-forms-section-title-size Section title font size 1.25rem
--octa-forms-section-title-border Line under a section title (0 to remove it) 1px solid #ececf0
--octa-forms-section-text-size Section text font size 1rem
--octa-forms-section-muted Section text, note and step progress colour #5f5f6b
--octa-forms-thank-you-bg Thank-you panel background #f6f7f9
--octa-forms-thank-you-border Thank-you panel border #e5e5ea

When the variables are not enough, write normal CSS. Two rules keep you out of trouble.

A checkbox is an <input> too. If you style every input in a field, you also blow up the checkbox into a full-width, padded, bordered box. It then looks oversized or centred and pushes the layout around. This is the most common styling bug.

/* Wrong — this also hits the checkbox and radio */
.octa-forms__field input { width: 100%; padding: 12px; border: 1px solid #ccc; }

Always exclude checkbox and radio from generic input styling:

/* Right */
.octa-forms__field input:not([type="checkbox"]):not([type="radio"]),
.octa-forms__field select,
.octa-forms__field textarea {
width: 100%;
padding: 12px;
border: 1px solid #ccc;
border-radius: 8px;
}

Apply the same :not(...) to any :focus, background or border rule that targets input. Style the checkbox only through its own selectors:

.octa-forms__field--checkbox input[type="checkbox"] { width: 1.1em; height: 1.1em; accent-color: #2563eb; }
.octa-forms__field--checkbox label { gap: 0.5rem; align-items: flex-start; }
.octa-forms__consent { font-size: 0.9rem; line-height: 1.45; }
.octa-forms__consent a { text-decoration: underline; }

The submit button needs a class, not bare button

Section titled “The submit button needs a class, not bare button”

.octa-forms__submit keeps real specificity on purpose, so a theme reset like button { background: none } cannot strip it. To restyle the button with your own rule, use a selector that includes the class, or set the --octa-forms-submit-* variables:

.octa-forms__submit { border-radius: 999px; }
Class What it is
.octa-forms The <form> element. Set your variables here.
.octa-forms__field A single field wrapper (vertical flex column).
.octa-forms__field--{type} Type modifier: text, email, tel, date, select, textarea, file, checkbox.
.octa-forms__field label A field label.
.octa-forms__label--sr A label hidden visually but kept for screen readers (placeholder-labels mode). Leave it hidden.
.octa-forms__field--checkbox label The checkbox row (box plus consent text).
.octa-forms__consent The consent text next to a checkbox.
.octa-forms__consent a A link inside consent text.
.octa-forms__actions The submit button wrapper.
.octa-forms__submit The submit button (keeps real specificity).
.octa-forms-error / .octa-forms-error--form A field error / a form-level error (added by the helper).
.octa-forms__thank-you / -title / -text The thank-you screen that replaces the form after a successful submit.
.octa-forms__noscript The message shown when JavaScript is off.

The focus ring and the invalid-field and error colours keep their normal weight on purpose, so a light theme override does not accidentally remove those accessibility signals.

The floating handset button and its popup are neutral by default (a black button and handset). They read their colours, radius and spacing from --cb-* variables scoped to .octa-forms-callback. Set them in your theme to retint the whole widget.

--cb-primary is the widget’s brand colour. Setting it alone retints, in one line:

  • the floating button and its pulsing rings,
  • the button and tab focus outlines,
  • the active tab colour and its underline,
  • the input focus ring inside the popup,
  • the popup’s submit button.
.octa-forms-callback {
--cb-primary: #e11d2a; /* your brand colour */
}

That is the important one to know: the popup’s send button takes its colour from --cb-primary, not from --octa-forms-submit-bg. Inside the popup the widget’s own styles win. To change the popup button, set --cb-primary (or override .octa-forms-callback__popup .octa-forms__submit).

Variable Controls Default
--cb-primary Brand colour: button, rings, focus, active tab, popup focus and submit #111
--cb-white Button icon, popup surfaces var(--color-white, #fff)
--cb-black Popup text var(--color-black, #444)
--cb-gray-dark Muted text and placeholders var(--color-gray-dark, #777)
--cb-gray-medium Borders and tab separators var(--color-gray-medium, #d9dee6)
--cb-gray-light Light fills var(--color-gray-light, #f7f9fc)
--cb-radius Corner radius across the widget var(--radius, 10px)
--cb-space-* Spacing scale (xs…xxl) Falls back to --spacing-* theme tokens
--cb-fs-* Font-size scale (small…huge) Falls back to --font-size-* theme tokens

Because spacing and type fall back to your theme tokens (--spacing-*, --font-size-*), the widget often adopts your theme’s rhythm with no extra work. Override the --cb-* values only where you want to differ.

A worked example, retinting to a brand and rounding the corners:

.octa-forms-callback {
--cb-primary: #e11d2a;
--cb-radius: 16px;
--cb-gray-medium: #dfe3ea;
}

If you need finer control than the variables give, these are the pieces you will style most often.

Class What it is
.octa-forms-callback The widget host (fixed position). Set --cb-* here.
.octa-forms-callback--left / --right Which side the button sits on.
.octa-forms-callback__toggle The round floating button.
.octa-forms-callback__icon The handset icon (a CSS mask, coloured with currentColor).
.octa-forms-callback__bubble The small teaser bubble by the button.
.octa-forms-callback__popup The open popup wrapper.
.octa-forms-callback__dialog The popup panel.
.octa-forms-callback__tabs / __tab The contact-method tabs (phone, callback, message).
.octa-forms-callback__tab[aria-selected="true"] The active tab.
.octa-forms-callback__phone / __tel The phone number shown in phone mode.

The form inside the popup is the same .octa-forms markup described above, so everything in the sections before this one applies there too. Its layout rules are zero specificity, so your theme still wins.

If you would rather style everything yourself from scratch, turn the form stylesheet off with the disable_styles setting or the octa_forms_styles_enabled filter. When it is off the --octa-forms-* variables and the base layout go away too, so your theme has to supply the full layout, including the checkbox row. See Frontend assets.

  • No rule targets .octa-forms__field input without :not([type="checkbox"]):not([type="radio"]).
  • The checkbox renders as a small native box on the left, with the consent text wrapping beside it and no gap pushing the button around.
  • The submit button is styled through .octa-forms__submit or the --octa-forms-submit-* variables, not bare button.
  • The widget is retinted with --cb-primary (remember the popup send button follows it, not --octa-forms-submit-bg).
  • Focus, error and thank-you states still stand out against your theme.
  • .octa-forms__label--sr stays visually hidden.
  • Checked in every place a form appears (page sections, the widget popup), since different contexts can carry different theme styles.