Rules and selectors
A rule is a selector followed by declarations in braces. The selector names the style the rule is for; each declaration sets one property.
.button { padding: 2px 8px; } /* a class */
.button, .toggle { cursor: pointer; } /* a selector list shares one block */
* { font-family: "Inter"; } /* the base of every style */
- A selector is a class name: the name of one style.
- The leading . is optional.
- Class names are case-sensitive.
- Property names and keywords aren't.
- * is the base style.
- It applies to every class defined so far.
- Every class defined after it starts from it.
- Class rules override it, so put it first.
- A library style whose class no file defines gets the base.
A class can appear in any number of rules.
- Later declarations win.
- There is no specificity, the browser rule that ranks one selector above another. File order is the only priority.
- The exception is child position rules, which always win.
For example, .label-help gets its padding and color from the first rule below and its underline from the second (from themes/styles.css, shortened):
.label, .label-help {
padding: 0px 8px;
color: var(--text-2);
}
.label-help, .group-label-help, .drag-label-help, .group-drag-label-help {
text-underline-thickness: 1px;
text-underline-style: dotted;
}
- There are no combinators: selectors that match an element by where it sits inside another.
- .a .b, .a > .b and the like are errors.
Pseudo-classes and states
A control can be in several states at once, such as hovered and focused. A pseudo-class after the class name, such as :hover, makes a rule apply only in that state.
Each style has a slot per element state: one set of values for each combination of states. A rule writes to every slot whose state bits include the selector's bits:
| Selector | State |
| (none) | Every state, including disabled |
| :hover | Under the pointer |
| :active | Being pressed |
| :focus | Has keyboard focus |
| :selected or ::highlight(selected) | Selected or checked (a selected row, a toggle that's on) |
| ::selection | Selected text in the element |
| :disabled | Disabled |
| :not(:hover), :not(:active), :not(:focus), :not(:selected) | Slots without that bit |
| :focus-visible | Focus ring, shown when focus came from the keyboard |
| :normal | Same as no pseudo-class |
- ::selection:
- Its background-color is the highlight behind the selected text.
- It takes two colons: :selection is an error.
:disabled:
- It is exclusive: it can't combine with other states.
- Disabled controls are also faded (see Opacity).
- A :disabled rule changes only what it declares; the rest comes from the base rule.
From themes/styles.css:
.menu-item-row:disabled { background-color: transparent; opacity: 0.45; }
- :focus-visible:
- The focus ring is an outline drawn around the control that has keyboard focus.
- It takes only box-shadow (see Box shadow and focus ring).
States combine:
- .a:hover also applies while hovered and pressed.
- .a:focus::selection applies to selected text in a focused element.
- Write the general rule first and the specific ones after:
.text-style { background-color: var(--bg-control); border-color: var(--line-strong); }
.text-style:hover { background-color: var(--bg-active); }
.text-style:active { border-color: var(--accent); background-color: var(--bg-pressed); }
.text-style:focus { border-color: var(--accent); background-color: var(--bg-control); }
.text-style::highlight(selected) { color: var(--text-1); background-color: var(--accent-soft); }
.textbox::selection { color: var(--text-1); background-color: var(--selection); }
.textbox:not(:focus)::selection { background-color: var(--selection-inactive); }
- Only "state" properties differ per slot.
- The property tables mark them "state".
- A layout property such as padding or border-style in a :hover rule changes the style for every state.
Changing state
When a control changes state, its look moves from the old slot to the new one:
- The box (background and border) blends from the old slot to the new one.
- Text, icons and shadows switch at once.
Easing is the curve a blend follows over its time:
- Ease-out is cubic-bezier(0.2, 0, 0, 1).
- Ease-in-out is cubic-bezier(0.4, 0, 0.2, 1).
| Change | Time | Easing |
| Hover in | 70 ms | ease-out |
| Hover out | 140 ms | ease-out |
| Press | at once | |
| Release | 120 ms | ease-out |
| Focus in or out | 100 ms | ease-out |
| Selected (a row) | at once | |
| A switch, check box or toggle turning on or off | 120 ms | ease-in-out |
| Disabled | at once | |
| Invalid (BeginInvalid) | the border at once, the halo 150 ms | ease-out |
- How two colors blend depends on the colors:
- Colors whose hues differ by more than 20 degrees (both ends non-grey) blend in Oklab, which keeps lightness and saturation.
- Greys and shades of one hue blend in sRGB.
- A transparent end adds only its alpha.
- A change that reverses mid-blend continues from what's shown, with the new timing.
- A part blends its on/off flip only with ElementFlags::AnimatesSelected.
- Other selection changes are instant.
Popups and palette switches:
| Change | Time | Easing |
| A menu or popup opening | 120 ms, fading in and sliding 4px away from what it opens from | ease-out |
| A tooltip appearing | 100 ms, fading in | ease-out |
| Either closing | 80 ms, fading out | ease-out |
| Another palette (RequestReloadTheme) | 200 ms, with the old look fading out over the new one | ease-in-out |
With reduced motion every change is instant:
Child position
:first-child, :last-child and :only-child style an element by its place among its parent's children. For example, the segments of a button group can round only the outer corners of the group:
.segment { border-radius: 0; }
.segment:first-child { border-radius: 4px 0 0 4px; }
.segment:last-child { border-radius: 0 4px 4px 0; }
.segment:only-child { border-radius: 4px; }
- An only child takes the :first-child and :last-child rules, then :only-child.
- :first-child:last-child equals :only-child.
- They apply on top of all the class's other rules and *, wherever those are.
- They take box properties only:
- backgrounds
- borders (border-*, -tachyon-border-dash-*)
- box-shadow
- margin, padding
- gap, row-gap, column-gap
- cursor
- Anything else is an error, because size and text are computed before the last child is known.
- They combine with states, as in .a:first-child:hover.
- A class is required: *:first-child is an error.
- Every child counts, including hidden ones.
Comments
- /* block */
- // to end of line
@font-face
An @font-face rule adds a font file to a font family. A font-family property then picks the family by name.
@font-face { font-family: "Inter"; src: url("../fonts/Inter/Inter-Regular.ttf"); }
@font-face { font-family: "Inter"; src: url("../fonts/Inter/Inter-Bold.ttf"); }
- font-family is the family name, quoted or as plain words.
- src is the file:
- url("path") or "path"
- an optional format(...) after it is ignored
- one source per rule
- Faces with the same family name form one family.
- Weight and style come from the files.
- Relative paths are relative to the palette file's folder (themes/), in any style file.
- Use it with font-family: "Inter";.
Custom properties
A custom property is a value with a name that starts with --. Define it once and use it in many rules:
- Define them in :root blocks: any number, in any file.
- Use them with var(--name).
:root {
--accent: #6B9BFF; /* a color */
--bg-band: linear-gradient(90deg, #000, #333); /* a gradient */
--radius-md: 4px; /* one number */
--radius-md-first: 4px 0px 0px 4px; /* 2 to 4 numbers, for shorthands */
--focus-ring: 0 0 0 3px #6B9BFF4D; /* a box-shadow (numbers then a color) */
--shadow-control: none; /* no box-shadow */
}
What a variable can hold, and where you can use it:
| Value | Usable in |
| Color or gradient (any color form, or var() of an earlier color variable) | Color properties, gradient stops, box-shadow colors |
| One number (px/em suffix ignored) | Any property taking var() and one number |
| 2 to 4 numbers | margin, padding, border-width, border-radius (one value per slot) |
| A box-shadow list, or none | box-shadow |
- Numeric variables are whole numbers from 0 to 65535.
- Fractions are cut off.
- Negatives become 0.
- An undefined variable is an error.
- A variable of the wrong kind is an error.
From themes/styles.css, where --border and --radius-md hold one number each:
.text-style {
border-width: var(--border);
border-radius: var(--radius-md);
}
Previous: How a theme loads | CSS and theming | Next: Values