# NuvoUI LLM Guide v2 **For @nuvoui/core 2.x. For 1.x use nuvoui-llm-guide.md.** **Purpose**: Drop this file into a project so an AI assistant writes correct NuvoUI 2.x code. **Source of truth**: `core/src/styles` (SCSS). When this guide and the source disagree, the source wins. **Companion docs in the repository**: `core/docs/TOKENS.md` (every token and value, generated), `core/docs/MIGRATION-v2.md` (1.x to 2.x), `core/CHANGELOG.md`. --- ## Table of Contents 1. [What NuvoUI Is](#what-nuvoui-is) 2. [Install and Configure](#install-and-configure) 3. [The Token System](#the-token-system) 4. [token() vs get-color() vs var()](#token-vs-get-color-vs-var) 5. [Theming](#theming) 6. [The Law: Syntax Rules](#the-law-syntax-rules) 7. [Utility Classes](#utility-classes) 8. [Complete Mixin Reference](#complete-mixin-reference) 9. [Configuration Maps and Defaults](#configuration-maps-and-defaults) 10. [Validation and Errors](#validation-and-errors) 11. [Anti-Patterns](#anti-patterns) 12. [Anti-Hallucination Table](#anti-hallucination-table) 13. [Common Patterns](#common-patterns) --- ## What NuvoUI Is NuvoUI core is a **human-readable, utility-first SCSS framework** and the foundation layer of the NuvoUI design system. ### Philosophy 1. **Human-readable names**: `between` not `justify-between`, `x-center` not `items-center`, `z-modal` not `z-50`, `bg-surface` not `bg-white`. 2. **SCSS only, no JavaScript**: real Sass modules (`@use`), compiled by Dart Sass. No runtime, no HTML scanning, no PostCSS plugin. 3. **`apply()` is the core feature**: every utility is also a mixin, called through `@include apply(...)` with state and breakpoint modifiers. 4. **Design system foundations**: colours, spacing, radii, type, shadows, z-index and motion are design tokens emitted as `--nu-*` CSS custom properties. Classes and mixins output those tokens, so everything follows the active theme. 5. **Config-driven**: every utility class is generated from a configuration map. Custom values are written in SCSS (`p(1.5rem)`), never in HTML. ### Package layers - `@nuvoui/core`: foundations (this guide). SCSS only. - Component packages build on core and use component variables named with `__` or `--` (see [Naming grammar](#naming-grammar)). ### Three ways to style ```html
Item
``` ```scss @use "@nuvoui/core" as *; // 2. apply(): classes as mixins, plus custom values, states and breakpoints .card { @include apply(flex, between, x-center, p(4), bg(surface), text-color(regular), rounded(md), shadow(md) hover, p(6) lg); } ``` ```scss @use "@nuvoui/core" as nuvo; // 3. Plain SCSS with tokens and functions .card { padding: nuvo.token(space-4); background: nuvo.token(bg-surface); @include nuvo.media-up(md) { padding: nuvo.token(space-6); } } ``` --- ## Install and Configure Requires Dart Sass `>=1.79.0 <2` (peer dependency). Without a bundler that reads the package `sass` export, load it as `@use "pkg:@nuvoui/core"` with the Node package importer. ```html ``` ### SCSS entry and `with()` `@use "@nuvoui/core"` resolves to `src/styles/index.scss`. Configure once, in the first `@use` of the module, with `with()`. ```scss @use "@nuvoui/core" as * with ( $brand: #4361ee, $generate-utility-classes: true, $breakpoints: ( "sm": 640px, "md": 768px, "lg": 1024px, ) ); ``` What the entry emits: | Output | When | | --- | --- | | `@layer reset, components` (reset, tooltips) | Always (tooltips follow the `tooltips` flag alone, independent of `$generate-utility-classes`) | | `@layer base` (base element styles, `.sr-only`) | `base` feature flag on (default), independent of `$generate-utility-classes` | | `@property` registrations (transform variables, `--nu-flex-gap`) | Always, independent of feature flags | | Tokens (`:root` fixed block, light block, dark block, inverse blocks, dark fallback, high-contrast twins when set) | `theme` feature flag on (default), independent of `$generate-utility-classes` | | Utility classes | `$generate-utility-classes: true` (default `false` in `index.scss`) and the feature flag on | Mixin-only projects keep `$generate-utility-classes: false` and still get every token. Every compiled entry that `@use`s the package emits all of the above except the classes (about 42 KB compressed). Compile one entry that imports your partials, not one stylesheet per component. Base layer (`base` flag): `body` sets `bg-base` and `text-regular`; buttons (`button`, `input` of type `submit`, `button`, `reset`) and fields (`input` without a type, text-like types, `textarea`, `select`) are styled from tokens with a `:focus-visible` ring. A `select` with `multiple` or a `size` other than 1 keeps its natural height; a single-line `select` (`size="1"` included) takes `--nu-control-height-md`. Invalid fields use `--nu-danger` (`--nu-border-control` when no `danger` role exists). Removing a key the base layer reads (`$spacings` 1, 2, 4, `$font-sizes` `sm`, `$control-heights` `md` and others) raises an error naming the map. ### Cascade layers Reset, base and components live in `@layer reset, base, components`. Utility classes and your own CSS are unlayered, so they always beat base styles without `!important`. ### Direct access ```scss @use "@nuvoui/core" as nuvo; @use "sass:map"; @use "sass:color"; .footer { padding: map.get(nuvo.$spacings, 4); // 1rem (raw map value) border-color: color.mix(nuvo.$brand, #fff, 50%); // Sass maths on the configured colour color: nuvo.token(text-muted); // var(--nu-text-muted) } ``` Exported colour variables: `$brand`, `$success`, `$danger`, `$warning`, `$info`. Maps: `$spacings`, `$percentages`, `$opacities`, `$breakpoints`, `$font-sizes`, `$font-weights`, `$line-heights`, `$border-widths`, `$border-radii`, `$shadow-sizes`, `$shadow-colors`, `$z-indexes`, `$z-index-levels`, `$durations`, `$delays`, `$timing-functions`, `$control-heights`, `$control-padding-inline`, `$color-primitives`, `$basic-colors`, `$semantic-colors`, `$shade-stops`, `$position-fractions`, `$grid-item-sizes`, `$container-names`. Not public (Undefined variable): `$primary`, `$secondary`, `$default-color`, `$theme-tokens`, `$color-names`, `$color-names-with-basic`, `$colors`, `$colors-primitives`, `$color-cache`, `$digit-map`, `$translate-values`, `$negative-translate-values`, `$high-contrast-theme`. --- ## The Token System Every token is a CSS custom property `--nu-{name}`. Full list with values: `core/docs/TOKENS.md`. ### Tiers | Tier | Examples | Changes per theme? | | --- | --- | --- | | **Palette** | `--nu-red-100`, `--nu-brand-700` | No. Fixed shades, stops 50, 100 to 900, 950 | | **Basic** | `--nu-black`, `--nu-white`, `--nu-transparent` | No | | **Role** | `--nu-brand`, `--nu-danger-subtle`, `--nu-text-red` | Yes. Points at a different stop of its own scale | | **Theme** | `--nu-bg-surface`, `--nu-text-muted`, `--nu-border-regular` | Yes | | **Scale** | `--nu-space-4`, `--nu-radius-md`, `--nu-z-modal`, `--nu-font-family` | No | Palette hues are plain colours with no meaning. Roles (brand, danger, ...) carry meaning and point at palette stops. Brand is a role, not a hue. Fixed tiers (palette, basic, scale, `--nu-font-family`) are declared once in a `:root`-only block. The light block (`:root, [data-theme="light"]`) holds theme and role tokens only, so a runtime `:root { --nu-font-family: ... }` override also applies inside `data-theme="light"` sections. ### Families | Family | Tokens | | --- | --- | | Surfaces | `bg-base` (page), `bg-surface` (cards, drawers), `bg-alternate` (table headers, sidebars, kanban columns), `bg-raised` (menus, popovers, toasts), `bg-hover`, `bg-selected`, `bg-disabled` | | Text | `text-regular`, `text-muted`, `text-subtle`, `text-inverted`, `text-link`, `text-link-hover` | | Borders | `border-regular`, `border-strong`, `border-control` (form control edges, 3:1), `border-control-hover` | | Focus | `border-focus` (colour, applied as `outline-color`), `border-focus-width`, `border-focus-offset` | | Brand | `brand` (the fill), `brand-hover`, `brand-active`, `brand-subtle`, `brand-border`, `text-brand`, `text-on-brand` | | Statuses | same set for `success`, `danger`, `warning`, `info` (`danger`, `danger-hover`, ...) | | Error aliases | `error`, `error-hover`, `error-active`, `error-subtle`, `error-border`, `text-error`, `text-on-error`: always the matching danger token (`--nu-error` is `var(--nu-danger)`) | | Hue roles | `{hue}-subtle`, `{hue}-border`, `text-{hue}` for gray, red, blue, green, yellow, purple, pink | | Palette | `{name}-{stop}` for every role and hue | | Scales | `space-*`, `radius-*`, `border-width-*`, `font-family` (one token), `font-size-*`, `font-weight-*`, `line-height-*`, `shadow-*`, `opacity-*`, `z-*`, `duration-*`, `ease-*`, `control-height-{sm,md,lg}`, `control-padding-inline-{sm,md,lg}` | There are no delay tokens and no disabled text token (disabled text uses opacity). `--nu-font-family` is emitted from `$font-family`; base elements read it (the raw Sass value when the `theme` flag is off). ### Naming grammar - **Prefix**: `--nu-` then single hyphens only: `--nu-text-muted`, `--nu-brand-hover`. - **Component variables** always contain `__` or `--` after the component name: `--nu-btn--BackgroundColor`, `--nu-sidebar__link--Color`. They can never clash with core tokens. Core's tooltip uses this grammar: `--nu-tooltip--Background`, `--nu-tooltip--Color`, `--nu-tooltip--ZIndex`. - **Internal utility variables** also carry `--nu-` but are not tokens (`token()` rejects them): `--nu-border-style`, `--nu-flex-gap`, `--nu-translate-x`, `--nu-scale-x`, `--nu-rotate`, `--nu-skew-x`, `--nu-backdrop-blur` and so on. The transform variables and `--nu-flex-gap` are registered with `@property` (`inherits: false`), so they never leak into descendants, and they are read without `var()` fallbacks: an unset one takes its registered initial value. The registrations are emitted whenever the package loads, independent of feature flags, so the mixins work with the utility classes off. - **Classes stay short**: `text-muted`, `bg-danger`. Only the variables carry `--nu-`. - **Role state ladder**: `--nu-{role}` (the bare name is the resting fill: `--nu-brand`, `--nu-danger`, `--nu-error`), `-hover`, `-active`, plus `-subtle` (tinted background) and `-border` (soft edge). There is no `--nu-{role}-default`: `token(brand-default)` and a `"brand-default"` theme key raise a rename error pointing at `brand`. - **`text-{role}` vs `text-on-{role}`**: - `text-{role}` = role-coloured text on a normal page (`--nu-text-danger` for an error message). - `text-on-{role}` = text placed on the role's solid fill (`--nu-text-on-brand` on a `--nu-brand` button). - Built-in `text-on-*` exists only for roles (brand, success, danger, warning, info, error). There is no `text-on-{hue}`. - **Word meanings**: - `regular` = the normal variant of text and border (`text-regular`, `border-regular`). - `base` = the bottom layer of the surface stack (`bg-base`, the page). - `default` is not a token word. The resting fill is the bare role name. - Past participles for states: `inverted`, `disabled`. - **Decimal keys**: `.` becomes `_` in tokens (`--nu-space-0_5`) and is escaped in classes (`.p-0\.5`). --- ## token() vs get-color() vs var() | Tool | Returns | Follows theme | Use for | | --- | --- | --- | --- | | `token(name)` | `var(--nu-name)` | Yes | Any token in plain SCSS. Validated at compile time | | `get-color(name, stop)` | A real colour (`#0058bb`) | No | Sass maths (`color.mix`, `color.adjust`), compile-time contrast | | `var(--nu-name)` written by hand | Itself | Yes | Allowed, but not validated: a typo fails silently in the browser | | Mixins (`bg(brand)`) | `var(--nu-...)` | Yes | Normal styling through `apply()` | ```scss @use "@nuvoui/core" as nuvo; @use "sass:color"; .notice { background: nuvo.token(warning-subtle); border-inline-start: nuvo.token(border-width-4) solid nuvo.token(warning); color: nuvo.token(text-regular); } .legacy-email-button { background: nuvo.get-color(brand, 700); // fixed colour for email clients without custom properties border-color: color.adjust(nuvo.get-color(brand, 700), $lightness: -10%); } ``` `token()` rules: pass the name without `--nu-`; unknown names raise an error listing close matches; an old `{role}-default` name raises `token(brand-default): renamed in 2.0, use token(brand)`; `token()` raises an error when the `theme` feature flag is off. Internal variables (`--nu-flex-gap`) and component variables (`--nu-tooltip--Color`) are not tokens. `get-color()` rules: role and palette names first (`get-color(red)` is palette red `#ef4444`, not CSS `red`), then any CSS colour or named colour; stops 50, 100 to 900, 950 only; never returns `var()`; errors on unknown names. --- ## Theming ### Light, dark, nested - Fixed tokens: `:root` only. Light theme tokens: `:root, [data-theme="light"]`. - Dark tokens: `[data-theme="dark"]`, and `:root:not([data-theme])` inside `@media (prefers-color-scheme: dark)`. Omit `data-theme` on the root to follow the OS. - Palette shades never change; role and theme tokens are redefined in dark. - Set `data-theme` on any element to re-theme its subtree. - `data-theme="inverse"`: always the opposite of the page. Dark values on a light page (`[data-theme="inverse"]` shares the dark block); light values under `[data-theme="dark"]` (`[data-theme="dark"] [data-theme="inverse"]`), under the OS fallback (`:root:not([data-theme]) [data-theme="inverse"]` in the dark media query) and inside another inverse section (`[data-theme="inverse"] [data-theme="inverse"]` flips back: light values, or dark values when the outer inverse section has a `[data-theme="dark"]` ancestor or the OS fallback root; a light or dark section between the two inverse sections is not taken into account). One level of nesting is supported; deeper inverse nesting is not. Full theme token set, pure CSS, not emitted when `$enable-dark-mode` is false. Limit: it resolves against the nearest `[data-theme="dark"]` ancestor or the page, so inside a `[data-theme="light"]` section of a dark page it still gives light values; use `data-theme="dark"` there. Replaces the v1 `inverted-*` layer. ```html ``` `$enable-dark-mode: false` removes the dark block and the fallback. ### High contrast `$high-contrast-light` and `$high-contrast-dark` (both empty by default, nothing emitted until set) apply under `[data-contrast="high"]` on the root, on an element that sets `data-theme`, or on an ancestor of either. The light map applies wherever the theme resolves to light and the dark map wherever it resolves to dark, inverse and nested inverse sections and the OS fallback included. Every theme selector gets a twin compounded with `:is([data-contrast="high"], [data-contrast="high"] *)` (one attribute more specificity), emitted after the theme blocks in the same order, so the winning twin is always the twin of the winning theme block. The light-valued and dark-valued twins carry the same keys: both maps' keys, the `{role}` fill for every `{role}` or `text-on-{role}` either map sets, every `{role}` fill when either map sets `text-regular` or `text-inverted` (text-on is recomputed from the new text colours), and a `text-on-{role}` that `$light-theme` or `$dark-theme` sets explicitly (kept, never recomputed). When a high-contrast map changes a `{role}` fill whose `text-on-{role}` is set explicitly in the base theme, set `text-on-{role}` in the high-contrast map too. A key its own map lacks keeps the base theme value, so a key set in one map never leaks into the other theme. An element without `data-theme` inherits; its own `data-contrast` does not restyle it. `$high-contrast-dark` is ignored when `$enable-dark-mode` is false. `$high-contrast-theme` no longer exists. ```scss @use "@nuvoui/core" as * with ( $high-contrast-light: ("text-muted": #000, "border-control": gray 900), $high-contrast-dark: ("text-muted": #fff) ); ``` ```html ``` ### Override maps and the `name stop` syntax `$light-theme`, `$dark-theme`, `$high-contrast-light` and `$high-contrast-dark` hold only the tokens you change; they merge over the built-in defaults. Keys are token names without `--nu-`. A value is any CSS value, or a scale reference: a role or palette name, a space, and a stop. `brand 800` is emitted as `var(--nu-brand-800)`. Keys may be unquoted, even words Sass reads as colours (`tan`); quoted colours (`"#111"`) work like unquoted ones, and a quoted `"#ggg"` raises an error. All bad values are reported in one error. A new custom key goes in both `$light-theme` and `$dark-theme` (only `$light-theme` when dark mode is off). ```scss @use "@nuvoui/core" as * with ( $brand: #1ab5a5, $light-theme: ( "brand": brand 800, "bg-base": #fff, "bg-card": gray 50, ), $dark-theme: ( "bg-card": #202020, ) ); ``` Rules: - A new key (here `bg-card`) must be in both `$light-theme` and `$dark-theme`. A new `bg-*`, `text-*` or `border-*` colour token gets a utility class (`.bg-card`) and works in mixins (`bg(card)`). - Typos, malformed references (`brand600`, `brand-600`, `brand 450`), quoted invalid hex (`"#ggg"`) and unknown scales raise errors. - An old `{role}-default` key (`brand-default`, `error-default`) raises an error naming the bare key (`brand`). There is no fallback. - A new key must not reuse a palette, basic colour or scale token name (`red-500`, `white`, `space-4`, `opacity-50`): the reserved-name error is raised even when the key is in one map only. Role and hue names must not collide: a name in both `$semantic-colors` and `$color-primitives`, the name `error`, a basic colour name (`black`), or a name whose generated tokens equal a built-in token (`link` would generate `text-link`) or a scale token (`opacity` would generate `opacity-50`) raises an error. Colour map keys may be quoted or unquoted. - Extra roles and hues work: a role added to `$semantic-colors` (`"accent": #8b5cf6`) gets a palette, the full role ladder, `text-on-accent` and classes; a hue added to `$color-primitives` (`"teal": #14b8a6`) gets a palette, hue roles and classes. - `error`, `error-*`, `text-error` and `text-on-error` cannot be set; override `danger`, `danger-*`. - `text-on-{role}` is computed from the `{role}` fill in light and dark alike (higher WCAG contrast of that theme's own `text-regular` and `text-inverted`; high-contrast blocks use the theme merged with their map). A computed value below 4.5:1 emits a warning naming the map to fix (point the fill at another stop, or set `text-on-{role}`). Set it explicitly to override; an explicit value also applies under high contrast unless the high-contrast map sets its own. An explicit `text-on-{role}` gets only the plain `.text-on-{role}` class, like a computed one. - A custom `text-on-{name}` key for a name that is not a role (`"text-on-panel"`, in both maps) is an ordinary custom text token: it gets `.text-on-panel` with `hover:` and `group-hover:` forms, and `text-color(on-panel)`. - A quoted `hsl()`, `hwb()` or `oklch()` value for a `{role}` fill, `text-regular` or `text-inverted` cannot be used to compute `text-on-{role}` and raises an error; write it unquoted or set the `text-on-{role}` tokens. Default role stops (every role and hue; full values in `core/docs/TOKENS.md`): | State | Light | Dark | | --- | --- | --- | | `{role}` fill | 700 | 400 | | `-hover` | 800 | 300 | | `-active` | 900 | 200 | | `-subtle` | 50 | 950 | | `-border` | 200 | 800 | | `text-{name}` | 700 | 300 | Light exceptions: `warning` fill 500, hover 600, active 700 (it takes dark `text-on-warning`); `text-success`, `text-warning`, `text-info`, `text-green` and `text-yellow` use 800. Light `text-warning` is 5.53:1 on white but 4.42:1 on gray 100 (`bg-alternate`, `bg-hover`), an accepted exception. Write theme overrides as `name stop` references, never copied hex values. ### Theme mixins - `prefers-dark { ... }`: `@media (prefers-color-scheme: dark)`. - `dark-mode { ... }`: applies exactly where the dark tokens resolve (on and under `[data-theme="dark"]`, an inverse section that resolves dark, and `:root:not([data-theme])` when the OS is dark), stopping at a nested `data-theme` section that resolves light. Emits CSS `@scope` (Chrome 118, Safari 17.4, Firefox 146; browsers without `@scope` ignore it). Inside a rule it matches that selector on a dark context element or inside one (`.logo:scope, .logo:is(:scope *)`); at the top level it applies to the dark context elements. Works in pseudo-element rules (`&::before`, `::placeholder`): the pseudo-element stays last. Emits nothing when `$enable-dark-mode` is false. - `dark-theme`: emits the dark token declarations (use inside your own selector). - `high-contrast { ... }`: `@media (forced-colors: active)`. This is the OS forced-colours mode, unrelated to `data-contrast`. ```scss @use "@nuvoui/core" as *; .logo { @include dark-mode { filter: invert(1); } &::before { @include dark-mode { opacity: 0.8; } } } .print-preview { @include dark-theme; } ``` --- ## The Law: Syntax Rules ### 1. Utility class format ``` property-value@breakpoint hover:property-value@breakpoint group-hover:property-value ``` Examples: `bg-brand`, `p-4@md`, `hover:bg-brand-subtle`, `text-muted`, `focus:border-brand`. State prefixes generated: `hover:` and `group-hover:` for colours (`bg`, `text`, `border`), border widths, width and height, opacity, transforms (translate, rotate, scale, origin), plus `hover:` for shadows; `active:` and `focus:` for border colours, opacity and shadows. Spacing, display and layout classes have no state prefix: use `apply()` with a state. ### 2. `apply()` format ```scss @use "@nuvoui/core" as *; .x { @include apply( flex, p(4), rounded(md), bg(brand, 100), mt(4) sm, bg(brand-subtle) hover, shadow(lg) hover lg ); } ``` Each item: a mixin name, optionally with arguments, then at most one state and one breakpoint, separated by spaces. Items are separated by commas. States: `hover`, `focus`, `focus-visible`, `focus-within`, `active`, `visited`, `disabled`, `enabled`, `checked`, `required`, `invalid`, `valid`, `placeholder-shown`, `read-only`, `first-child`, `last-child`, `only-child`, `first-of-type`, `last-of-type`, `empty`, `target`, `group-hover` (emits `.group:hover &`). An unknown modifier, or a second state or breakpoint, raises an error. Namespaced loads work the same: with `@use "@nuvoui/core" as nuvo;`, write `@include nuvo.apply(p(4), font-size(12px), duration(250ms));`. The items reach `apply()` as text; numeric arguments become real numbers, a quoted argument is read as the value it spells (`font-size("1.25rem")` gives `1.25rem`, as with `apply()` and the direct call) and CSS functions such as `clamp()` are accepted, so the namespaced form takes the inputs of `apply()` with `as *` and of the direct `@include`, with three exceptions set by Sass: a map argument cannot be written into a text item (pass the item as a call, `nuvo.apply(nuvo.animate-spin((duration: 2s)))`), `opacity(...)` with anything but a number (`opacity(inherit)`, `opacity("50")`) resolves to the Sass colour function, so write `nuvo.apply(nuvo.opacity(inherit))` (or `opacity(50)` for a key), and a colour function other than `rgb()` or `rgba()` with a shade (`bg(oklch(...), 600)`) raises an error because only a direct call or `as *` passes it as a colour. Items of keys and words are cached; items with raw values (`p(13px)`) go in size-capped caches, so repeated raw values are parsed once and compile time stays linear in the number of unique values. ### 3. Modifier order (critical) Modifiers always come **after** the utility. State first, breakpoint last. ```scss @use "@nuvoui/core" as *; .ok { @include apply(bg(gray, 100) hover lg); } ``` ```scss // Raises an error .bad { @include apply(hover:bg(gray, 100)); } .bad { @include apply(text(white)); } ``` Never write `hover:` or a breakpoint before the utility inside `apply()`. Never write `lg:` anywhere. ### 4. Breakpoints `xs` 480px, `sm` 640px, `md` 768px, `lg` 1024px, `xl` 1280px, `2xl` 1536px. Mobile first: a breakpoint means "this width and up" (`min-width`). ```html
...
``` ```scss @use "@nuvoui/core" as *; .layout { @include apply(flex, col, row md); } ``` ### 5. Numbers are keys A unitless number in a scale mixin is a **configuration key**, never a raw value. `p(4)` is spacing key 4 = `var(--nu-space-4)` = 1rem, the same as `.p-4`. A unitless number that is not a key raises an error. Write a unit for any custom value. ```scss @use "@nuvoui/core" as *; .box { @include apply(p(4), m(-2), width(300px), height(50vh), rounded(12px), border(3px)); } ``` ### 6. Colour words match class names `bg(surface)` matches `.bg-surface`; `text-color(muted)` matches `.text-muted`; `border-color(regular)` matches `.border-regular`. Never repeat the family: `bg(bg-surface)` is an error. ### 7. Class to mixin conversion | Class | Mixin | | --- | --- | | `p-4` | `p(4)` | | `mt-4@sm` | `mt(4) sm` | | `bg-brand` | `bg(brand)` | | `bg-brand-100` | `bg(brand, 100)` | | `bg-danger-subtle` | `bg(danger-subtle)` | | `text-muted` | `text-color(muted)` | | `text-on-brand` | `text-color(on-brand)` or `text-on-color(brand)` | | `border-regular` | `border-color(regular)` | | `hover:opacity-75@sm` | `opacity(75) hover sm` | | `rounded-lg` | `rounded(lg)` | | `shadow-md-dark` | `shadow(md, dark)` | | `z-modal` | `z-modal` or `z-index(modal)` | | `group-hover:bg-blue` | `bg(blue) group-hover` (emits `.group:hover &`) | --- ## Utility Classes Generated only with `$generate-utility-classes: true`. Responsive variants use `@{breakpoint}` (`.p-4@md`). ### Colour classes | Pattern | Value | | --- | --- | | `.bg-{role}`, `.border-{role}` | `--nu-{role}` (flips) | | `.text-{role}` | `--nu-text-{role}` (flips) | | `.text-on-{role}` | `--nu-text-on-{role}` | | `.bg-{role}-subtle`, `.bg-{hue}-subtle` | `--nu-{name}-subtle` | | `.bg-{hue}`, `.border-{hue}` | `--nu-{hue}-500` (fixed) | | `.text-{hue}` | `--nu-text-{hue}` (flips) | | `.bg-{name}-{stop}`, `.text-{name}-{stop}`, `.border-{name}-{stop}` | `--nu-{name}-{stop}` (fixed), stops 50 to 950 | | `.bg-error`, `.text-error`, `.border-error`, `.bg-error-subtle`, `.text-on-error` | danger aliases | | `.bg-{word}` | base, surface, alternate, raised, hover, selected, disabled | | `.text-{word}` | regular, muted, subtle, inverted, link, link-hover | | `.border-{word}` | regular, strong, control, control-hover | | `.bg-black`, `.text-white`, `.border-transparent` ... | basic colours | Roles: brand, success, danger, warning, info. Hues: gray, red, blue, green, yellow, purple, pink. No class exists for role state words (`-hover`, `-active`, `-border`): use mixins or `token()`. Colour classes (except `.text-on-{role}`) also have `hover:` and `group-hover:` forms; border colour classes add `active:` and `focus:`. ### Layout classes are compound Flex modifiers only apply on a `.flex` element (`.flex.between`, `.flex.col`, `.flex.row@md`), flex item classes only on its direct children (`.flex > .grow`, `.flex > .fill-6`), and grid templates only on a `.grid` element (`.grid.cols-3`). Write `class="flex between x-center"`, never `between` alone. Inline layouts are `flex inline` and `grid inline`: there is no `.inline-flex` or `.inline-grid` class (the mixins exist). `.col-span-*` and `.row-span-*` are standalone. A parent made flex with `@include apply(flex)` on a semantic class does not enable the child classes: add the `flex` class to it, or use the child mixins (`grow`, `no-shrink`, `fill(n)`) in SCSS. ### Logical (RTL) classes `ps-*`, `pe-*`, `ms-*`, `me-*`, `ms-auto`, `me-auto`, `start-*`, `end-*`, `border-s`, `border-e`, `border-s-{w}`, `border-e-{w}`, `rounded-s-*`, `rounded-e-*`, `rounded-ss-*`, `rounded-se-*`, `rounded-es-*`, `rounded-ee-*`, `text-start`, `text-end`. `px-*`, `py-*`, `mx-*`, `my-*` and `space-x-*` are also logical. Physical classes (`pl-*`, `mr-*`, `left-*`, `text-left`) stay for content that must keep its side in both directions. --- ## Complete Mixin Reference All mixins below work inside `apply()` unless marked **standalone**. (L) marks logical, direction-aware mixins. Arguments in `[]` are optional. ### Display and visibility `block`, `inline`, `inline-block`, `flex`, `inline-flex`, `grid`, `inline-grid`, `table`, `table-row`, `table-cell`, `contents`, `hide` (display none), `show` (display revert: the element's natural display), `visible`, `invisible`, `collapse` Overflow: `overflow-hidden`, `overflow-visible`, `overflow-scroll`, `overflow-auto`, `overflow-x-hidden`, `overflow-y-hidden`, `overflow-x-scroll`, `overflow-y-scroll`, `overflow-x-auto`, `overflow-y-auto` Removed in 2.0 (unknown mixin errors): `flex-inline` (use `inline-flex`), `grid-inline` (use `inline-grid`), `d-tbl`, `d-tbl-row`, `d-tbl-cell` (use `table`, `table-row`, `table-cell`), `text-size()` (use `font-size()`). `.flex` and `.grid` follow the `flex` and `grid` flags, not `displays`. ### Flexbox - Direction: `row`, `row-reverse`, `col`, `col-reverse` - Wrap: `wrap`, `nowrap`, `wrap-reverse` - Main axis (justify-content): `start`, `end`, `center`, `between`, `around`, `evenly` - `stretch`: direct children share the main axis equally (`& > * { flex: 1 1 0% }`, class `.flex.stretch`), no argument; a child with `fill-{n}`, `grow` or `no-grow` keeps its own flex value, at every breakpoint (the class rule is `.flex:where(.stretch) > *`, one class of specificity) - Cross axis (align-items): `x-start`, `x-end`, `x-center`, `x-stretch`, `x-baseline` - Content (align-content): `x-content-start`, `x-content-end`, `x-content-center`, `x-content-between`, `x-content-around`, `x-content-evenly`, `x-content-stretch` - Self: `self-auto`, `self-start`, `self-end`, `self-center`, `self-stretch`, `self-baseline` - Grow and shrink: `grow`, `no-grow`, `grow-only`, `shrink`, `no-shrink`, `shrink-twice`, `fill-full`, `fill-auto`, `fill($size)` Axis rule: no prefix = main axis; `x-` = cross axis. `x` does not mean horizontal. With `col`, the axes swap. ### Grid - Template: `cols($count)`, `rows($count)` (a whole number of 1 or more, `calc()` or `var()`), `cols-custom($template)` (`cols-custom(2fr 1fr)` or `cols-custom("2fr_1fr")`, `_` works as a space outside brackets and parentheses, so `"[main_start]_1fr"` keeps the line name), `auto-fit($min-width)`, `auto-fill($min-width)` (a `$grid-item-sizes` key, `auto-fit(sm)` is `minmax(250px, 1fr)` as `.auto-fit-sm`, a length or percentage, `calc()` or `var()`, or a whole `minmax()` track used as written (`auto-fill(minmax(100px, 20%))`); a unitless number such as 4 raises an error) - Flow: `flow-row`, `flow-col`, `flow-dense` (same names as the `.flow-*` classes) - Alignment (grid only: justify-items, align-items, place-items): `justify($value)`, `align($value)`, `place($value)` with `start`, `end`, `center`, `stretch` - Items: `col-span($n)`, `row-span($n)`, `col-start($n)`, `col-end($n)`, `row-start($n)`, `row-end($n)`, `grid-place-at($col, $row)`, `grid-col-at($col)`, `grid-row-at($row)`. A number must be a whole line number: an unquoted `1 / 3` is Sass division (0.333) and raises an error; write `"1 / 3"` or `list.slash(1, 3)`. ### Container `container-base` (full width, `margin-inline: auto`), `container([$display])` (responsive max widths, breakpoint minus 100px, `calc(40em - 100px)` for em or rem breakpoints; `block`, `flex` or `grid`), `container-padding($padding)` (block padding; a spacing key gives its token like `py()`: `container-padding(4)` is `var(--nu-space-4)`, `container-padding(1rem)` stays raw) ### Spacing Values: a `$spacings` key gives `var(--nu-space-{key})`; values with units, `calc()`, `var()` and the CSS keywords valid for the property (`auto` for margin and sizing, `normal` for gap) pass through; `0` is allowed. Any other word raises an error. - Padding: `p`, `px` (L), `py` (L), `pt`, `pr`, `pb`, `pl`, `ps` (L), `pe` (L). No negative keys or negative raw lengths (`p(-1rem)`), no `auto`. - Margin: `m`, `mx` (L), `my` (L), `mt`, `mr`, `mb`, `ml`, `ms` (L), `me` (L). Negative keys allowed: `m(-4)` gives `calc(-1 * var(--nu-space-4))`. - Auto margins: `ml-auto`, `mr-auto`, `ms-auto` (L), `me-auto` (L), `mx-auto` (L, `margin-inline: auto`). The classes come after the `.m-*` classes of the same breakpoint, so `mx-auto m-4` keeps the centring. - Gap: `gap`, `gap-x`, `gap-y`. No negative keys; `gap(normal)` is allowed. `gap` and `gap-x` also set `--nu-flex-gap` on direct children (`& > *`), which `fill($size)` reads, so a nested flex container does not inherit an outer gap. - Between children: `space-x` (L, `margin-inline-start`), `space-y` (L, `margin-block-start`). Negative keys allowed. - Inset: `inset`, `inset-x`, `inset-y`, `inset-auto`, `inset-x-auto`, `inset-y-auto`. Negative keys allowed. `inset-x()` / `.inset-x-*` and `.border-x` are physical (left and right), unlike `mx`, `px` and `space-x`, which are logical; use `inset-start()` / `inset-end()` where the direction matters. ### Sizing `width`, `height`, `min-width`, `min-height`, `max-width`, `max-height`: spacing keys, percentage keys (`width(50p)` is `50%`, `width(33p)` is `33.333%`), breakpoint names for the width mixins only (`width(lg)` is `1024px`, as `.w-lg`), or raw values with a CSS length unit (`width(4)` is `var(--nu-space-4)`; `width(300px)`, `width(50%)`, `width(auto)`, `height(fit-content)` pass through). No negative keys. A number followed by other text (`width(3xl)`) raises an error. Percent: `width-percent($n)`, `height-percent($n)`, `min-width-percent($n)`, `min-height-percent($n)`, `max-width-percent($n)`, `max-height-percent($n)` (`width-percent(50)` is `50%`; the keys 33 and 66 are exact thirds, `33.333%` and `66.667%`, as are `.w-33p` and `.w-66p`; `width-percent(33%)` stays `33%`). Percentage width and height classes have `hover:` and `group-hover:` forms with and without a breakpoint (`.hover:w-50p`, `.hover:w-50p@md`). Keywords: `w-auto`, `w-full`, `w-screen`, `w-min`, `w-max`, `w-fit`, `h-auto`, `h-full`, `h-screen`, `h-min`, `h-max`, `h-fit`, `min-w-full`, `max-w-full`, `min-h-full`, `max-h-full` ### Position - Type: `static`, `relative`, `absolute`, `fixed`, `sticky` (also sets `top: 0` and `z-index: 999`, not the `z-sticky` token; the `.sticky` class puts both inside `:where()`, so `.top-*` and `.z-*` override them), `absolute-center` - Offsets: `top`, `right`, `bottom`, `left`, `inset-start` (L), `inset-end` (L). Spacing keys (negative allowed), `$position-fractions` keys `25p`, `33p`, `50p`, `66p`, `75p`, `100p` (and `-25p`; `right(25p)` is `25%`, as `.right-25p`), or raw values (`top(50%)`). The inset mixins take the same values. A number followed by `p` that is not a fraction key raises an error. `$position-fractions` is configurable (`!default`). ### Colour - `bg($color, [$stop])`: role (`brand`), role state (`danger-hover`, `brand-subtle`), hue (`red`, fixed 500), theme word (`surface`, `alternate`, `raised`, ...), name with stop (`bg(red, 300)`), error alias (`error`), basic (`white`), raw colour (hex, CSS colour name, or a colour function: `rgb()`, `hsl()`, `hwb()`, `lab()`, `lch()`, `oklab()`, `oklch()`, `color()`, `color-mix()`, `light-dark()`) or `var()`. Without a stop a colour function passes through on every path; with a stop the value must be one Sass reads as a colour (hex, `rgb()`, a CSS colour name, or a colour function in a direct call or `as *` load), so `color-mix()` and `light-dark()` take no stop. With the `theme` flag off, colour names raise an error saying so (no tokens are emitted); pass a colour value. In a namespaced load Sass evaluates `hwb(0 0% 0%)` or `rgb(255, 0, 0)` first, and a result that prints as a palette name (`red`) resolves as the palette colour. - `text-color($color, [$stop])`: role (`brand` gives `--nu-text-brand`), hue (`red` gives `--nu-text-red`, flips), theme word (`regular`, `muted`, `subtle`, `inverted`, `link`, `link-hover`), `on-{role}`, name with stop, raw colour, CSS keyword (`inherit`, `currentcolor`). - `border-color($color, [$stop])`: role, hue, theme word (`regular`, `strong`, `control`, `control-hover`), name with stop, raw colour. - `text-on-color($color, [$stop])`: a role gives `var(--nu-text-on-{role})`; anything else is computed at compile time (higher-contrast theme text colour). - `adaptive-contrast($color)` **standalone recommended**: sets background and a compile-time contrast text colour; under `prefers-contrast: more` the background moves away from the text colour (darker under light text, lighter under dark text), so the ratio rises. Needs a real colour. - `gradient($direction, $colors...)`: colour names resolve like `bg()` (`gradient(to right, brand, purple 80%)` gives `var(--nu-brand)` and `var(--nu-purple-500) 80%`); colour values and positions are written as passed; an unknown word raises the `bg()` error. ### Borders - Width: `border($width, [$style: solid], [$color])` (colour defaults to `--nu-border-regular`), `border-top`, `border-right`, `border-bottom`, `border-left`, `border-start` (L), `border-end` (L) each `($width, [$style: solid], [$color])`, colour resolved like `border()`. Width is a `$border-widths` key (0, 1, 2, 4, 8 give `var(--nu-border-width-{key})`) or a value with a unit. Width 0 sets only the width. - `border-all($width, $style, $color)` (same output as `border()` with all three arguments), `border-style($style)` (sets `--nu-border-style` for borders on the same element; `solid`, `dashed`, `dotted`, `double`, `none`, a CSS-wide keyword or `var()`), `border-color($color, [$stop])` - Radius: `rounded([$key])` (default `md`), `rounded-t`, `rounded-r`, `rounded-b`, `rounded-l`, `rounded-tl`, `rounded-tr`, `rounded-br`, `rounded-bl`, `rounded-s` (L), `rounded-e` (L), `rounded-ss` (L), `rounded-se` (L), `rounded-es` (L), `rounded-ee` (L). Keys: xs, sm, md, lg, xl, 2xl, full, none, or a value with a unit. - `pill`: fully rounded, `var(--nu-radius-full)` ### Shadows - `shadow([$size: md], [$color: default])`: config keys, or `shadow(none)` (`box-shadow: none`, as `.shadow-none`). Sizes: sm, md, lg, xl, hero, monster. Colours: default, dark, darker, darkest. Default colour outputs `var(--nu-shadow-{size})`. - `shadow-inset([$size], [$color])`, `elevation($level)` (1 to 5; any other level raises an error) - `shadow-base($x, $y, $blur, $spread, $color)`: raw custom shadow ### Typography - `font-size($key)`: `$font-sizes` key gives a token; values with units, CSS size keywords, `calc()` and `var()` pass through. - `font($weight)`: `$font-weights` key (`bold`) or a number 1 to 1000. - `leading($value)`: `$line-heights` key, a number, a length, `calc()`, `var()` or a keyword. Shortcuts: `leading-none`, `leading-tight`, `leading-snug`, `leading-normal`, `leading-relaxed`, `leading-loose`. - Alignment: `text-left`, `text-center`, `text-right`, `text-justify`, `text-start` (L), `text-end` (L) - Case: `uppercase`, `lowercase`, `capitalize`, `normal-case` - Style: `italic`, `not-italic`, `underline`, `line-through`, `no-underline` - Overflow: `truncate([$lines: 1])` (`truncate(2)` matches `.truncate-2`), `truncate-lines([$lines: 2])`, `overflow-label`. Line counts are whole numbers of 1 or more, `calc()` or `var()`. Limit: the responsive `.truncate@{bp}` and `.overflow-label@{bp}` classes come after the responsive display, overflow and sizing classes of every breakpoint, so `truncate@sm overflow-visible@lg` stays truncated at `lg`; undo it in your own styles. - `.flex.inline`, `.grid.inline`, `.container.flex` and `.container.grid` are emitted with `:where()` (one class of specificity), so `.hide`, `.py-0` and `.max-w-full` override them. `.absolute-center` keeps its `-50%` translate in `:where()`, so `.translate-*` classes adjust it. - Breaking: `break-normal`, `break-words`, `break-all` - Whitespace: `whitespace-normal`, `whitespace-nowrap`, `whitespace-pre`, `whitespace-pre-line`, `whitespace-pre-wrap` - Tracking: `tracking($value)` (a key, 0, a value with a unit, `calc()`, `var()` or a CSS-wide keyword; `tracking(2)` raises an error), `tracking-tighter`, `tracking-tight`, `tracking-normal`, `tracking-wide`, `tracking-wider`, `tracking-widest` - `responsive-typography([$breakpoint])`: class generator, **standalone** ### Opacity and cursor `opacity($value)`: a `$opacities` key gives its token (`opacity(50)` gives `var(--nu-opacity-50)`, 0.5 by default, as `.opacity-50`); a percentage (`50%`), `calc()`, `var()` or a CSS-wide keyword passes through; `opacity(0.5)` raises an error. `cursor($value)` ### Vertical alignment `align-baseline`, `align-top`, `align-middle`, `align-bottom` ### Transforms `translate($x, [$y])`, `translate-x($v)`, `translate-y($v)`, `translate-z($v)`, `scale($v)`, `scale-x($v)`, `scale-y($v)`, `rotate($angle)`, `skew-x($angle)`, `skew-y($angle)`, `origin($value)` (an `$origin-values` key such as `top-left` gives `top left`, like `.origin-top-left`; otherwise one to three position keywords, lengths, percentages, `calc()` or `var()`; a quoted value reads as unquoted (`origin("top left")`), and `calc()` and `var()` parts stay whole in a namespaced `nu.apply(origin(calc(10px + 5%) var(--y)))`; anything else raises an error), `apply-transform`. - Translate: a unitless number is a spacing key (`translate-x(4)` gives `var(--nu-space-4)`, `translate-x(-4)` gives `calc(-1 * var(--nu-space-4))`); fraction keys `25p`, `33p`, `50p`, `66p`, `75p`, `100p` (and `-25p`) give percentages; values with units pass through (`translate-x(1rem)`). A number that is not a key (`translate-x(7)`) raises an error. Classes exist for keys 0 to 20 and the fractions (`.translate-x-4`, `.-translate-y-50p`). - Scale: a class key gives its factor (`scale(150)` is `1.5`, as `.scale-150`); other values pass through as factors (`scale(1.5)`). - Rotate: `rotate(45)` is the `$rotation-values` key (45deg, as `.rotate-45`; keys 0, 45, 90, 180, 270, negated for `-45`); other angles need a unit (`rotate(30deg)`). Skew has no keys: write a unit (`skew-x(6deg)`). 0 is written `0deg`; any other unitless number or word raises an error. ### Transitions - Property: `transition` (all, plus the default duration and easing tokens from `$transition-default-duration` (`200`) and `$transition-default-ease` (`in-out`); the `.transition` class keeps them in `:where()`), `transition-none`, `transition-colors`, `transition-opacity`, `transition-shadow`, `transition-transform` (the last four also set the default duration and easing, in `:where()` for the classes) - `duration($value)`: `$durations` key (`duration(200)` gives `var(--nu-duration-200)`) or a time (`427ms`) - `delay($value)`: `$delays` key (raw value, no token) or a time - `ease($name)`: `$timing-functions` key (`linear`, `in`, `out`, `in-out`), a CSS keyword, `cubic-bezier()`, `steps()`, `linear()` or `var()`. Shortcuts: `ease-linear`, `ease-in`, `ease-out`, `ease-in-out` ### Z-index `z-index($value)`: level name (`modal` gives `var(--nu-z-modal)`), numbered key (`10`), integer, `auto`, `calc()` or `var()`. Shortcuts: `z-base`, `z-hover`, `z-dropdown`, `z-sticky`, `z-fixed`, `z-overlay`, `z-drawer`, `z-modal`, `z-popover`, `z-tooltip`, `z-toast`, `z-max`, `z-auto`. `z-index-utilities` is a class generator, **standalone**. ### Backdrop filters - Single: `backdrop-filter($value)`, `backdrop-blur($v)` (none, sm, md, lg, xl, 2xl or a length in px, rem, em), `backdrop-brightness($v)`, `backdrop-contrast($v)`, `backdrop-grayscale($v)`, `backdrop-hue-rotate($v)` (key or angle; class `.backdrop-hue-rotate-{key}`), `backdrop-invert($v)`, `backdrop-saturate($v)`, `backdrop-sepia($v)`. Keys match as numbers or strings (`backdrop-brightness(90)` gives `brightness(90%)`); other values must be a percentage (blur: length, hue rotate: angle), unitless 0, `calc()` or `var()`, else an error - Combine several: start with `backdrop-layer`, then the single mixins - Presets: `backdrop-frosted`, `backdrop-glass`, `backdrop-dark-glass`, `backdrop-crystal`, `backdrop-matte`, `backdrop-none` - Every `backdrop-filter` is emitted with `-webkit-backdrop-filter`. The value maps and `$backdrop-filter-presets` are `!default` (configurable) - `generate-backdrop-filter-utilities` is a class generator, **standalone** ### Animations (standalone) `animate-bounce([$props])`, `animate-pulse([$props])`, `animate-spin([$props])`, `animate-ping([$props])`, `animate-fade([$props])`, `animate-shake([$props])`, `hover-ready` (all properties, default duration and easing tokens), `ensure-keyframes($name)`. `$props` is a map (`(duration: 2s, iteration: 3)`); an unknown key raises an error listing the valid keys, an unknown `animate-fade` type raises an error, and `animate-shake` intensity and `animate-bounce` offsets are lengths (a unitless number other than 0, `calc()` or `var()` raises an error); `animate-ping` scale is a unitless number. Quoted map values work like unquoted ones (`(duration: "2s")`). Keyframes are emitted once, at the stylesheet root (outside `@media`, `@supports`, `@container` and `@scope`), so an animation used first inside a breakpoint still runs below it. Inside `apply()` with `as *` they take a map (`apply(animate-spin((duration: 2s)))`); in a namespaced load pass the item as a call (`nu.apply(nu.animate-spin((duration: 2s)))`). `.hover-ready` keeps its duration and easing in `:where()`, so `.duration-*` and `.ease-*` override them. Classes: `.animate-pulse`, `.animate-spin`, `.animate-ping`, `.animate-fade-in`, `.animate-fade-out`, `.animate-fade-in-up`, `.animate-fade-in-down`, `.animate-shake`, `.hover-ready`. ### Media and container queries (standalone, take a content block) - Media: `media-up($bp)`, `media-down($bp)`, `media-between($lower, $upper)`, `media-only($bp)`. Accept breakpoint names (an unquoted `2xl` works) or lengths (`media-up(873px)`); a unitless number that is not a name raises an error. Ranges end 0.02px below the upper breakpoint. `media-only()` takes a length in the breakpoint unit; another unit raises an error. - Container: `container-up($size, [$name])`, `container-down`, `container-between($lower, $upper, [$name])`, `container-only`, `container-query($size, [$name])` (a breakpoint name resolves to its width, a length or `calc()` is used as is; any other word or a unitless number raises an error), `container-type([$type: inline-size])`, `container-name($name)` - Preferences and features: `prefers-dark`, `dark-mode`, `reduced-motion`, `high-contrast`, `touch`, `fine-pointer`, `landscape`, `portrait`, `print`, `display-mode([$mode])`, `supports($property)` (a quoted declaration with a colon, `supports("display: grid")`; anything else raises an error) - Layout helpers: `aspect-ratio($ratio, [$height])` (`aspect-ratio(16, 9)` or `aspect-ratio("16/9")` give `16 / 9`; an unquoted `16/9` is Sass division, which warns and passes 1.78), `dvh`, `safe-area-inset($property, $position)` (`$position` is `top`, `right`, `bottom` or `left`) ### Theme and components `dark-theme` (emits dark token declarations), `tooltip-variables` (tooltip defaults: `--nu-tooltip--Background`, `--nu-tooltip--Color`, `--nu-tooltip--ShadowColor`, `--nu-tooltip--ZIndex`, `--nu-tooltip--TransitionDuration`, `--nu-tooltip--TransitionTimingFunction`, `--nu-tooltip--TransitionDelay`, `--nu-tooltip--FontSize`, `--nu-tooltip--FontWeight`, `--nu-tooltip--TextTransform`, `--nu-tooltip--WidthSmall`, `--nu-tooltip--WidthMedium`, `--nu-tooltip--WidthLarge`, `--nu-tooltip--BorderRadius`; tooltips show on hover and `:focus-visible`; every theme block re-declares `--nu-tooltip--Background` and `--nu-tooltip--Color`, so a tooltip inside a `data-theme` section takes that section's colours) ### Functions `token($name)`, `get-color($color, [$stop])`, `find-text-color($background, [$text-colors])` (higher-contrast text colour for an opaque colour; candidates default to the light theme `text-regular` and `text-inverted`), `to-number($value, [$append-unit])` (`"12px"` gives the number `12px`), `create-color-scale($color)` (map of stop to colour; takes a colour or a string holding one, such as `"#007bff"`). `get-breakpoint-value($bp)` (a breakpoint name or a length; `get-breakpoint-value(md)` is `768px`). Not public: `to-color()`, `from-hex()`, `hex-to-dec()`, `luminance()`, `is-valid-color()`, `is-valid-shade()`, `to-css-var()`, `validate-theme-tokens()`, `has-token()` and the `apply()` internals (`pam`, `pai`, `am`). Sass does not report an unknown function: it writes the call into the CSS as text, so never call them. --- ## Configuration Maps and Defaults All variables below are `!default` and set through `with()`. ### Global ```scss $generate-utility-classes: false; // true in build.scss (dist) $enable-dark-mode: true; $enable-debugger: false; $default-unit: rem; $parent-selector: ""; // e.g. ".app" scopes every class under .app $base-size: 1rem; // html font-size $column-count: 12; ``` Feature flags (`$feature-flags`, all `true`): alignments, animations, backdrops, base, borders, container, container-queries, cursors, displays, flex, grid, opacity, position, shadows, sizing, spacing, tooltips, theme, transforms, transitions, typography (was `config-typo`), z-index. A flag missing from the map counts as on. Each flag owns its classes (`.flex` and `.grid` belong to `flex` and `grid`; `displays: false` keeps them). `base` controls the base element layer and works without `$generate-utility-classes`; the reset has no flag. Turning a flag off removes its classes; `theme` off also removes all tokens and `--nu-font-family` (base elements use the `$font-family` value), makes scale mixins output raw values (`p(4)` is `1rem`), and makes colour names in `bg()`, `text-color()`, `border-color()` (`bg(brand)`, `bg(red, 600)`, `text-color(muted)`) and `token()` raise an error saying the flag is off. `black`, `white` and `transparent` are emitted as plain colours, `text-on-color()` is computed at compile time, and `border()` without a colour sets no `border-color` (the border takes `currentcolor`). ### Colours ```scss $brand: #007bff; $success: #28a745; $danger: #dc3545; $warning: #ffc107; $info: #17a2b8; $color-primitives: ("gray": #6b7280, "red": #ef4444, "blue": #3b82f6, "green": #22c55e, "yellow": #eab308, "purple": #a855f7, "pink": #ec4899); $basic-colors: ("black": #000, "white": #fff, "transparent": transparent); ``` `$semantic-colors` maps role names to colours (by default the five role variables; add a key for an extra role). Shade stops are fixed: 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950 (`$shade-stops`, not configurable). Stop 500 is the configured colour unchanged; other stops are generated in OKLCH with the same hue. `$primitive-color-names` is derived from `$color-primitives` and not configurable; `$theme-tokens` is not exported. ### Breakpoints and spacing ```scss $breakpoints: ("xs": 480px, "sm": 640px, "md": 768px, "lg": 1024px, "xl": 1280px, "2xl": 1536px); $spacings: (0: 0, 1: 0.25rem, 2: 0.5rem, 3: 0.75rem, 4: 1rem, 5: 1.25rem, 6: 1.5rem, 8: 2rem, 10: 2.5rem, 12: 3rem, 16: 4rem, 20: 5rem, 24: 6rem, 32: 8rem, 40: 10rem, 48: 12rem, 56: 14rem, 64: 16rem); $percentages: (0, 5, 10, 20, 25, 30, 33, 40, 50, 60, 66, 70, 75, 80, 90, 100); // widths and heights $opacities: (0: 0, 5: 0.05, 10: 0.1, 20: 0.2, 25: 0.25, 30: 0.3, 33: 0.33, 40: 0.4, 50: 0.5, 60: 0.6, 66: 0.66, 70: 0.7, 75: 0.75, 80: 0.8, 90: 0.9, 100: 1); // config/_opacity.scss $control-heights: ("sm": 2rem, "md": 2.5rem, "lg": 3rem); $control-padding-inline: ("sm": 0.75rem, "md": 1rem, "lg": 1.25rem); ``` ### Borders and shadows ```scss $border-widths: (0, 1, 2, 4, 8); // px $border-radii: ("xs": 0.25rem, "sm": 0.375rem, "md": 0.5rem, "lg": 0.75rem, "xl": 1rem, "2xl": 1.25rem, "full": 99in, "none": 0); $shadow-colors: ("default": rgb(0 0 0 / 10%), "dark": rgb(0 0 0 / 20%), "darker": rgb(0 0 0 / 35%), "darkest": rgb(0 0 0 / 50%)); // $shadow-sizes: sm (0 1px 2px 0), md (0 4px 6px -1px), lg (0 10px 15px -3px), xl (0 20px 25px -5px), // hero (0 20px 25px 5px), monster (0 20px 55px 20px). Each entry is a map of x, y, blur, spread. ``` ### Typography ```scss $font-family: system-ui, -apple-system, "BlinkMacSystemFont", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", sans-serif; $font-sizes: ("xs": 0.75rem, "sm": 0.875rem, "md": 1rem, "base": 1rem, "lg": 1.25rem, "xl": 1.5rem, "2xl": 1.75rem, "3xl": 2rem, "4xl": 2.5rem); $font-weights: ("thin": 100, "extralight": 200, "light": 300, "normal": 400, "medium": 500, "semibold": 600, "bold": 700, "extrabold": 800, "black": 900); $line-heights: ("none": 1, "tight": 1.25, "snug": 1.375, "normal": 1.5, "relaxed": 1.625, "loose": 2); ``` ### Z-index and motion ```scss $z-indexes: ("auto": auto, "0": 0, "10": 10, "20": 20, "30": 30, "40": 40, "50": 50, "60": 60, "70": 70, "80": 80, "90": 90, "100": 100, "200": 200, "500": 500, "1000": 1000); $z-index-levels: ("base": 0, "hover": 10, "dropdown": 100, "sticky": 200, "fixed": 300, "overlay": 400, "drawer": 500, "modal": 600, "popover": 700, "tooltip": 800, "toast": 900, "max": 9999); $timing-functions: ("linear": linear, "in": cubic-bezier(0.4, 0, 1, 1), "out": cubic-bezier(0, 0, 0.2, 1), "in-out": cubic-bezier(0.4, 0, 0.2, 1)); $durations: ("0": 0ms, "75": 75ms, "100": 100ms, "150": 150ms, "200": 200ms, "300": 300ms, "500": 500ms, "700": 700ms, "1000": 1000ms); // $delays: same keys and values as $durations ``` ### Theme maps ```scss $light-theme: (); // overrides only $dark-theme: (); // overrides only $high-contrast-light: (); // overrides only, empty: nothing emitted $high-contrast-dark: (); // overrides only, empty: nothing emitted ``` Built-in theme defaults (light / dark; `core/docs/TOKENS.md` is generated from the source): `bg-base` #fbfafc / #1a1a1a, `bg-surface` #fff / #2e2e2e, `bg-alternate` gray 100 / #242424, `bg-raised` #fff / #3a3a3a, `bg-hover` gray 100 / #3a3a3a, `bg-selected` brand 50 / brand 950, `bg-disabled` gray 100 / #333, `text-regular` #1a1a1a / #f1f1f1, `text-muted` #666 / #a8a8a8, `text-subtle` #949494 / #6e6e6e, `text-inverted` #f1f1f1 / #1a1a1a, `text-link` brand 700 / brand 300, `text-link-hover` brand 800 / brand 200, `border-regular` #d3d5d9 / #474747, `border-strong` gray 400 / gray 500, `border-control` gray 500 / gray 400, `border-control-hover` gray 600 / gray 300, `border-focus` brand 600 / brand 400, `border-focus-width` 2px, `border-focus-offset` 2px. --- ## Validation and Errors NuvoUI 2 fails at compile time instead of emitting broken CSS. Every message names the valid options. | Input | Result | | --- | --- | | `p(7)`, `width(300)`, `border(3)`, `rounded(8)`, `font-size(17)` | Error: not a configured key; write a unit | | `p(-4)`, `gap(-2)`, `width(-4)` | Error: negative keys only for margin, inset, offsets, translate and space | | `p(-1rem)`, `rounded(-2px)` | Error: negative raw values only for margin, inset, offsets, translate and space | | `media-up(800)`, `container-down(600)` | Error: not a breakpoint name; write a unit | | `backdrop-brightness(1.2)`, `backdrop-blur(3)` | Error: use a key or a value with the right unit | | `fill(0)`, `animate-fade((type: sideways))`, `get-color("#ggg")` | Error | | `p(auto)` | Error: auto is not a valid padding | | `gap(auto)`, `width(foo)` | Error: unknown value (lists keys and the keywords valid for the property) | | `width(3xl)`, `top(40p)` | Error: not a sizing key / not a fraction key | | `tooltip` removed from `$z-index-levels` (tooltips emitted) | Error: tooltips need the key | | `bg(primary)`, `bg(secondary)`, `bg(brand-default)`, `bg(nope)` | Error: unknown colour (lists roles, hues and theme words) | | `bg(brand)`, `text-color(muted)`, `token(text-muted)` with the `theme` flag off | Error: the theme feature flag is off (no tokens are emitted); pass a colour value | | `bg(bg-surface)` | Error: unknown colour (the generic message, listing theme words such as `surface`) | | `bg(red, 450)`, `get-color(red, 450)` | Error: invalid shade (lists stops) | | `get-color(var(--x))` | Error: a CSS variable has no compile-time colour | | `get-color(nope)` | Error: unknown colour | | `token(text-mutd)` | Error: unknown token, close matches: text-muted | | `token(brand-default)`, `token(danger-default)` | Error: renamed in 2.0, use `token(brand)` | | `token(--nu-text-muted)` | Error: pass the name without the prefix | | `leading(foo)`, `ease(bounce)`, `duration(fast)`, `z-index(top)`, `font(heavy)` | Error: unknown value (lists keys) | | `opacity(0.5)`, `tracking(2)`, `elevation(7)`, `border-style(groove)` | Error: unknown value (lists what is accepted) | | `translate-x(7)` | Error: not a spacing key | | `rotate(30)`, `skew-x(10)` | Error: write a unit (`30deg`); `rotate(45)` is the `$rotation-values` key (45deg) | | `p(4px 8px)`, `m(0 auto)` | Error: `p: unknown value 4px 8px` (scale messages name the mixin: `width: unknown value foo`) | | `apply(p(4) hovr)`, `apply(p(4) md lg)`, `apply(p(4) hover focus)` | Error: unknown modifier / more than one breakpoint / more than one state | | `animate-shake((intensity: 8))`, `container-query(30)` | Error: write a unit (or a breakpoint name for `container-query`) | | `shadow(huge)`, `shadow(md, blue)` | Error: size or colour not found | | `$light-theme: ("bg-surfase": ...)` | Error: unknown tokens | | `$light-theme: ("bg-base": brand600)` | Error: malformed scale reference | | `$light-theme: ("brand-default": ...)`, also in `$dark-theme` and the high-contrast maps | Error: renamed to `brand` in 2.0, rename the key | | `$light-theme: ("error": ...)`, `("error-default": ...)` | Error: error tokens follow danger; set `danger` | | `$light-theme: ("space-4": 2rem)`, `("red-500": ...)` | Error: custom token reuses a palette, basic colour or scale token name | | `$color-primitives` with a hue also in `$semantic-colors`, or a name `error`, `black`, `link` or `opacity` | Error: name collision | | `with ($primary: ...)`, `with ($secondary: ...)`, `with ($default-color: ...)` | Error: variable not declared with !default (removed in v2) | | Animation `$props` with unknown keys (`animate-spin((durration: 2s))`) | Error: unknown key (lists valid keys) | | `cols(foo)`, `cols(0)`, `col-span(full)`, `truncate-lines(1.5)`, `auto-fit(4)` | Error: not a count / not a line count / not a minimum width | | `container(size)`, `scale(foo)`, `rotate(foo)`, `translate-z(50%)`, `cursor(foo)`, `aspect-ratio(video)` | Error: not a display value / not a scale / not an angle / a percentage on the z axis / unknown cursor / not a ratio | | `font-size(-1rem)`, `duration(-100ms)`, `fill(1.5)`, `media-between(lg, sm)` | Error: negative size or time / whole columns only / lower breakpoint must be smaller | | Removed mixins (`flex-inline`, `text-size`, `grid-position`, `flow-in-row`, ...) | Error: Undefined mixin (direct) or Mixin not found (in `apply()`) | | `with ($high-contrast-theme: ...)` | Error: variable not declared with !default (use `$high-contrast-light`, `$high-contrast-dark`) | ```scss // Raises an error .a { @include apply(p(7)); } .a { @include apply(width(300)); } .a { @include apply(gap(-2)); } .a { @include apply(p(auto)); } .a { @include apply(gap(auto)); } .a { @include apply(bg(primary)); } .a { @include apply(bg(bg-surface)); } .a { @include apply(bg(red, 450)); } .a { color: get-color(var(--x)); } .a { color: token(brand-default); } .a { @include apply(leading(foo)); } .a { @include apply(z-index(top)); } .a { @include apply(shadow(huge)); } ``` --- ## Anti-Patterns 1. **Inventing utilities**: `flex-center` does not exist. Use `flex, center, x-center`. 2. **Tailwind syntax**: `justify-between`, `items-center`, `lg:p-4`, `w-[137px]` do not exist. Use `between`, `x-center`, `p-4@lg`, and SCSS for custom values. 3. **Colon or prefix modifiers in apply()**: `hover:bg(...)`, `lg:p(4)`. Modifiers go after: `bg(...) hover`, `p(4) lg`. 4. **Raw numbers as lengths**: `p(16)` is key 16 (4rem), not 16px. Write `p(16px)` for pixels. 5. **Hard-coded colours for themed UI**: `bg(#fff)` will not flip in dark mode. Use `bg(surface)`. 6. **Palette shades for themed text**: `text-color(gray, 600)` is fixed. Use `text-color(muted)`. 7. **Hand-written `var(--nu-...)`** where `token()` works: typos are not caught. 8. **`get-color()` for theme-aware styling**: it returns a fixed colour. Use mixins or `token()`. 9. **Alpha syntax**: there is no `bg-danger@10` or opacity colour syntax. Use `bg-danger-subtle`. 10. **`text-on-{hue}`**: only roles have it. For text on a palette fill use `text-on-color(red, 700)`. 11. **Setting `error-*` tokens**: they follow danger. Override `danger-*`. 12. **`-default` role names**: `--nu-brand-default`, `token(brand-default)`, `bg(brand-default)`, a `"brand-default"` theme key. The fill is the bare name: `--nu-brand`, `token(brand)`, `bg(brand)`, `"brand"`. 13. **Custom theme token in one map only**: a new key must be in both `$light-theme` and `$dark-theme` (only `$light-theme` when `$enable-dark-mode` is false). 14. **Animation options in namespaced apply()**: `nu.apply(animate-spin((duration: 2s)))` fails, because Sass cannot write a map into a plain-text item. Pass the item as a call: `nu.apply(nu.animate-spin((duration: 2s)))`, or `@include nu.animate-spin(...)` directly. With `as *`, `apply(animate-spin((duration: 2s)))` works. 15. **Grid templates**: `cols-custom(1fr auto 1fr)` and `cols-custom("1fr_auto_1fr")` give the same track list. A comma inside the template (`repeat(2, 1fr)`) needs quotes: `cols-custom("repeat(2, 1fr) 200px")`. --- ## Anti-Hallucination Table | Wrong | Correct (v2) | Notes | | --- | --- | --- | | `bg-primary`, `text-primary`, `bg(primary)` | `bg-brand`, `text-brand`, `bg(brand)` | `primary` is not a colour in core | | `$primary` | `$brand` | | | `bg-secondary`, `$secondary` | a palette hue: `bg-purple`, `$color-primitives` | No secondary role | | `--primary`, `--primary-500` | `--nu-brand`, `--nu-brand-500` | Every token has `--nu-` | | `--text-color`, `.text-color` | `--nu-text-regular`, `.text-regular` | | | `--border-base`, `.border-base` | `--nu-border-regular`, `.border-regular` | | | `--bg-surface` | `--nu-bg-surface` | | | `--link-color` | `--nu-text-link` | | | `--nu-text-default`, `--nu-text-normal` | `--nu-text-regular` | `default` is not a token word | | `--nu-brand-default`, `--nu-danger-default`, `bg(brand-default)` | `--nu-brand`, `--nu-danger`, `bg(brand)` | The fill is the bare role name | | `--nu-brand-text` | `--nu-text-brand` | | | `--nu-status-danger` | `--nu-danger` | No `status-` prefix | | `--nu-brand-1000`, `bg(red, 450)` | stops 50, 100 to 900, 950 | | | `.text-on-red-700` | `text-on-color(red, 700)` mixin | Per-shade classes removed | | `.text-on-blue` | `.text-on-brand` etc. (roles only) | | | `bg-danger@10`, `bg-danger/10` | `bg-danger-subtle` | | | `.gap-auto`, `gap(auto)` | `gap-0`, `gap(normal)` | `gap: auto` is not valid CSS | | `bg(bg-surface)`, `text-color(text-muted)` | `bg(surface)`, `text-color(muted)` | Short words | | `p(4)` = 4rem or 16px | `p(4)` = key 4 = 1rem | Keys, not units | | `p(1)` = 1px | `p(1px)` | `p(1)` is 0.25rem | | `text(white)`, `color(white)` | `text-color(white)` | No `text()` mixin | | `bg-white` for cards | `bg-surface` | Follows theme | | `ml(4)` for RTL-safe spacing | `ms(4)` | Logical | | `border-left(1)` for RTL-safe edges | `border-start(1)` | Logical | | `left(4)` for RTL-safe offsets | `inset-start(4)` | Logical; class `.start-4` | | `start(4)`, `end(4)` mixins | `inset-start(4)`, `inset-end(4)` | `start`, `end` are flex alignment | | `flex-col`, `flex-row` | `col`, `row` | | | `class="between"` without `flex`, `class="inline-flex"` | `class="flex between"`, `class="flex inline"` | Flex classes are compound with `.flex` | | `flex-none`, `flex-1` | `no-grow`, `grow` | `grow` is `flex: 1 1 0%`; `fill-full` is `flex: 0 0 100%`, not `flex-1` | | `items-center`, `justify-center` | `x-center`, `center` | | | `y-center` | `x-center` | `x-` = cross axis | | `w()`, `h()`, `min-w()`, `max-w()` | `width()`, `height()`, `min-width()`, `max-width()` | | | `flex-inline`, `grid-inline`, `d-tbl` | `inline-flex`, `inline-grid`, `table` | Removed in 2.0 | | `text-size()` | `font-size()` | Removed in 2.0 | | `flow-in-row`, `grid-position()`, `position-col()` | `flow-row`, `grid-place-at()`, `grid-col-at()` | Renamed to match classes | | `role="tooltip"` on a tooltip trigger | `data-tooltip` alone | Keeps the trigger's real role | | Tooltip without `data-tooltip-position` (no arrow) | Shown on top by default | Keyboard focus slides it in like hover | | `.backdrop-hue-90` | `.backdrop-hue-rotate-90` | Matches the mixin name | | `.inverted-*`, `--inverted-*` | `data-theme="inverse"` | Opposite of the page | | `font-weight(bold)` | `font(bold)` | | | `line-height(tight)` | `leading(tight)` | | | `rounded(8)` | `rounded(lg)` or `rounded(8px)` | | | `shadow(0 4px 20px ...)` | `shadow-base(0, 4px, 20px, 0, ...)` | `shadow()` takes keys | | `z-index(9999)` for modals | `z-modal` | Named levels | | `[data-theme="system"]` for auto dark | omit `data-theme` | Tokens and the `dark-mode` mixin follow the OS only on `:root:not([data-theme])`; `system` is not read | | `--flex-gap`, `--translate-x`, `--border-style` | `--nu-flex-gap`, `--nu-translate-x`, `--nu-border-style` | Internal variables carry `--nu-` | | `--tooltip-bg`, `--tooltip-text-color` | `--nu-tooltip--Background`, `--nu-tooltip--Color` | Component grammar | | `opacity(0.5)` | `opacity(50)` | Keys, as `.opacity-50` | | `scale(1.5)` only | `scale(150)` or `scale(1.5)` | Both give 1.5 | | `luminance()`, `to-color()` | not public | | | `@import "@nuvoui/core"` | `@use "@nuvoui/core"` | Sass modules | --- ## Common Patterns ```scss @use "@nuvoui/core" as *; // Card on the page .card { @include apply(p(4), bg(surface), text-color(regular), border(1), rounded(lg), shadow(sm), shadow(md) hover); } // Centered flex row .toolbar { @include apply(flex, between, x-center, gap(2), px(4), py(2), bg(alternate)); } // Responsive grid .gallery { @include apply(grid, cols(1), cols(2) md, cols(3) lg, gap(4)); } // Brand button .button { @include apply(inline-flex, x-center, gap(2), px(4), py(2), rounded(md), bg(brand), text-color(on-brand), cursor(pointer), transition-colors, duration(150), ease(out), bg(brand-hover) hover, bg(brand-active) active); &:focus-visible { outline: token(border-focus-width) solid token(border-focus); outline-offset: token(border-focus-offset); } } // Danger alert .alert-danger { @include apply(p(4), rounded(md), bg(danger-subtle), text-color(danger), border(1, solid, danger-border)); } // Form field .field { @include apply(w-full, px(3), py(2), bg(surface), text-color(regular), border(1, solid, control), rounded(sm), border-color(control-hover) hover); } // Selected list row .row.is-selected { @include apply(bg(selected), text-color(brand)); } // Floating menu .menu { @include apply(absolute, z-dropdown, p(2), bg(raised), rounded(md), shadow(lg), border(1)); } // RTL-safe sidebar item .sidebar-item { @include apply(flex, x-center, gap(3), ps(4), pe(2), py(2), border-start(4, solid), text-start); } // Tag built on a hue role .tag-purple { @include apply(inline-flex, px(2), rounded(full), bg(purple-subtle), text-color(purple), font-size(sm), font(medium)); } // Container .page { @include apply(container-base, container, px(4)); } ``` ```html

Title

Body text

Payment failed
...
``` **Remember**: tokens follow the theme, numbers are keys, colour words match class names, modifiers come last, and the source in `core/src/styles` is the final authority.