# Styling Elements

> Mode, tokens with a full accent ramp, Google Fonts, rules scoped to a published class list, preset bare, and restyling live.

Source: https://shopfa.st/docs/elements/styling

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`:

```js
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](https://shopfa.st/docs/elements/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
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:

```js
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:

```js
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 `::placeholder` on `.sf-el-field-input`. Several selectors may share a rule with commas. No element
  names, ids or combinators.
- **Properties** may be written `borderRadius` or `"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.
- **`!important`** is 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:

```js
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:

```text
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.

```js
brand: { preset: "bare", rules: { /* all yours */ } }
```

## Changing the brand later

```js
elements.update({ brand: { mode: "dark", tokens: { accent: "#0f766e" } } });
```

Restyles every element in place: no remount, no flash, and the buyer keeps what they typed.
