# "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)  
URL: https://www.smootify.io/docs/astro/developers/css  
Updated: 2026-10-21

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 in your own CSS.

## The stylesheet

The integration adds the stylesheet next to the Smootify script, unless `runtimeCssUrl` is `false`: see [Install Smootify in Astro](https://www.smootify.io/docs/astro/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 the component’s scoped `<style>`, or in `global.css`. Smootify clones templates, and the clone keeps the component’s scope attribute, so scoped rules still match the state classes. Use `:global()` for HTML that Smootify writes from data, which has no scope attribute, and for classes it sets on a host:

```astro
<style>
  smootify-product.is-on-sale .product-price {
    color: #b42318;
  }
  :global(smootify-product.loaded) .product-info {
    opacity: 1;
  }
</style>
```

See [Write components with Smootify elements](https://www.smootify.io/docs/astro/build/components#5-styles).

## 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**: the `hidden` attribute, on the card (`.sm-dyn-item`) or on `smootify-product`. Smootify marks the `hidden` it sets and removes only that one.

```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/astro/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 to add skeletons is in [loading](https://www.smootify.io/docs/astro/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 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/astro/build/popovers) and [Market suggestion](https://www.smootify.io/docs/astro/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/astro/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/astro/reference/attributes/upload) and [File uploads](https://www.smootify.io/docs/astro/features/file-uploads).

## Snippets

Short rules for common needs. Put them in `global.css` 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 checkbox or radio button.** To style the whole label, text included, when its input is checked:

```css
label:has(input:checked) { /* your styles */ }
```

**The page while a menu is open.** A menu is a `[popover]` or a `<dialog>`. To change the page behind it, for example to stop it from scrolling:

```css
body:has(:popover-open, dialog[open]) { overflow: hidden; }
```

## Where people get stuck

> [!STUCK]
> - **Elements with a condition always show.** The stylesheet is not loaded: do not set `runtimeCssUrl: false` unless the site has those rules itself.
> - **Nothing pulses while the page loads.** In 2.0 only elements with a `skeleton` attribute are placeholders. Add it where you want one.
