Styling Elements
Mode, tokens with a full accent ramp, Google Fonts, rules scoped to a published class list, preset bare, and restyling live.
Elements paint no background of their own: they take whatever is behind them, so a checkout in a white card looks
like part of that card. Everything else comes from brand:
const elements = shopfast.elements({
items,
brand: {
mode: "light",
fonts: ["Fraunces:wght@500;700", "Inter:wght@400;500;600"],
tokens: { accent: "#b2452a", text: "#221f1c", border: "#ddd5c9", radius: "4px", font: "Inter, system-ui, sans-serif", fontHeading: "Fraunces, Georgia, serif" },
rules: {
".sf-el-pay": { borderRadius: "999px", letterSpacing: "0.02em" },
".sf-el-method[data-selected]": { backgroundColor: "#fbf3f0" },
},
},
});
Try every token, font and rule live in the playground: it shows the code as you go.
Mode
mode is "light", "dark" or "auto" (the default, which follows the buyer’s system setting). Set it when your page
is always light or always dark, or a dark-mode visitor gets a dark checkout on your white page.
Tokens
Tokens are the broad strokes. Each one becomes a CSS variable every element reads.
| Token | Variable | What it styles |
|---|---|---|
accent |
--sf-el-accent |
Buttons, the selected method, focus rings and links. The whole ramp comes from it. |
onAccent |
--sf-el-on-accent |
Text on the accent. Picked for contrast when you leave it out. |
text |
--sf-el-text |
Body text, labels and amounts. |
textMuted |
--sf-el-text-muted |
Hints, captions and secondary text. |
border |
--sf-el-border |
Field borders, dividers and method cards. |
surface |
--sf-el-surface |
Behind each element. Transparent unless you set it. |
surfaceSunken |
--sf-el-surface-sunken |
Quiet panels: the test card, the totals box, chips. |
input |
--sf-el-input |
Inside fields and selects. |
danger |
--sf-el-danger |
Errors and invalid fields. |
success |
--sf-el-success |
Savings, applied codes and the paid state. |
warning |
--sf-el-warning |
Test mode and notices. |
radius |
--sf-el-radius |
Corner radius of fields, buttons and cards (0 to 32px). |
gap |
--sf-el-gap |
Space between rows and sections (4 to 40px). |
fontSize |
--sf-el-font-size |
Base text size (12 to 20px). |
font |
--sf-el-font |
Body font stack. |
fontHeading |
--sf-el-font-heading |
Headings and the total. Defaults to font. |
fontMono |
--sf-el-font-mono |
Codes and the test card. |
Colors take hex, rgb(), hsl() or oklch(). Lengths take px or rem. A value that does not fit is refused with a
trace, and the default stays.
The accent ramp
Set accent and elements build eleven steps from it (--sf-el-accent-50 to --sf-el-accent-950) in OKLCH, so hover
states, the selected method’s tint, focus rings and links all come from your color rather than a generic blue. The
step closest to your accent is your accent exactly; the others keep its hue and are mapped into what screens can show.
Contrast
Elements check text against your colors with the WCAG 2 formula. Leave out onAccent and they pick white or near
black, whichever reads better on your accent. When text on the accent, or text on a solid surface, falls under
4.5:1 (WCAG AA), you get a trace with the ratio:
Text on the accent is 1.98:1, below WCAG AA (4.5:1). Pick a darker or lighter accent.
Fonts
An element is its own document, so the fonts your page loads do not reach it. Load them into the elements:
brand: {
fonts: [
"Inter:wght@400;500;600",
{ family: "Fraunces", weights: [500, 700], italic: true },
"https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@700",
],
tokens: { font: "Inter, system-ui, sans-serif" },
}
Google Fonts only, up to four families, and the address is rebuilt from the family names and weights rather than
used as you pass it: any other stylesheet would reach a payment page without going through the rules below. Then name
the family in font, fontHeading or fontMono.
Rules
rules is CSS for the published class names, and only those:
rules: {
".sf-el-button": { borderRadius: "2px" },
".sf-el-field-input:focus-visible": { boxShadow: "0 0 0 3px rgb(178 69 42 / .25)" },
".sf-el-method[data-selected]": { borderColor: "#b2452a", backgroundColor: "#fbf3f0" },
".sf-el-total-amount": { fontFamily: "Fraunces, Georgia, serif" },
}
- Selectors are one class from the list below, optionally with one state,
[data-selected],[data-disabled],[data-loading],[data-invalid],[data-locked],[data-open],[data-empty],[data-applied],[data-checked]or[data-test], and one of:hover,:focus,:focus-visible,:focus-within,:active,:disabled,:checked, or::placeholderon.sf-el-field-input. Several selectors may share a rule with commas. No element names, ids or combinators. - Properties may be written
borderRadiusor"border-radius". Numbers get px where CSS needs a unit. - Hover rules only apply on devices with a mouse, so phones never get a stuck hover.
!importantis removed. Your rules already win over the defaults.var()may read the elements’ own tokens (var(--sf-el-accent)), nothing else.
What is refused
A checkout that can be restyled freely can be made to lie about what it charges: hide the total, cover the pay button, dress a decline up as a success. So these are refused everywhere:
| Refused | Why |
|---|---|
position, inset, top, right, bottom, left, float |
Can move parts out of place |
z-index |
Can stack one part over another |
content, quotes, counters |
Can add text that is not the checkout’s own |
pointer-events |
Can make buttons ignore or pass on clicks |
transform, translate, rotate, scale, zoom |
Can move, flip or shrink parts |
clip, clip-path, mask and -webkit-mask |
Can cut parts out of view |
filter, backdrop-filter, mix-blend-mode |
Can hide or recolor parts |
overflow |
Can cut off the total or the pay button |
direction, unicode-bidi, writing-mode |
Can reorder the characters of a price |
text-indent, text-overflow, line clamps |
Can push text out of view |
animation, all, appearance, behavior, -moz-binding |
Run code or reset every protection at once |
Variables (--x: …) |
Use tokens instead |
Any value with url(), image(), image-set(), expression(), attr(), env(), @, \, comments, ; or braces |
Fetches something, runs code or smuggles in more CSS |
| Lengths under −32px or over 4000px | Pull parts out of the frame |
Totals, the pay button, prices and the legal texts are protected (marked in the class list). They take colors,
borders, radius, padding, shadows and type, and nothing that could hide them: no opacity, visibility, display,
size or margins, text no smaller than 12px, solid colors only, and text at 3:1 contrast or better against its
background.
Every refusal is a trace, never a silent drop:
elements.on("trace", (e) => console.warn(e.message));
// Rule ".foo" refused: .foo is not a ShopFast Elements class.
// .sf-el-pay { position } refused: position can move parts of the checkout out of place.
The checks after your rules
After your rules apply, each element checks its protected parts in the browser: still visible, inside the element, not see-through, at least 10px and readable against their background. If one fails, that element turns your rules off and says why:
Rules turned off in the payButton element: .sf-el-pay cannot be read against its background with them.
The class names
These are a promise: once a class is on this list it keeps existing and keeps meaning the same thing, even when what is behind it is rewritten. New classes are only ever added.
| Group | Class | Protected | What it is |
|---|---|---|---|
| Roots | .sf-el-checkout |
The all-in-one checkout element. | |
| Roots | .sf-el-summary |
The cart summary element. | |
| Roots | .sf-el-email |
The email element. | |
| Roots | .sf-el-payment |
The payment method element. | |
| Roots | .sf-el-coupon |
The coupon and gift card element. | |
| Roots | .sf-el-consent |
The terms, marketing and withdrawal element. | |
| Roots | .sf-el-billing |
The billing details element (billingAddress). | |
| Roots | .sf-el-custom-fields |
The custom fields element. | |
| Roots | .sf-el-product |
The product element. | |
| Roots | .sf-el-buy-button |
The buy button element. | |
| Roots | .sf-el-cart-button |
The cart button element. | |
| Roots | .sf-el-pay-button |
The pay button element. | |
| Roots | .sf-el-tip |
The tip element. | |
| Roots | .sf-el-offers |
The add-ons and upsells element. | |
| Layout | .sf-el-section |
A titled block inside an element (contact, details, payment). | |
| Layout | .sf-el-heading |
Section and element headings. | |
| Layout | .sf-el-subheading |
Small text next to or under a heading. | |
| Layout | .sf-el-divider |
A hairline between blocks. | |
| Layout | .sf-el-grid |
Two or three fields side by side. | |
| Fields | .sf-el-field |
A label, its input and its hint or error. | |
| Fields | .sf-el-field-label |
A field label. | |
| Fields | .sf-el-field-input |
Text inputs and text areas. | |
| Fields | .sf-el-field-select |
Selects (country, custom field choices). | |
| Fields | .sf-el-field-hint |
Help text under a field. | |
| Fields | .sf-el-field-error |
The error under a field. | |
| Fields | .sf-el-field-optional |
The “Optional” mark next to a label. | |
| Fields | .sf-el-check |
A checkbox row (marketing, custom checkboxes). | |
| Fields | .sf-el-check-box |
The checkbox itself. | |
| Fields | .sf-el-check-label |
The text next to a checkbox. | |
| Fields | .sf-el-code-row |
The code input and its Apply button. | |
| Fields | .sf-el-verify |
The email confirmation code box. | |
| Buttons | .sf-el-button |
Every button. | |
| Buttons | .sf-el-button-primary |
Primary buttons (Add to cart, Apply). | |
| Buttons | .sf-el-button-secondary |
Secondary buttons. | |
| Buttons | .sf-el-button-ghost |
Quiet text buttons (Remove, Have a code?). | |
| Buttons | .sf-el-pay |
Yes | The pay button, wherever it is. |
| Buttons | .sf-el-link |
Text links. | |
| Buttons | .sf-el-chip |
Small toggles (tip amounts, test outcomes). | |
| Cart | .sf-el-lines |
The list of cart lines. | |
| Cart | .sf-el-line |
Yes | One cart line. |
| Cart | .sf-el-line-image |
A line’s cover or picture. | |
| Cart | .sf-el-line-name |
Yes | A line’s product name. |
| Cart | .sf-el-line-option |
Yes | A line’s option (“30 Days”, “Commercial license”). |
| Cart | .sf-el-line-qty |
Yes | The quantity stepper or badge. |
| Cart | .sf-el-line-price |
Yes | A line’s price. |
| Cart | .sf-el-line-notice |
A line’s notice (“Only 3 left”). | |
| Cart | .sf-el-qty-button |
The + and − buttons. | |
| Cart | .sf-el-empty |
The empty cart message. | |
| Totals | .sf-el-totals |
Yes | The totals box. |
| Totals | .sf-el-row |
Yes | One row of the totals (Subtotal, Tax). |
| Totals | .sf-el-row-label |
Yes | A totals row label. |
| Totals | .sf-el-row-amount |
Yes | A totals row amount. |
| Totals | .sf-el-discount |
Yes | Discount and gift card rows. |
| Totals | .sf-el-total |
Yes | The total row. |
| Totals | .sf-el-total-label |
Yes | The word “Total”. |
| Totals | .sf-el-total-amount |
Yes | The amount the buyer pays. |
| Totals | .sf-el-applied-code |
An applied discount or gift card chip. | |
| Payment | .sf-el-method-list |
The list of ways to pay. | |
| Payment | .sf-el-method |
One way to pay. | |
| Payment | .sf-el-method-icon |
A method’s icon. | |
| Payment | .sf-el-method-label |
A method’s name. | |
| Payment | .sf-el-method-note |
Yes | What happens next (“You finish on PayPal”) or a price change. |
| Payment | .sf-el-method-panel |
What opens under the selected method. | |
| Payment | .sf-el-test-card |
The test card shown in test mode. | |
| Payment | .sf-el-test-badge |
Yes | The “Test mode” badge. |
| Payment | .sf-el-outcome |
A simulated outcome chip in test mode. | |
| Payment | .sf-el-limit-note |
Why a way to pay is not offered for this total. | |
| Offers | .sf-el-offer |
One add-on or upsell. | |
| Offers | .sf-el-offer-title |
An offer’s title. | |
| Offers | .sf-el-offer-price |
Yes | An offer’s price. |
| Offers | .sf-el-progress |
The reward progress bar (“$5 to free bonus”). | |
| Offers | .sf-el-progress-bar |
The filled part of the progress bar. | |
| Product | .sf-el-product-cover |
The product picture. | |
| Product | .sf-el-product-title |
The product name. | |
| Product | .sf-el-product-description |
The short description. | |
| Product | .sf-el-product-price |
Yes | The product price. |
| Product | .sf-el-product-compare |
The crossed-out regular price. | |
| Product | .sf-el-variant |
One option in the option picker. | |
| Product | .sf-el-tier |
One quantity price (“5+ · 10% off”). | |
| Product | .sf-el-badge |
Small labels (Sale, Instant delivery). | |
| Product | .sf-el-count |
The item count on the cart button. | |
| Legal | .sf-el-consent-terms |
Yes | The terms checkbox and its text. |
| Legal | .sf-el-waiver |
Yes | The EU and UK immediate delivery box. |
| Legal | .sf-el-seller |
Yes | Who sells (“Sold by …”). |
| Legal | .sf-el-trust |
The secure checkout note and badges. | |
| Feedback | .sf-el-alert |
A message box. | |
| Feedback | .sf-el-alert-title |
A message box title. | |
| Feedback | .sf-el-alert-text |
A message box text. | |
| Feedback | .sf-el-spinner |
The loading spinner. | |
| Feedback | .sf-el-skeleton |
Placeholder bars while an element loads. | |
| Feedback | .sf-el-done |
The paid state. | |
| Overlay | .sf-el-overlay |
The backdrop of the overlay checkout (buy and cart buttons). | |
| Overlay | .sf-el-overlay-panel |
The overlay’s panel. | |
| Overlay | .sf-el-overlay-close |
The overlay’s close button. |
Starting from nothing
preset: "bare" strips the elements down to structure: no borders, no backgrounds, no radius, no padding. Layout,
type and focus rings stay, so it is still usable while you build your own look with rules.
brand: { preset: "bare", rules: { /* all yours */ } }
Changing the brand later
elements.update({ brand: { mode: "dark", tokens: { accent: "#0f766e" } } });
Restyles every element in place: no remount, no flash, and the buyer keeps what they typed.