# "CSS: states, skeletons and variables"

> What Smootify's stylesheet does, the state classes and page flags you can style, the skeleton placeholders and their variables, popover backdrops, upload classes and the CSS variables Smootify writes.

Status: Ready (Smootify 2.0)  
1.x page: https://docs.smootify.io/docs/custom-code/css  
URL: https://www.smootify.io/docs/developers/css  
Updated: 2026-10-10

Smootify’s stylesheet is small and leaves your design alone. Most of what you style comes from Smootify in two forms: classes it adds to elements as their state changes, and CSS variables it writes or reads. This page lists what you can use, for combo classes in the Designer or for your own CSS.

## The stylesheet

The loader adds the stylesheet when its script tag has `data-css="true"`: see [Install Smootify 2.0](https://www.smootify.io/docs/start/install). It holds:

- **the `condition` rules**: every `[condition="…"]`, `[customer-condition="…"]` or `[cart-condition="…"]` element is hidden with `display: none !important` while its state does not match. Without the stylesheet, these elements always show;
- **the skeleton placeholders** for elements with a `skeleton` attribute (below);
- **a few defaults**: closed popovers are hidden, popovers and the cart’s dialog lose the browser’s padding and border (your classes still set theirs), and the cart’s dialog, the market suggestion and the `store-availability` dialog get a backdrop.

Your own CSS goes in a `<style>` tag in Site settings, Custom code, or in a Code Embed. In the Designer, the same classes work as combo classes.

## State classes

Smootify adds and removes classes as things change. The main ones:

- **On `smootify-product`**: `loaded`, `is-available` / `is-not-available`, `is-on-sale`, `is-free`, `is-new`, `is-variable`, `price-varies`, `has-subscription`, `not-existing-variant`, `is-currently-out-of-stock`, and the stock levels `many-in-stock`, `low-stock`, `low-not-last-stock`, `last-in-stock`.
- **On the values of a picker**: `is-active` on the selected value, `is-disabled` on a value that cannot be bought (it stays clickable).
- **On `smootify-cart`**: `loaded`, `is-rendered`, `is-empty-cart`, `has-taxes`, `has-empty-quote`.
- **On a product that Shopify does not return**: Webflow’s `w-dyn-bind-empty`, on the Collection item or on `smootify-product`.

```css
smootify-product.is-on-sale .product-price {
  color: #b42318;
}
variant-swatches button.is-active {
  outline: 2px solid currentColor;
}
variant-swatches button.is-disabled {
  opacity: 0.4;
}
```

Every element’s classes are in the Reference: [state classes](https://www.smootify.io/docs/reference/state-classes).

### `loaded`

`loaded` goes on an element once Smootify has written it: products, prices, the cart, the account lists, sort and filter widgets, images and more. Use it for an entrance transition:

```css
smootify-product .product-info {
  opacity: 0;
  transition: opacity 0.3s;
}
smootify-product.loaded .product-info {
  opacity: 1;
}
```

## Page flags

Some states apply to the whole page, on `html` or `body`:

- **`html[logged-in]`** and **`body.is-user-logged-in`** while a customer is logged in;
- **`html[customer-status="loaded"]`** and **`body.customer-status-loaded`** once the session is known, logged in or not;
- **`body.has-customer-names`**, **`has-addresses`** / **`no-addresses`**, **`has-orders`** / **`no-orders`**, **`has-subscriptions`** / **`no-subscriptions`** for the account pages;
- **`body.has-wishlist`**, **`body.has-last-viewed`**, **`body.has-cart-upsells`** while those lists have products;
- **`has-related`**, **`has-complementary`**, **`has-best-seller`** and the other `has-<source>` classes on `html` and `body`, while a `smootify-product` list with that `data-id` has products;
- **`body.cookie-banner-visible`** while the `smootify-consent` banner shows.

```css
html:not([logged-in]) .account-menu {
  display: none;
}
```

## Skeletons

An element with a `skeleton` attribute is a placeholder until Smootify writes it. Smootify then removes the attribute, also when the value is missing and the element is hidden. Only elements with the attribute are placeholders.

- `skeleton="text"`, `"button"` or `"block"`: a pulsing background, transparent text and border, children invisible, not clickable;
- `skeleton="image"`, on an Image: a pulsing blur;
- `skeleton="hide"`: hidden (`visibility: hidden`), so it keeps its space.

Their colours and corner radius come from three [CSS variables](#css-variables).

Customer values (`[customer]`, `[customer-condition]`, `[company]` and the customer metafields) pulse the same way until `html[customer-status="loaded"]`, without a `skeleton` attribute. How designers add skeletons is in [loading](https://www.smootify.io/docs/reference/attributes/loading).

## Popovers and backdrops

The mini cart’s dialog, the market suggestion and the `store-availability` dialog get a backdrop, coloured by two [CSS variables](#css-variables). The market suggestion and the `store-availability` dialog also blur the page behind, through the class `with-blur`, unless the element has `disable-blur`. Other popovers, Webflow’s Popover element included, have no backdrop: set `--smootify-backdrop-color` on a panel’s class to give it one. The popovers inside that panel do not get it.

A closed popover is hidden with `display: var(--smootify-closed-display, none) !important`, so style its open state with `:popover-open`. To show a closed popover in the page at some breakpoints, set `--smootify-closed-display` on its class there. More in [Popovers](https://www.smootify.io/docs/build/popovers) and [Market suggestion](https://www.smootify.io/docs/features/market-suggestion).

## CSS variables

Five variables change the look of skeletons and backdrops. Smootify writes the other four, for your CSS to read:

Variables you set:

| Variable | Default | What it sets |
|---|---|---|
| `--skeleton-loading-start` | `hsl(200, 20%, 80%)` | the first color of the pulse |
| `--skeleton-loading-end` | `hsl(200, 20%, 95%)` | the second color of the pulse |
| `--skeleton-radius` | `4px` | the corner radius of the placeholder |
| `--smootify-backdrop-color` | none on popovers, `rgba(0, 0, 0, 0.8)` on the mini cart's dialog | the backdrop of a popover, set on the panel's class (popovers inside it do not get it), and of the mini cart's dialog |
| `--smootify-closed-display` | `none` | how a closed popover shows, set on the panel's class: `flex` at a breakpoint shows it in the page there |
| `--backdrop-color` | `rgba(0, 0, 0, 0.1)` | the backdrop of the market suggestion and of the `store-availability` dialog |

Variables Smootify writes:

| Variable | Written on | Value |
|---|---|---|
| `--metafield-rating` | a `rating` or `list.rating` metafield element | its share of the scale, as a percentage (`90%`) |
| `--metafield-color` | a color metafield element | the color |
| `--upload-progress` | each upload item and its progress element | from `0` to `1` while the file uploads |
| `--scrollbar-width` | `html` | the width of the page's scrollbar, measured at load |

A `property="--my-name"` attribute on a metafield element writes the rating or the colour into that variable instead. See [Metafields on the page](https://www.smootify.io/docs/build/metafields).

A fill inside a rating element with `width: var(--metafield-rating)` draws stars, and an upload’s progress element can scale with `--upload-progress`:

```css
:root {
  --skeleton-loading-start: #e4e4e7;
  --skeleton-loading-end: #f4f4f5;
  --skeleton-radius: 8px;
}
.filter-panel {
  --smootify-backdrop-color: rgba(15, 23, 42, 0.6);
}
.stars-fill {
  width: var(--metafield-rating);
}
[upload="progress"] {
  transform: scaleX(var(--upload-progress, 0));
  transform-origin: left;
}
```

## Upload classes

Upload fields (`file-uploader`, `file-input`, `configurator-file-input`, server plan) use your markup and add classes you style:

- on the element: `is-dragover` while a file is dragged over, `is-uploading`, `has-files`, `is-full` when no more files fit, `is-invalid` when a file was refused;
- on each item: `is-uploading`, `is-done`, `is-error`, `is-image`.

The parts are in [upload](https://www.smootify.io/docs/reference/attributes/upload) and [File uploads](https://www.smootify.io/docs/features/file-uploads).

## Snippets

Short rules for things the Designer cannot do alone. Paste them in **Site settings › Custom code › Head code**, inside a `<style>` tag, and change the styles to fit your design.

**A tooltip on swatch buttons.** Buttons that show only a colour or an image can show the value’s name on hover: every `variant-swatches` button carries its value in `data-value`.

```css
variant-swatches button { position: relative; }
variant-swatches button:hover::before {
  content: attr(data-value);
  position: absolute;
  bottom: calc(100% + 0.4rem);
  left: 50%;
  transform: translateX(-50%);
  z-index: 10;
  padding: 0.2rem 0.5rem;
  border-radius: 0.3rem;
  background: black;
  color: white;
  font-size: 0.8rem;
  white-space: nowrap;
}
```

**No shimmer on a cart line that changes.** While its quantity changes or it is removed, a line gets the class `loading` and a pulsing layer over it. To keep the line still (it still ignores clicks until the change is done):

```css
cart-item.loading::before,
cart-item.loading bundle-item > *::before { display: none !important; }
```

**A checked Radio button or Checkbox.** Webflow styles the checked state of the input only. To style the whole Radio button or Checkbox, label included, when it is checked (`.w--redirected-checked` is the class Webflow gives a custom-styled input):

```css
.w-radio:has(.w--redirected-checked), .w-radio:has(input:checked) { /* your styles */ }
.w-checkbox:has(.w--redirected-checked), .w-checkbox:has(input:checked) { /* your styles */ }
```

**The page while the Navbar menu is open.** Webflow styles the open state of the menu only. To change the page, for example to stop it from scrolling:

```css
body:has([data-nav-menu-open]) { overflow: hidden; }
```

## Where people get stuck

> [!STUCK]
> - **Elements with a condition always show.** The stylesheet is not loaded: add `data-css="true"` to the loader, or link the stylesheet.
> - **Nothing pulses while the page loads.** In 2.0 only elements with a `skeleton` attribute are placeholders. Add it where you want one.
> - **Upload styles stopped working.** 1.x used FilePond and its `.filepond--*` classes. 2.0 uses your markup: style the upload parts and classes above.
> - **Styles for the cookie banner no longer apply.** They target `is-shopify-cookie-banner-visible`, the 1.x class. In 2.0 the body class is `cookie-banner-visible`.

## Coming from 1.x

- **No automatic skeleton.** 1.x animated every text, image, button and link of an element that had not loaded yet. 2.0 animates only elements with a `skeleton` attribute. Adding the `loaded` class to switch the skeleton off is no longer needed.
- **Search, search and discovery, account lists and store availability** no longer pulse on their own either: add `skeleton` to the placeholders you want.
- **The state classes and the `condition` rules are unchanged.** `smootify-variant` now gets `is-on-sale` only when the compare-at price is above the price, as `smootify-product` does.
- **`is-shopify-cookie-banner-visible`** is now `cookie-banner-visible`. **`.filepond--*`** is gone with FilePond.
