## Surfaces
A **surface** is anything that has a look: a button, a field, a balloon, a plain `div`.
The surface engine paints all of them **in one place**, from a small set of values called **tokens**.
- Components only say *what* they want (a background, a border, an ink).
- The engine decides *how* to paint it, in every visual state.
- Designers restyle anything with the same **generic classes**: `fill-primary`, `content-accent`, `rise-2`...
The engine is a preview and it is **opt-in**: it paints only inside `.pacem-surfaces`.
Everywhere else the standard theme is in charge. This site opts in on its `body`.
```html
<body class="pacem-surfaces">
...
</body>
```
## How it works
### Host, face and content
Every surface plays three roles. Often one element plays all of them.
| Role | Job |
|---|---|
| **host** | holds the visual states and **reads the tokens** |
| **face** | paints the box: background, border, shadows, outline |
| **content** | paints the ink: text color, text shadow |
A button is all three. A field is the host, its inner `input` is the face.
A menu is the host, its `nav` is the face.
### Tokens and slots
You write **tokens** on the host. The engine turns them into **slots**, and the face paints the slots.
```
host: --pacem-surface-background-color (token: you write it)
↓
--pacem-slot-background-color (slot: the engine's, for the current state)
↓
face: background-color
```
- Tokens **do not inherit**: each host reads its own.
- A token you don't write keeps the **standard look** of that property.
- Never write slots: they are the engine's output.
### Layers
Rules are ordered **state by state**. A later state always wins, whatever the specificity:
```
default → focus → hover → active → selected → readonly → disabled
```
Inside each state, from generic to specific:
```
engine → kinds → variants → utilities → app
```
- **kinds**: what a component is (the button, the field...)
- **variants**: what it becomes in a context (a button in a pager, in a menu...)
- **utilities**: the designer's classes (`fill-*`, `rise-*`...)
- **app**: your own rules
So a disabled `fill-primary` button is gray: *disabled* comes after *default*.
## Traits
Traits are the **features of a theme** that are on or off: shadows, gradients, glows...
Each theme starts from the traits of its base and can override them in `pacem-themes.json`.
| Trait | Toggles | `dark` | `light` |
|---|---|:---:|:---:|
| `shadows` | drop shadows | ✓ | |
| `gradients` | glossy gradients | ✓ | |
| `glows` | glowing hovers | | |
| `blur` | backdrop blur | | |
| `dark-scheme` | dark color scheme | ✓ | |
| `field-underline` | underlined fields (boxed otherwise) | ✓ | |
| `field-outline` | focus outline on every field | | |
In css, a trait is a zero-specificity selector: `:--name` when on, `:--no-name` when off.
```css
@layer surfaces.default {
:--shadows .my-card { --pacem-surface-shadow-elevation: var(--pacem-rise-2); }
:--no-shadows .my-card { --pacem-surface-border-width: 1px; --pacem-surface-border-style: solid; }
}
```
Any other name is a **custom trait**, off unless a theme turns it on.
`npx pacem-theme-css traits` lists them all.
## Tokens
One token per painted property: `--pacem-surface-{property}`.
**The box**
| Token | Paints |
|---|---|
| `background-color` | the background |
| `background-image-1`, `-2`, `-3` | up to three background images |
| `border-width`, `border-style`, `border-color`, `border-radius` | the border |
| `border-image` | a **frame** (see below) |
| `shadow-elevation`, `shadow-glow`, `shadow-inset` | the three entries of `box-shadow` |
| `outline-color` | the outline |
**The ink**
| Token | Paints |
|---|---|
| `color` | the text color |
| `text-shadow` | the text shadow |
**Size, spacing, motion**
| Token | Paints |
|---|---|
| `width`, `height`, `aspect-ratio` | the size |
| `margin-top`, `-right`, `-bottom`, `-left` | the margins |
| `padding-top`, `-right`, `-bottom`, `-left` | the paddings |
| `duration-background`, `-color`, `-border`, `-shadow`, `-outline`, `easing` | the transitions |
Some properties are **lists** made of several tokens: `box-shadow` (elevation, glow, inset), `background-image`
(1, 2, 3), `transition` (the durations). The **first** token of a list must be set, or the whole list keeps its
standard look. A ring alone, for example, needs a neutral elevation too: `--pacem-surface-shadow-elevation: 0 0 #0000`.
## States
A visual state has no tokens of its own: it is a **rule** that changes the tokens it needs.
What it doesn't change keeps its resting value.
| State | Selector | When |
|---|---|---|
| focus | `:--focused` | focused (never when disabled) |
| hover | `:--hovered` | hovered (never when disabled or read-only) |
| active | `:--pressed` | pressed (never when disabled or read-only) |
| selected | `:--selected` | `.pacem-selected` on a togglable host, or `:checked` |
| read-only | `:--readonly` | `.pacem-readonly`, or `[readonly]` |
| disabled | `:--disabled` | `.pacem-disabled`, or `:disabled` |
Each state rule goes in **its own layer**. In an embedded file:
```css
@layer surfaces.default {
.my-button { --pacem-surface-background-color: var(--pacem-color-info); }
}
@layer surfaces.hover {
.my-button:--hovered { --pacem-surface-background-color: var(--pacem-color-warning); }
}
```
Below, the same rules in a plain stylesheet. The disabled button stays gray: the button's own *disabled* rule comes later.
## Utilities
Generic classes that set tokens. They win over what a component sets, in the same state.
**Color**
| Class | Sets |
|---|---|
| `fill-{color}`, `fill-{color}-container` | the background, and a matching ink |
| `content-{color}` | the ink (it overrides a fill's ink) |
| `halo-{color}` | a 2px ring (with an elevation: add a `rise-*`, or it shows the standard glow) |
| `fill-none` | no background, in any state |
Color utilities change on hover, focus and press only when the host asks for it:
`fill-interactive` (or `content-`, `halo-interactive`), `pacem-hoverable`, `pacem-focusable`, `pacem-clickable`.
Buttons always do.
**Shape and size**
| Class | Sets |
|---|---|
| `shape-circle`, `shape-square` | a 1:1 box (the circle rounded) |
| `shape-ellipse`, `shape-rect` | a free ratio, or `ellipse-ratio-*` / `rect-ratio-*` (`16-9`, `4-3`...) |
| `size-1` ... `size-12` | the width, on the size scale |
| `size--1` ... `size--12` | the width, in pixels |
**Depth**
| Class | Sets |
|---|---|
| `rise-1` ... `rise-6` | a drop shadow (`rise--1` ... `rise--6`: finer steps) |
| `sink-1` ... `sink-6` | an inner shadow (`sink--1` ... `sink--6`: finer steps) |
**Borders**
| Class | Sets |
|---|---|
| `border-none` | no border at all |
| `border-size-0` ... `border-size-12` | the border width (`border-size--N`: N pixels) |
| `border-rise` | a bevel lit from above (with `border-{color}`, in that tint) |
The border width doesn't decide which sides show: the component's border styles do.
## Frames
`--pacem-surface-border-image` takes any image, a gradient or a `url()`, and paints it **in the border**.
Unlike a plain `border-image`, a frame **follows the radius**.
```css
.framed {
--pacem-surface-border-width: 2px;
--pacem-surface-border-radius: 12px;
--pacem-surface-border-image: linear-gradient(to right, var(--pacem-color-primary), var(--pacem-color-accent));
}
```
- Give it a **width token**: the border then turns solid by itself.
- The image is stretched over the whole box.
- `none` means no frame.
- Older browsers get a plain, square-cornered `border-image`.
The buttons' own gradient border (in themes with gradients) is a frame too.
## Writing your own rules
### Where they go
**In an embedded file** (the `embed` files of `pacem-themes.json`), wrap them in a state block:
```css
@layer surfaces.default { ... }
@layer surfaces.hover { ... }
```
The states are `default`, `focus`, `hover`, `active`, `selected`, `readonly`, `disabled`.
Inside, you can use the state selectors (`:--hovered`...) and the theme traits (`:--shadows`...).
**In a stylesheet of its own** (no build), name the layer in full and write the states out:
```css
@layer pacem.surfaces.hover.app {
.x:hover:not(.pacem-disabled, :disabled, .pacem-readonly) { ... }
}
```
The demos on this page are written this way.
### Five rules of thumb
1. **Target the host.** Tokens are read on the host only: the `pacem-input-text`, not its `input`.
2. **Write tokens, not slots.** `--pacem-surface-*`, never `--pacem-slot-*`.
3. **A border needs a style.** Width, color and radius show nothing while the style is `none`.
4. **Change a state in its own block.** A later state always wins over your default rule.
5. **Specificity counts only within a layer.**
The demo shows rules 1 and 3.
### When nothing happens
| You see | Do this |
|---|---|
| the border doesn't show | set a border style, or a width with a frame |
| a token on an inner element does nothing | move it to the host |
| `unknown :--x: a local alias of ...` | select the element itself, or `[surface-modifier]` |
| `"@layer ..." is not a surfaces block` | write `@layer surfaces.{state} { ... }` |
| your rule is lost on hover or when disabled | repeat the token in that state's block |
| your hover rule fires when disabled | use `:--hovered`, not `:hover` |
## Cookbook
**A rounded gradient border on the active link:**
```css
@layer surfaces.default {
pacem-a.active {
--pacem-surface-padding-top: var(--pacem-size-1);
--pacem-surface-padding-bottom: var(--pacem-size-1);
--pacem-surface-border-radius: var(--pacem-size-2);
--pacem-surface-border-width: 1px;
--pacem-surface-border-image: linear-gradient(to right, red, purple);
}
}
```
**A gleam**, an image layer over the surface's color:
```css
@layer surfaces.default {
.gleam:where([surface-modifier], .surface) {
--pacem-surface-background-image-1: linear-gradient(160deg, rgb(255 255 255 / 25%), rgb(255 255 255 / 0%) 60%);
}
}
```
Translucent stops let any fill show through; opaque ones cover it.
## Components Components are surfaces on their own: their decorator marks them, nothing to write. Put the utilities on the component; it paints its face. | Component | Face | Notes | |---|---|---| | `pacem-button` | itself | `fill-*` fills it | | `pacem-a` | itself | a link: `content-*` recolors it, `fill-*` boxes it | | `pacem-input-*`, `pacem-textarea`, `pacem-select`... | the inner input | | | `pacem-checkbox`, `pacem-radio` | the label | `fill-*` is the "on" color | | `pacem-slider`, `pacem-progressbar` | the track | `content-*` colors the progress | | `pacem-loader` | the spinner | `content-*` the arc, `fill-*` the ring | | `pacem-balloon` | its body and tail | | | `pacem-lightbox`, dialogs | the container | | | `pacem-sidebar`, `pacem-menu` | the drawer | | | `pacem-toast` | itself | | | `pacem-icon` | itself | `content-*` colors the glyph, `fill-*` boxes it | Plain markup opts in with a class: `surface` (no look of its own), `surface-button`, `surface-field`.
To provide the best experiences, we use technologies like cookies to store and/or access device information. Consenting to these technologies will allow us to process data such as browsing behavior or unique IDs on this site. Not consenting or withdrawing consent, may adversely affect certain features and functions.