Colorffy CSS

Customization

Override SCSS variables, mixins, and CSS custom properties.

Colorffy CSS is built to be customized — override variables at build time or CSS custom properties at runtime.

Set brand colors and fonts

Theme colors and fonts are CSS custom properties, so the same override works with the compiled CSS and with the SCSS source. Put it in your own stylesheet: it wins over the framework's layered defaults whatever the import order.

theme.css
:root {
  /* each theme color has a light-mode (-500) and dark-mode (-50) tone */
  --cffy-color-brand-primary-500: oklch(45% 0.2 275);
  --cffy-color-brand-primary-50: oklch(90% 0.06 275);
  --cffy-color-brand-secondary-500: oklch(62% 0.2 350);
  --cffy-color-brand-secondary-50: oklch(92% 0.05 350);

  --cffy-font-primary: 'Inter', sans-serif;
  --cffy-font-secondary: 'Inter', sans-serif;
}

Brand tokens exist for primary, secondary, accent, success, warning, danger, info and muted. The tonal steps (--cffy-primary-a10 … a90), the text colors that sit on them (--cffy-on-primary, --cffy-on-primary-container, …) and every component derive from them, so a light yellow secondary gets dark text without further overrides. To use one color in both modes, set --cffy-primary-base instead.

Override SCSS variables

A few component defaults are compile-time SCSS variables. Set them with @forward ... with before importing the framework:

theme.scss
@forward '@colorffy/css/scss/abstracts/variables' with (
  $shape-container: 8px,
  $shape-field: 4px,
  $shape-control: 4px,
);

@use '@colorffy/css/scss/main';

Use mixins and functions

The mixins module holds layout and text helpers; the functions module holds the color helpers the components are built with. A hoverable tinted tile with a gradient title:

@use '@colorffy/css/scss/abstracts/mixins' as mix;
@use '@colorffy/css/scss/abstracts/functions' as fn;

.promo-tile {
  --_promo-state: 0%;

  padding: var(--cffy-space-24);
  border-radius: var(--cffy-radius-lg);
  background-color: fn.state-layer(var(--_promo-state), fn.tonal(var(--cffy-accent-a10), 60%));
  color: var(--cffy-on-accent-container);

  &:hover {
    --_promo-state: var(--cffy-state-hover-opacity);
  }

  @include mix.media-query(768) {
    padding: var(--cffy-space-16);
  }
}

.promo-title {
  @include mix.text-gradient(45deg, var(--cffy-primary-base), var(--cffy-accent-base));

  background-clip: text;
  color: transparent;
}

See Mixins for the full list, including the functions module.

Customize in Nuxt 3

Feed your variables into every SCSS file via Vite:

nuxt.config.ts
export default defineNuxtConfig({
  css: ['~/assets/scss/main.scss'],
  vite: {
    css: {
      preprocessorOptions: {
        scss: {
          additionalData: '@use "~/assets/scss/variables.scss" as *;',
        },
      },
    },
  },
})

Override at runtime

:root {
  --cffy-primary-base: #4f46e5;
  --cffy-btn-radius: var(--cffy-radius-full);
}

A component hook such as --cffy-btn-radius reaches the component's base style. Variants with a shape of their own, like .btn-rounded or the floating action button, keep it.

Shape, weight & motion tokens

Radius, border width, font weight, easing and the focus ring are CSS custom properties on :root, and the components read them. Override them at runtime without recompiling the SCSS. Border width, weight, easing and the focus ring also work on a wrapper for one region; the radius steps feed the shape roles on :root, so reshape a region by setting the roles there instead.

:root {
  --cffy-radius-md: 10px;
  --cffy-border-width-sm: 1.5px;
  --cffy-fw-600: 650;
  --cffy-ease-decelerate: cubic-bezier(0.2, 0, 0, 1);
  --cffy-focus-ring-color: var(--cffy-primary-a10);
}
TokenDefault
--cffy-radius-none / -sm / -md / -lg / -xl / -full0 / 6px / 8px / 12px / 25px / 9999px
--cffy-border-width-sm / -md1px / 2px
--cffy-fw-400 … --cffy-fw-800400 … 800
--cffy-ease-decelerate / --cffy-ease-accelerateStandard enter / exit curves
--cffy-ease-spring, --cffy-ease-overshoot-soft, --cffy-ease-emphasized, --cffy-ease-bounce, --cffy-ease-power-in, --cffy-ease-power-in-out, --cffy-ease-sine-in-outlinear() curves
--cffy-focus-ring-color / --cffy-focus-ring-offset--cffy-on-background / .3rem
--cffy-duration-100, -150, -200, -250, -300, -400, -500, -600, -800That many milliseconds, from --cffy-duration-unit (1ms)

Every interaction duration derives from --cffy-duration-unit, so overall motion speed is a one-liner:

/* 20% faster motion */
:root { --cffy-duration-unit: .8ms; }

Under prefers-reduced-motion: reduce, --cffy-ease-spring, --cffy-ease-overshoot-soft and --cffy-ease-bounce resolve to --cffy-ease-decelerate, so nothing overshoots or bounces.

The SCSS variables ($rounded-*, $border-sm, $fw-*, $easing-*) still exist and set these tokens' defaults.

Shape roles

Components take their corners from three roles rather than a fixed step of the radius scale, so three tokens reshape the whole interface:

RoleDefaultComponents
--cffy-shape-container--cffy-radius-lg (12px)Cards, pane content, dialogs, popovers, popover menus, dropdowns, listboxes, the date popup, alerts, accordions, list groups, tables, <details>; tooltips up to --cffy-radius-md
--cffy-shape-field--cffy-radius-md (8px)Text fields, selects, tags, input groups, the color field, the file drop zone, skeletons; checkboxes use half, up to --cffy-radius-sm
--cffy-shape-control--cffy-radius-md (8px)Buttons, chips, toggle buttons, sidebar items, navbar links, tab links, calendar days, time slots, <kbd>; breadcrumb links and navbar initials up to --cffy-radius-sm; tab pills use 1.5× and the floating action button 2×
/* Sharp */
:root {
  --cffy-shape-container: 0;
  --cffy-shape-field: 0;
  --cffy-shape-control: 0;
}

/* Pill buttons and fields on rounder panels */
:root {
  --cffy-shape-container: var(--cffy-radius-xl);
  --cffy-shape-field: var(--cffy-radius-full);
  --cffy-shape-control: var(--cffy-radius-full);
}

Shapes inside a container follow it. Dropdown and menu items use half the panel radius, and the inner corners of grouped list items and accordions never exceed the container. Tall fields and controls are capped at the container radius, so a textarea, the file drop zone or a toggle tile doesn't become a capsule when fields or controls are pills. On phones, bottom sheets round their top corners at twice the container radius, and so do the dropdown sheet and the navigation drawer. Icons and images in lists and timelines, and icon tiles, keep their own small radius but never exceed the container's. Icon buttons, carousel arrows, close and remove buttons, progress bars, the navigation bar indicator and the ends of connected button groups stay round unless --cffy-shape-control is 0. Badges, avatars, pagination, segmented controls, step numbers and timeline dots keep --cffy-radius-full whatever the roles.

A component hook still wins for its component: --cffy-card-radius: 0 squares cards and leaves dialogs on the container role.

In SCSS, $shape-container, $shape-field and $shape-control set the roles' defaults. $card-border-radius, $dialog-border-radius, $form-border-radius and $button-border-radius follow their role unless you set them. A component you set to a different value keeps it, and stops following the role token at runtime.

Hover and pressed states

Interactive surfaces show hover and pressed states with a state layer: their own background mixed with the primary color, in oklab. It is the same recipe that builds the surface ramp (--cffy-surface-a0 is 4% primary, --cffy-surface-container 10%), so a hovered surface looks like the next step up that ramp. That covers list items, selectable and link cards, accordion headers, dropdown and menu items, tabs, chips, table rows, the file dropbox, and sidebar, navbar and menu links. Every component gets the same step, in light and dark mode. A destructive menu item and a file dropbox holding a valid file use their danger or success color instead.

:root {
  --cffy-state-hover-opacity: 6%;
  --cffy-state-pressed-opacity: 10%;
}
TokenDefaultControls
--cffy-state-layer-color--cffy-primary-baseColor mixed into the background. Use --cffy-on-background for neutral grey states
--cffy-state-hover-opacity8%Strength on hover
--cffy-state-pressed-opacity12%Strength while pressed or focused from the keyboard

Set them on :root, or on a wrapper to change one region. Buttons, toggle buttons and segmented controls keep their own state colors.

Spacing & type tokens (2.5+)

All component spacing flows through the --cffy-space-* tokens, and every step is derived from one base unit — so density is a runtime one-liner:

/* scale the whole UI's spacing to 80% */
:root { --cffy-space-unit: .2rem; }

/* or retune a single step */
:root { --cffy-space-12: .7rem; }

The steps are worked out on :root, so the unit only rescales them there. A wrapper that sets --cffy-space-unit alone keeps the page's spacing; to tighten one region, give it a density.

Font sizes use the --cffy-fs-* t-shirt scale (--cffy-fs-4xs … --cffy-fs-5xl, anchored at --cffy-fs-base), each with a --cffy-fs-{step}--line-height companion:

:root {
  --cffy-fs-base: 1rem;              /* opt out of fluid sizing */
  --cffy-fs-base--line-height: 1.6;
}

See the migration guide for the 2.0 → 2.5 rename tables.

Density

data-density sets how tight the interface is, for the whole page or one region. Spacing and the height of fields and buttons change together; text keeps its size and corners keep their shape.

<html data-density="compact">
  …
  <section data-density="spacious">…</section>
</html>
ModeSpacingFields (sm / md / lg)Controls (sm / md / lg)
compact0.8 × --cffy-space-unit1.75 / 2 / 2.5rem1.75 / 2 / 2.5rem
comfortable (default)--cffy-space-unit2 / 2.5 / 3rem2 / 2.25 / 3rem
spacious1.2 × --cffy-space-unit2.25 / 2.75 / 3.5rem2.25 / 2.5 / 3.5rem

Fields are text inputs, selects, tags, the combobox, multi-select, date and OTP fields; they read --cffy-field-height-sm|md|lg. Controls are buttons, chips, pagination and calendar days; they read --cffy-control-height-sm|md|lg. A component hook such as --cffy-input-height still wins over the mode.

The nearest data-density wins, so a region can differ from the page, and comfortable resets a region inside a compact page. On touch screens (pointer: coarse), compact keeps the comfortable heights so taps stay easy; only its spacing tightens. Menus, tooltips and the date popup render outside the element that opens them, so they follow the page's density, not a region's.

Select the mode from CSS

Where container style queries are supported, the --cffy-density custom property selects a mode too, so a stylesheet can set it, even inside a media query:

@media (width >= 64rem) {
  .admin-shell { --cffy-density: compact; }
}

It applies to the element's content; the element itself keeps the density around it. Browsers without style queries ignore it, so treat it as an enhancement and use the attribute where the density has to hold in every browser. The two combine: the nearest one wins.

Container widths

Component widths come from one t-shirt scale, --cffy-container-3xs (16rem) to --cffy-container-7xl (80rem), and the max-w-* utilities read the same steps:

TokenValueUsed by
--cffy-container-2xs18remSidebar, popover menu, toast, side sheet
--cffy-container-xs20remPopover
--cffy-container-sm24remConfirm dialog
--cffy-container-md28remsm dialog
--cffy-container-2xl42remmd dialog
--cffy-container-4xl56remlg dialog

The other steps are 3xs 16rem, lg 32rem, xl 36rem, 3xl 48rem, 5xl 64rem, 6xl 72rem and 7xl 80rem. Change a step to resize everything on it, or use a component's own hook for a single component:

:root {
  --cffy-container-2xs: 20rem;  /* sidebar, popover menu, toasts and side sheets */
  --cffy-sidebar-width: 16rem;  /* only the sidebar */
}

Theme presets

A theme preset is a named set of token overrides applied together: a brand color, a shape, maybe a typeface and a feel for motion. Every component reads the tokens, so a preset is plain CSS, with no rebuild and no component props. Scope it with an attribute and switch it at runtime:

presets.css
/* Luxury: black ink and square edges, champagne at night */
:root[data-theme="luxury"] {
  --cffy-color-brand-primary-500: #111111;
  --cffy-color-brand-primary-50: #e8d5a8;
  --cffy-shape-container: var(--cffy-radius-none);
  --cffy-shape-field: var(--cffy-radius-none);
  --cffy-shape-control: var(--cffy-radius-none);
  --cffy-font-primary: 'Playfair Display', Georgia, serif;
}

/* Playful: bright blue on pill-shaped controls */
:root[data-theme="playful"] {
  --cffy-color-brand-primary-500: #2f6bff;
  --cffy-color-brand-primary-50: #a9c1ff;
  --cffy-shape-container: var(--cffy-radius-xl);
  --cffy-shape-field: var(--cffy-radius-full);
  --cffy-shape-control: var(--cffy-radius-full);
}
<html data-theme="luxury">

Put the attribute on <html>. The color ramps, surfaces, spacing steps, durations and shadows are worked out on the root element from the brand, space, motion and depth tokens, so those tokens only take effect there. On a wrapper, a preset applies only what components read directly: the shape roles, the fonts, the state layer and component hooks. To preview another preset's colors in a region, such as a card in a theme picker, set that region's own styles from the preset's values.

What a preset can set

LayerTokensWhat follows
Brand--cffy-color-brand-<color>-500 (light mode) and -50 (dark mode), for primary, secondary, accent, success, warning, danger, info, mutedThe a10–a90 ramps, containers, the text on them (--cffy-on-<color>), and every component. primary also tints the surfaces and the hover and pressed state layer
Shape--cffy-shape-container, --cffy-shape-field, --cffy-shape-controlEvery corner, through the shape roles
Type--cffy-font-primary (headings), --cffy-font-secondary (text), --cffy-fs-base and its --line-height, --cffy-fw-*Headings, body, labels
Space--cffy-space-unitEvery spacing step, padding and gap
Motion--cffy-duration-unit, --cffy-ease-*Every transition
Depth--cffy-shadow-intensity, --cffy-shadow-xs … -xl, --cffy-border-width-smCards, menus, dialogs, outlines
States--cffy-state-layer-color, --cffy-state-hover-opacity, --cffy-state-pressed-opacityHover and pressed backgrounds
Tint--cffy-tonal-light-intensity, --cffy-tonal-dark-intensityHow strong tonal backgrounds are

Good practice

  • Set raw and role tokens, not derived ones. Override --cffy-color-brand-primary-500, not --cffy-primary-a10: the ramps, containers and on-colors are computed from the brand tone, and replacing one step breaks the ramp. Never set a private --_* variable.
  • Design both tones. -500 is the color in light mode and -50 in dark mode. Pick the dark tone for dark surfaces, a light, slightly desaturated version of the brand (around 80–90% lightness in oklch), rather than reusing the light one.
  • Check contrast. Text on a filled color picks black or white on its own, whichever reads better. Still check text on containers and the warning and danger colors in both modes against 4.5:1, with a tool such as the Colorffy contrast checker.
  • Keep shapes coherent. Take radii from the --cffy-radius-* scale, and keep the container role at least as round as the field and control roles. Pill fields and controls are capped at the container radius when they grow tall, so textareas and drop zones stay rectangles.
  • Use component hooks for exceptions. A preset that needs a dozen component hooks is fighting the roles; set a role, then one hook such as --cffy-card-radius where a component should differ.
  • Load fonts yourself. Add the @font-face or font link before setting the font tokens, and keep a fallback stack.
  • Keep presets small. Four to ten tokens usually make a preset. Anything that isn't a token, such as layout, belongs in your app's styles.
  • Avoid a flash of the wrong theme. When a preset comes from a saved preference, set the attribute on the server or in an inline script before the page paints. Switch at runtime with document.documentElement.dataset.theme = 'playful'.
  • Test the whole matrix. Light and dark mode, focus rings, disabled controls, and a phone width, where dialogs and pickers become sheets.

Starting points

The Orbit playground ships six presets you can copy. Each pairs a brand with one of five shapes:

PresetLight brand (-500)Dark brand (-50)Shape
Orbit#004617#d4f5deDefault
Luxury#111111#e8d5a8Sharp
Playful#2f6bff#a9c1ffPill
Enterprise#1e3a8a#c3d3f7Default
Wellness#4f7a5c#c6e3cdSoft
Studio#6d28d9#dac8ffSubtle
ShapeContainerFieldControl
Sharpnonenonenone
Subtlesmmdsm
Defaultlgmdmd
Softxllglg
Pillxlfullfull

The shape values are --cffy-radius-* steps.

Copyright © 2026