# @volverjs/style
A CSS/SCSS design system: BEM components (`.vv-button`, `.vv-button__label`,
`.vv-button--primary`), Tailwind-like utilities (`p-md`, `flex`, `grid-cols-3`), design
tokens as CSS custom properties, and an SCSS layer that generates all of it from maps.
Every selector is wrapped in `:where()`, so it has zero specificity and any CSS you write
wins without `!important`.
The vocabulary is close enough to Tailwind that guessing feels safe. It is not: a class
that does not exist fails silently, and about a third of the classes an agent tends to
guess (`display-flex`, `md:p-16`, `z-10`, `rounded-2xl`, `text-uppercase`, `hidden`) are
not in this library. Work from the reference files, not from memory.
## Workflow
1. **Locate the installed library.** In a consuming project it is
`node_modules/@volverjs/style/` (the package ships `src/`, `dist/` and
`design-tokens.json`). Inside the library repo itself use `./src` and `./dist`
(`pnpm build` regenerates `dist`).
2. **Read the reference for what you are about to write** (all in `references/`):
- [utilities.md](references/utilities.md): every utility class with its exact value
set and whether it has responsive variants.
- [components.md](references/components.md): canonical markup, elements, modifiers and
states for all 55 components, plus how states are expressed.
- [tokens.md](references/tokens.md): custom properties, CSS-only overrides, dark theme.
- [scss.md](references/scss.md): import paths, context configuration, extending and
creating components, mixins, cascade layers.
3. **Verify anything not in the references** against the compiled CSS before using it:
```bash
# does a utility exist? (responsive form: escape the colon)
grep -c -F '.gap-md' node_modules/@volverjs/style/dist/utilities.css
grep -c -F '.md\:grid-cols-3' node_modules/@volverjs/style/dist/utilities.css
# which modifiers does a component have?
grep -oE '\.vv-alert--[a-z-]+' node_modules/@volverjs/style/dist/components/vv-alert.css | sort -u
# the full map of a component (elements, modifiers, states, aliases)
cat node_modules/@volverjs/style/src/settings/components/_vv-alert.scss
```
4. **Prefer the library's idiom over custom CSS**: a token override or a modifier from the
map beats a new rule, and stays coherent with the dark theme.
## Where it differs from Tailwind
These are the mistakes that recur. Each row is verified against the compiled CSS.
| Habit | In @volverjs/style |
|---|---|
| `display-flex`, `hidden` | Bare names: `flex` `grid` `block` `inline-flex` `none` (there is no `hidden`) |
| `relative`, `absolute` | Same bare names, fine |
| `md:p-16`, `lg:gap-8` | Spacing and gap have **no responsive variants**. Use the dynamic tokens `xs sm md lg xl` (`p-md`, `gap-lg`), which grow with the viewport by themselves |
| `xxs:` prefix, `2xl:` | Prefixes are `xs: sm: md: lg: xl: xxl: xxxl:` (360, 576, 992, 1024, 1280, 1440, 1536px) and only display, position, visibility, flex/grid, sizing, text size/align, border width/style, aspect and columns accept them |
| `@md:flex-row` with no container | Container prefixes `@xs:` `@sm:` `@md:` `@lg:` `@xl:` `@xxl:` `@xxxl:` (20, 24, 28, 32, 36, 42, 48rem) read the nearest ancestor with the `@container` class and match nothing without one. Only display, flex direction and wrap, justify/align content and items, `grid-cols-`, `col-` and text align have them |
| `hover:bg-…`, `dark:text-…`, `focus:` | No state or theme variants at all. Use component states, tokens, or your own CSS |
| `max-w-md`, `max-w-2xl` | `max-w-screen-md` … `max-w-screen-xxxl`, `max-w-prose`, or `container`. `max-w-md` exists, but it is the dynamic spacing step (12 to 16px) |
| `z-10` … `z-50` | Semantic: `z-sticky` `z-fixed` `z-dropdown` `z-modal-backdrop` `z-modal` `z-popover` `z-tooltip` `z-toast` (plus `z-1`, `z-auto`) |
| `aspect-video` | `aspect-wide` (16/9); also `square` `photo` `tv` `ultrawide` |
| `rounded-2xl`, `rounded-3xl` | `rounded-xxl`, `rounded-xxxl` (shadows do use `shadow-2xl`) |
| `text-xl`, `text-4xl`, `text-40` | Dynamic `text-xs`…`text-xl` (responsive by design) or static `text-12 14 16 18 20 22 24 26 28 30 32 34 36 48 60 72 96 128`. No 40, 44, 56, 64, 80 |
| `font-italic`, `text-uppercase`, `tracking-loose` | Bare `italic` `non-italic` `uppercase` `lowercase` `capitalize`; `tracking-tighter/tight/normal/wide/wider/widest` |
| `text-blue-500`, `bg-gray-100` | Semantic palette: `text-word` `text-word-2` `bg-surface-1` `border-surface-3` `bg-surface-brand`, and for text in a color a role, `text-brand-readable` `text-danger-readable`, never a shade |
| `space-y-4`, `divide-y` | Not available: use `flex flex-col gap-md` |
| `w-1/2` | Exists, written literally: `w-1/2`, `col-span-6` |
| `border-none` to remove a border | `border-0` (`border-none` is `border-style: none`) |
| `p-30`, `m-11` | The scale has no 11, 15, 30, 60, 72: see the list in utilities.md |
Color semantics: `word` is text (`word-1`…`word-5` progressively fainter), `surface` is
background (`surface-1`…`surface-5` progressively deeper), `surface-{brand,success,…}`
are tinted backgrounds for badges and callouts, `alpha-*` are translucent overlays.
Every palette color has `-lighten-1..5` and `-darken-1..5`, which are fills, plus roles
that stay legible whatever color is set at runtime, in both themes:
| What you are painting | Token, for brand (same for accent, success, danger, info, warning) |
|---|---|
| Text on the page, down to `surface-2` | `text-brand-readable`; a step further, a pressed link: `text-brand-readable-strong` |
| Text on a tinted surface | `bg-surface-brand text-surface-brand-readable` |
| Text on a fill | `bg-brand text-brand-contrast`, `bg-brand-darken-1 text-brand-darken-1-contrast` |
| A bar, a focus ring, a border that shows a state | `bg-brand-graphic`, `border-brand-graphic` (3:1, not text) |
A shade written as text (`text-brand`, `text-danger-darken-1`) reads only by chance: with a
light tenant brand, or in the dark theme, it can fall to 1.5:1.
## Layout recipes (verified)
```html
…
…
…
…
…
…
Title
Secondary copy
Long text
```
## Components
Naming: block `.vv-{name}`, element `.vv-{name}__{el}`, modifier `.vv-{name}--{mod}`.
Available: `vv-button` `vv-button-group` `vv-input-text` `vv-textarea` `vv-select`
`vv-checkbox` `vv-radio` `vv-checkbox-group` `vv-radio-group` `vv-input-file`
`vv-input-range` `vv-card` `vv-dialog` `vv-alert` `vv-alert-group` `vv-dropdown`
`vv-dropdown-action` `vv-dropdown-option` `vv-dropdown-optgroup` `vv-nav` `vv-tab`
`vv-accordion` `vv-accordion-group` `vv-table` `vv-badge` `vv-avatar` `vv-avatar-group`
`vv-tooltip` `vv-progress` `vv-skeleton` `vv-spinner` `vv-breadcrumb` `vv-text`
`vv-empty` `vv-separator` `vv-item` `vv-item-group` `vv-field` `vv-prose` `vv-pagination`
`vv-message` `vv-bubble` `vv-bubble-group` `vv-message-scroller` `vv-attachment`
`vv-attachment-group` `vv-marker` `vv-questionnaire` `vv-kbd` `vv-kbd-group`
`vv-popover` `vv-command` `vv-input-otp` `vv-sidebar` `vv-calendar`.
Three facts shape the markup; components.md has the full structure of each component.
**Semantic children replace element classes.** Most elements have an alias, so the
natural tag is styled with no class: `.vv-input-text > label`, `.vv-button > svg`,
`.vv-card > header`, `.vv-dialog > article`, `.vv-accordion > summary`. Elements without
an alias (`__wrapper`, `__content`, `__close`, `__hint` on a ``) need the class.
**States come from the DOM, not from classes.** Each state also has a natural selector:
`[disabled]` or `[aria-disabled="true"]`, `[readonly]`, `[open]`, `[aria-pressed="true"]`,
`[aria-selected="true"]`, `:hover`, `:focus-visible`. Write ``; the `--disabled`/`.disabled` classes exist only to freeze a look in docs.
Exception: `current` (nav, tab) is class only.
**Form fields share one skeleton and one token set.** A block `` with a direct-child
`
`, a `__wrapper` around the control, a direct-child ``.
`valid`, `invalid`, `loading`, `floating`, `icon-before`, `icon-after` are modifiers on
the block. `disabled` always propagates from the control's attribute through a `:has()`
rule naming that component's own control: `:has(input[disabled])` for a text, file or range
field, `:has(textarea[disabled])` for a textarea, `:has(select[disabled])` for a select.
`readonly`
propagates the same way, but HTML only gives the attribute to a text-type input and a
textarea, so only `vv-input-text` and `vv-textarea` can use it. Everywhere else the recipe
is the same: disable the control, add `tabindex="-1"` and the `--readonly` modifier, which
undoes the disabled dimming. A disabled control submits nothing, so add a hidden input when
the value has to reach the server. Restyle all fields at once with `--input-*` tokens.
```html
Save
…
…
…
```
Button modifiers: `primary` `secondary` `danger` `ghost` `link` `action` `action-quiet`
`static-light` `static-dark` `icon-only` `rounded` `block` `reverse` `column` `full-bleed`.
The default button is the brand accent; there is no `accent`, `loading` or size modifier.
Traps: `vv-dialog` needs the `` panel (the
`` itself is the backdrop) and has no `__title`; `vv-alert` has no
`__description`/`__action`, icon and title live in `__header`; `vv-card__picture` goes
on the ` `; `vv-alert-group` wraps its alerts in ``; the checkbox
switch is `vv-checkbox--switch`; `vv-input-range` needs
`style="--input-range-progress: 45%"` on the block or the track renders empty.
## Tokens and theming
Tokens live on `:where(:host, :root, .theme)`, so `:root { --color-brand: #45cb85 }`
recolors the whole palette (shades derive with relative color syntax; there are no
`--color-brand-hue/saturation/lightness` channels). Component properties are prefixed
with the block: `--vv-button-background`, `--vv-button-state-hover-background`,
`--vv-button-modifier-primary-background`. Fields share `--input-*`.
```css
:root, :host, .theme { --color-brand: #45cb85; --color-tint: #45cb85; --rounded: 0.5rem; }
```
`--rounded` is the base of the radius scale: `--rounded-sm` … `--rounded-xxxl` are multiples
of it, so it rescales every corner (`none` and `full` stay). `--color-tint` gives the brand hue to gray, word and surface too; leave it out to keep the
neutrals. The readable, contrast and graphic roles follow the new brand by themselves. A
new button variant goes through the SCSS map (see scss.md) or, in plain CSS, names the
hover state itself, since a class beats the zero specificity of the library's state
rules: the recipe is in tokens.md.
Dark theme is a separate stylesheet (`@import '@volverjs/style/themes/dark'` or
`@use '@volverjs/style/scss/themes/dark'`) loaded after the base. With it loaded the page
follows `prefers-color-scheme`; force with `class="theme theme--dark"` or
`class="theme theme--light"` on `` or on any subtree. Both classes are required;
there is no `data-theme`. Keep custom colors as `var(--color-…)` so they follow the theme.
## SCSS
```scss
@use '@volverjs/style/scss/context' with (
$color-brand: #45cb85,
$font-family-sans: "'Inter', sans-serif", // one quoted string, or it will not compile
$use-css-layers: true
);
@use '@volverjs/style/scss';
@use '@volverjs/style/scss/themes/dark';
```
Extend a component by deep-merging its map before the library is emitted
(`ctx.$vv-button: map.deep-merge(ctx.$vv-button, (modifier: (success: (…))))`), create
one with `spread-map-into-bem`, and query breakpoints with `@include ctx.bp-up(md)`, the nearest `@container` with
`@include ctx.cq-up(md)`.
When cherry-picking, alias modules that share a basename (`props/spacing` and
`utilities/spacing` collide) and take `scss/props` whole with any component. Layers are
emitted as `volver.reset`, `volver.utilities`, … Details and signatures in scss.md.
## With @volverjs/ui-vue
Vue components render these classes; `modifiers="primary"` becomes `.vv-button--primary`,
so valid modifier names are the ones in components.md. Utilities go on wrappers and
slots as usual (`
`). Import `@volverjs/style` once in the entry
file and the dark theme next to it; toggle `theme theme--dark` on `` (the library
docs use `useDark({ attribute: 'class', valueDark: 'theme theme--dark', valueLight: 'theme theme--light' })`).
## Before you finish
- Every class you wrote is in the references or was confirmed with grep.
- Responsive prefixes only on groups that support them; spacing uses dynamic tokens.
A `@` container prefix also needs an ancestor with the `@container` class.
- Disabled comes from the attribute, and readonly too where HTML has one; elsewhere
readonly is the modifier. Hints are linked with `aria-describedby`, labels have `for`.
- Dialogs have the `__wrapper` article; range inputs have `--input-range-progress`.
- Colors are tokens, not hex, so the dark theme keeps working.
- Text in a color is a `-readable` role, text on a fill its `-contrast`, a bar or a ring
its `-graphic`: never a shade, which stops reading with a light brand or in the dark.
---
# Utility classes
Every class below was verified against `src/settings/*.scss`, `src/utilities/*.scss`
and the compiled `dist/utilities/*.css`. If a class is not in this file, assume it
does not exist and check `node_modules/@volverjs/style/dist/utilities/.css`
before using it.
## How names are built
- `{prefix}-{value}` for most groups: `p-16`, `gap-md`, `text-brand`, `w-1/2`.
- **Bare value names** (no prefix) for display, position, visibility, font style,
text transform, text decoration line, font smoothing and font-variant-numeric:
`flex`, `grid`, `none`, `relative`, `invisible`, `italic`, `uppercase`, `underline`.
- Responsive variant: `{breakpoint}:{class}`, mobile-first (`min-width`), **only for
the groups marked "Responsive: yes" below**. Prefixes: `xs:` `sm:` `md:` `lg:` `xl:`
`xxl:` `xxxl:`. There is no `xxs:` prefix (xxs is 0px, the unprefixed class).
| Breakpoint | xs | sm | md | lg | xl | xxl | xxxl |
|---|---|---|---|---|---|---|---|
| min-width | 360px | 576px | 992px | 1024px | 1280px | 1440px | 1536px |
- Container variant: `@{step}:{class}` (`@md:flex-row`), the width of the nearest ancestor
with the `@container` class (`container-type: inline-size`); with none it never applies.
Only for display, `flex-` direction and wrap, `justify-` `content-` `justify-items-`
`items-`, `grid-cols-`, `col-` (span, auto, full) and `text-` align. Emitted after the
responsive classes of the same utility, so it wins when both apply.
| Container step | @xs | @sm | @md | @lg | @xl | @xxl | @xxxl |
|---|---|---|---|---|---|---|---|
| min-width | 20rem | 24rem | 28rem | 32rem | 36rem | 42rem | 48rem |
## Scales shared by several groups
**Static spacing scale** (used by margin, padding, gap, inset, sizing):
```text
0 px 1 2 3 4 5 6 7 8 9 10 12 14 16 18 20 22 24 26 28 32 34 36 38 40 44 48 50 52 56 64
80 96 100 112 128 144 150 160 176 192 208 224 240 256 288 300 320 384
```
There is no 11, 15, 30, 60 or 72 step. `px` is 1px.
**Dynamic spacing scale**: `xs` `sm` `md` `lg` `xl`. Each one resolves to a different
static step per viewport, so the same class breathes on larger screens:
| token | < 576px | ≥ 576px | ≥ 1024px |
|---|---|---|---|
| xs | 6 | 8 | 10 |
| sm | 10 | 12 | 14 |
| md | 12 | 14 | 16 |
| lg | 16 | 22 | 36 |
| xl | 24 | 36 | 48 |
Prefer the dynamic tokens for layout rhythm (`p-md`, `gap-lg`, `mb-xl`) and static
numbers only for pixel-exact needs. Spacing utilities have **no** responsive
variants, so `md:p-16` does nothing: the dynamic tokens are how spacing adapts.
**Color names** (used by `text-`, `bg-`, `border-`, `decoration-`):
```text
brand accent success danger info warning gray each also -lighten-1..5 and -darken-1..5
brand-contrast accent-contrast success-contrast white or black, the text that reads on that fill,
danger-contrast info-contrast warning-contrast also -darken-1..5-contrast, text- only (no gray-contrast)
brand-readable brand-readable-strong the color as text on the surface, text- only
surface-brand-readable surface-success-readable ... the color as text on its tinted surface, text- only
brand-graphic accent-graphic ... the color as a bar, ring or state border, 3:1
word word-1..word-5 text colors, from strongest to faintest
surface surface-1..surface-5 backgrounds, from page to deepest inset
surface-brand surface-accent surface-success surface-danger surface-info surface-warning
alpha alpha-1..alpha-5 translucent overlays
white black transparent shadow backdrop
```
## Spacing (not responsive)
| Class | Values | Notes |
|---|---|---|
| `m-` `mx-` `my-` `mt-` `mr-` `mb-` `ml-` | spacing scale + dynamic | margin |
| `p-` `px-` `py-` `pt-` `pr-` `pb-` `pl-` | spacing scale + dynamic | padding |
| `n-` `nx-` `ny-` `nt-` `nr-` `nb-` `nl-` | spacing scale + dynamic | **negative** margin (`n-16` = `margin: -16`) |
| `m-auto` `mx-auto` `my-auto` `mt-auto` `mr-auto` `mb-auto` `ml-auto` | | margin only, no `p-auto` |
## Layout
| Class | Values | Responsive |
|---|---|---|
| display (bare) | `block` `inline-block` `inline` `flex` `inline-flex` `grid` `inline-grid` `none` | yes |
| display (bare) | `table` `inline-table` `table-caption` `table-cell` `table-column` `table-column-group` `table-footer-group` `table-header-group` `table-row-group` `table-row` `flow-root` `contents` `list-item` | no |
| position (bare) | `static` `fixed` `absolute` `relative` `sticky` | yes |
| visibility (bare) | `visible` `invisible` | yes |
| `inset-` `inset-x-` `inset-y-` `top-` `right-` `bottom-` `left-` | spacing scale + dynamic + `1/2` `1/3` `2/3` `1/4` `2/4` `3/4` `full` `auto` | no |
| `z-` | `1` `sticky` `fixed` `dropdown` `modal-backdrop` `modal` `confirm-backdrop` `confirm` `popover` `tooltip` `toast` `notification-alert` `auto` | no |
| `overflow-` `overflow-x-` `overflow-y-` | `visible` `hidden` `scroll` `auto` | no |
| `scrollbar-` | `thin` (thin, `word-5` thumb on a transparent track) `none` (hidden, still scrolls) | no |
| `container` | | width 100%, max-width follows the current breakpoint, the whole width below `xs` (360px); no size suffixes |
| `@container` | | `container-type: inline-size`, the box the `@` variants measure; it stops taking its width from its content, so not on an `inline-flex` or a flex item with no basis; no prefixes |
| `aspect-` | `auto` `square` `photo` (3/2) `tv` (4/3) `wide` (16/9) `ultrawide` (21/9) | yes |
| `object-` (fit) | `fill` `contain` `cover` `none` `scale-down` | no |
| `object-` (position) | `top` `bottom` `left` `right` `center` `left-top` `left-bottom` `right-top` `right-bottom` | no |
| `columns-` | `1`..`12` | yes |
| `break-after-` `break-before-` | `auto` `avoid` `all` `avoid-page` `page` `left` `right` `column` | no |
| `break-inside-` | `auto` `avoid` `avoid-page` `avoid-column` | no |
The z-index scale is semantic and stacks in the order listed (1, 1010, 1020, 1025,
1030, 1040, 1050, 1060, 1070, 1080, 1090, 1100). There is no `z-10`/`z-50`.
`display: none` is the class `none`; there is no `hidden` class.
## Flexbox
| Class | Values | Responsive |
|---|---|---|
| `flex-` (direction) | `row` `col` `row-reverse` `col-reverse` | yes |
| `flex-` (wrap) | `wrap` `nowrap` `wrap-reverse` | yes |
| `flex-` (size) | `1` `auto` `initial` `none` | yes |
| `grow` `grow-0` `shrink` `shrink-0` | | no |
| `order-` | `1`..`12` `first` `last` `none` | yes |
| `justify-` | `start` `end` `center` `between` `around` `evenly` | yes |
| `justify-items-` | `start` `end` `center` `baseline` `stretch` | yes |
| `justify-self-` | `auto` `start` `end` `center` `baseline` `stretch` | yes |
| `items-` | `start` `end` `center` `baseline` `stretch` | yes |
| `self-` | `auto` `start` `end` `center` `baseline` `stretch` | yes |
| `content-` (align-content) | `start` `end` `center` `between` `around` `evenly` | yes |
| `gap-` `gap-x-` `gap-y-` | spacing scale + dynamic | **no** |
## Grid
All grid utilities are responsive.
| Class | Values |
|---|---|
| `grid-cols-` | `1`..`12` `none` |
| `grid-rows-` | `1`..`6` `none` |
| `col-span-` | `1`..`12` `full`; plus `col-auto` |
| `row-span-` | `1`..`6` `full`; plus `row-auto` |
| `col-start-` `col-end-` | `1`..`13` `auto` |
| `row-start-` `row-end-` | `1`..`7` `auto` |
| `grid-flow-` | `row` `col` `dense` `row-dense` `col-dense` |
| `auto-cols-` `auto-rows-` | `auto` `min` `max` `fr` |
`gap-*` is shared with flexbox and is not responsive.
## Sizing (all responsive)
| Class | Values |
|---|---|
| `w-` | spacing scale, `auto` `full` `screen` `min` `max` `fit`, fractions `1/2` `1/3` `2/3` `1/4` `2/4` `3/4` `1/5`..`4/5` `1/6`..`5/6` `1/12`..`11/12` |
| `h-` | spacing scale, `auto` `full` `screen` `min` `max` `fit`, fractions `1/2`..`5/6` (no twelfths) |
| `max-w-` | spacing scale, `0` `none` `full` `min` `max` `fit` `prose` (65ch), `screen-xs` `screen-sm` `screen-md` `screen-lg` `screen-xl` `screen-xxl` `screen-xxxl` |
| `max-h-` | spacing scale, `full` `screen` `min` `max` `fit` |
| `min-w-` | spacing scale, `0` `full` `min` `max` `fit` |
| `min-h-` | spacing scale, `0` `full` `screen` `min` `max` `fit` |
`screen` uses `100dvw`/`100dvh`. The spacing scale includes the dynamic steps
(`w-md`, `h-lg`), which grow with the viewport like the spacing does. So the Tailwind
container idiom `max-w-md` is 12 to 16px here, not a container width: use
`max-w-screen-md` or `container`. Fractions are written literally in the markup
(`class="w-1/2"`).
## Typography
| Class | Values | Responsive |
|---|---|---|
| `text-` (size, static) | `smaller` `larger` `6` `8` `10` `12` `14` `16` `18` `20` `22` `24` `26` `28` `30` `32` `34` `36` `48` `60` `72` `96` `128` | yes |
| `text-` (size, dynamic) | `xs` `sm` `md` `lg` `xl` | yes |
| `text-` (align) | `left` `center` `right` `justify` `start` `end` | yes |
| `text-` (color) | color names | no |
| `text-ellipsis` `text-clip` `text-balance` | | no |
| `font-` (family) | `sans` `serif` `mono` | no |
| `font-` (weight) | `thin` `extralight` `light` `normal` `medium` `semibold` `bold` `extrabold` `black` | no |
| font style (bare) | `italic` `non-italic` | no |
| `leading-` | `none` `tight` `snug` `normal` `relaxed` `loose` | no |
| `tracking-` | `tighter` `tight` `normal` `wide` `wider` `widest` | no |
| text transform (bare) | `uppercase` `lowercase` `capitalize` `normal-case` | no |
| decoration line (bare) | `underline` `overline` `line-through` `no-underline` | no |
| `decoration-` | color names; style `solid` `double` `dotted` `dashed` `wavy`; thickness `auto` `from-font` `0` `1` `2` `4` `8` | no |
| `underline-offset-` | `auto` `from-font` `0` `1` `2` `4` `8` | no |
| `whitespace-` | `normal` `nowrap` `pre` `pre-line` `pre-wrap` | no |
| word break | `break-normal` `break-words` `break-all` `truncate` | no |
| `align-` (vertical-align) | `baseline` `top` `middle` `bottom` `text-top` `text-bottom` `sub` `super` | no |
| `list-` | `none` `disc` `decimal` `lower-latin` `upper-latin` `lower-roman` `upper-roman` `inside` `outside` | no |
| font smoothing (bare) | `antialiased` `subpixel-antialiased` | no |
| numeric variants (bare) | `normal-nums` `ordinal` `slashed-zero` `lining-nums` `oldstyle-nums` `proportional-nums` `tabular-nums` `diagonal-fractions` `stacked-fractions` | no |
Dynamic text sizes resolve per viewport like dynamic spacing:
| token | < 576px | ≥ 576px | ≥ 1024px |
|---|---|---|---|
| xs | 10 | 12 | 14 |
| sm | 12 | 14 | 16 |
| md | 14 | 16 | 18 |
| lg | 16 | 18 | 20 |
| xl | 18 | 20 | 22 |
There is no `text-40`, `text-44`, `text-56`, `text-64` or `text-80`; the scale jumps
36 → 48 → 60 → 72 → 96 → 128.
## Colors and backgrounds (not responsive)
| Class | Values |
|---|---|
| `text-` `bg-` `border-` `decoration-` | color names (see shared scales) |
| `bg-` (size) | `auto` `cover` `contain` |
| `bg-` (position) | `top` `bottom` `left` `right` `center` `left-top` `left-bottom` `right-top` `right-bottom` |
| `bg-` (repeat) | `repeat` `no-repeat` `repeat-x` `repeat-y` `repeat-round` `repeat-space` |
| `bg-` (attachment) | `fixed` `local` `scroll` |
| `bg-clip-` | `border` `padding` `content` `text` |
| `bg-origin-` | `border` `padding` `content` |
| `bg-gradient-` | `1`..`30`, `brand` `accent` `success` `danger` `info` `warning` `gray` |
| `bg-noise-` | `1`..`5` |
| `bg-` (patterns) | `grid` `chessboard` `check` `chevron` `close` `minus` `none` |
`bg-` is overloaded: `bg-center`, `bg-cover`, `bg-fixed`, `bg-none` are not colors.
Text on a filled color takes the `-contrast` of that color, not `text-white`:
`` stays readable when the brand turns light.
On a darker shade take the contrast of that shade: `bg-warning-darken-5
text-warning-darken-5-contrast`. The contrast colors are `text-` classes only. There is
no `text-tint`: `--color-tint` has no utilities.
Text written in a color takes a readable role, not a shade: `text-brand-readable` on the
page, `text-brand-readable-strong` for a stronger one, and on a tinted surface the role of
that surface, ``. A role is
the color itself while it reads and moves only when it would not reach 4.6:1, darker in the
light theme and brighter in the dark one. They are `text-` classes only: there is no
`bg-brand-readable`, and no `text-brand-darken-1-readable`.
## Borders
| Class | Values | Responsive |
|---|---|---|
| `border` `border-0` `border-2` `border-4` `border-8` | width (bare `border` = 1px) | yes |
| `border-x-` `border-y-` `border-t-` `border-r-` `border-b-` `border-l-` | `0` `2` `4` `8`; bare side (`border-t`) = 1px | yes |
| `border-` (style) | `solid` `dashed` `dotted` `double` `hidden` `none` | yes |
| `border-` (color) | color names | no |
| `rounded` `rounded-` | `none` `sm` (bare `rounded` = .25rem) `md` `lg` `xl` `xxl` `xxxl` `full` | no |
| `rounded-t-` `rounded-b-` `rounded-tl-` `rounded-tr-` `rounded-bl-` `rounded-br-` | same radius names; bare corner (`rounded-tl`) = .25rem | no |
`border-none` sets `border-style: none`; to remove a border width use `border-0`.
The radius scale is `xxl`/`xxxl`, not `2xl`/`3xl`.
## Effects, filters, transitions (not responsive)
| Class | Values |
|---|---|
| `shadow` `shadow-` | `sm` (bare `shadow` = default) `md` `lg` `xl` `2xl` `inner` `none` |
| `opacity-` | `0` `5` `10` … `95` `100` (every multiple of 5) |
| `mix-blend-` `bg-blend-` | CSS blend mode names (`multiply` `screen` `overlay` …) |
| `blur` `blur-` | `none` `sm` (4px) `md` (12px) `lg` (16px) `xl` (24px) `xxl` (40px) `xxxl` (64px); bare `blur` = 8px |
| `backdrop-blur` `backdrop-blur-` | `none` `sm` `md` `lg` `xl` `xxl` `xxxl`; bare `backdrop-blur` = 8px (`backdrop-blur-blur` is kept as an alias) |
| `brightness-` | `0` `50` `75` `90` `95` `100` `105` `110` `125` `150` `200` `250` `500` `1000` |
| `contrast-` | `0` `50` `75` `100` `125` `150` `200` `300` |
| `saturate-` | `0` `50` `100` `150` `200` |
| `transition-` | `none` `all` `colors` `opacity` `shadow` `transform` (each sets duration 300 and ease-in-out) |
| `transition-property-` | same names, property only |
| `duration-` `delay-` | `75` `100` `150` `200` `300` `500` `700` `1000` |
| `ease-` | `linear` `in` `out` `in-out` |
Transition and duration tokens are `0s` unless the user has
`prefers-reduced-motion: no-preference`, so motion switches itself off for users
who asked for it, and the `duration-` and `delay-` classes read them. The shadow,
opacity, filter, `transition-property-` and `ease-` classes read their tokens too, with
the value as a fallback, so a token set on the page reaches them. There are no `animate-*` utilities; animation keyframes exist only
as `--animation-*` custom properties.
## Transforms (not responsive)
| Class | Values |
|---|---|
| `scale-` `scale-x-` `scale-y-` | `0` `50` `75` `90` `95` `100` `105` `110` `125` `150` |
| `rotate-` | `0` `1` `2` `3` `6` `12` `45` `90` `180` |
| `translate-` `translate-x-` `translate-y-` | `1/2` `1/3` `2/3` `1/4` `2/4` `3/4` `full` |
| `-translate-` `-translate-x-` `-translate-y-` | same, negative |
| `origin-` | `center` `top` `top-right` `top-left` `bottom` `bottom-right` `bottom-left` `right` `left` |
## Interactivity and tables (not responsive)
| Class | Values |
|---|---|
| `cursor-` | `auto` `default` `pointer` `wait` `text` `move` `help` `not-allowed` `none` `grab` `grabbing` `zoom-in` `zoom-out` `progress` `crosshair` `copy` `alias` `cell` `context-menu` `no-drop` `vertical-text` `all-scroll` `col-resize` `row-resize` `n-resize` `e-resize` `s-resize` `w-resize` `ne-resize` `nw-resize` `se-resize` `sw-resize` `ew-resize` `ns-resize` `nesw-resize` `nwse-resize` |
| `pointer-events-` | `none` `auto` |
| `user-select-` | `none` `text` `all` `auto` |
| table (bare) | `border-collapse` `border-separate` `table-auto` `table-fixed` |
---
# Components
Verified against `src/settings/components/_vv-*.scss`, `dist/components/*.css` and the
canonical examples in `docs/contents/components/`. Every class named here exists;
markup shown is the documented structure.
## Rules that apply to every component
**Naming.** Block `.vv-{name}`, element `.vv-{name}__{element}`, modifier
`.vv-{name}--{modifier}`. The `vv` prefix follows `$components-prefix`.
**Semantic children replace element classes.** Many elements carry an alias, so the
library styles the natural HTML child and the BEM class is optional. Marked with † in
the tables below. Examples: `.vv-input-text > label` (no `__label` needed),
`.vv-button > svg`, `.vv-card > header`, `.vv-dialog > article`. Elements without an
alias (`__wrapper`, `__content`, `__close`, `__hint` on `
`... see tables) need
the class. Using the alias form keeps markup lean; using the class is never wrong.
**States have three equivalent hooks.** For each state the library emits the BEM class
(`.vv-button--disabled`), a bare class (`.vv-button.disabled`) and the natural selector
(`[disabled]`, `:hover`, `[open]`, `[aria-pressed="true"]`...). Use the natural one in
real markup; the classes exist to freeze a look for docs and tests.
| state | natural selector |
|---|---|
| disabled | `[disabled]` or `[aria-disabled="true"]` |
| readonly | `[readonly]` |
| checked / indeterminate | `:checked` / `:indeterminate` |
| open / close | `[open]` / `:not([open])` |
| pressed | `[aria-pressed="true"]` |
| selected | `[aria-selected="true"]` |
| dirty | `:not(:has(*:placeholder-shown))` |
| popover | `[popover]` |
| hover, active, focus-visible, focus-within, target, empty | the pseudo-class itself |
| current (nav, tab) | **class only**: `class="vv-nav__item-label current"` |
**Form components share one pattern.** `vv-input-text`, `vv-textarea`, `vv-select`,
`vv-input-file`, `vv-input-range`: a block `` containing, in order, a direct-child
`
`, a `__wrapper` holding the control, and a direct-child ``.
On these components:
- `valid`, `invalid`, `loading`, `floating`, `icon-before`, `icon-after` are **modifier
classes** on the block. `aria-invalid` is good a11y but styles nothing.
- `disabled` propagates from the attribute on the control, through a `:has()` rule naming
that component's own control, so no class is needed (`vv-input-text--disabled` also
works). The rule is `:has(input[disabled])` for `vv-input-text`, `vv-input-file` and
`vv-input-range`, `:has(textarea[disabled])` for `vv-textarea` and
`:has(select[disabled])` for `vv-select`.
- `readonly` propagates the same way, but only where HTML has the attribute, which is a
text-type input and a textarea: `vv-input-text` and `vv-textarea`, through
`:has(input[readonly])` and `:has(textarea[readonly])`.
Everywhere else the attribute does not exist, so the read-only look is a modifier and the
recipe is the same for all of them: disable the control, add `tabindex="-1"` and
`--readonly`, which undoes the disabled dimming. It applies to `vv-select`,
`vv-input-range`, `vv-checkbox`, `vv-radio` and `vv-input-file`, whose readonly state
hides the drop area and freezes the wrapper background. `vv-input-file` does carry a
`:has(input[readonly])` hook, but `readonly` is not valid on a file input, so reach it
through the modifier rather than writing the attribute.
A disabled control contributes nothing to form submission, so pair it with a hidden input
carrying the same name when the value has to reach the server.
- Their look comes from the shared `--input-*` tokens (`--input-background-color`,
`--input-color`, `--input-min-height`, `--input-label-*`, `--input-hint-*`,
`--input-valid-color`, `--input-invalid-color`); override those to restyle every field
at once.
## vv-button
Only `a[href]`, `button`, `input[type=button|submit|reset]` receive the block styles; a
`