diff --git a/.changeset/fill-colors.md b/.changeset/fill-colors.md
new file mode 100644
index 000000000..5e3945910
--- /dev/null
+++ b/.changeset/fill-colors.md
@@ -0,0 +1,6 @@
+---
+"@tabler/core": minor
+"@tabler/docs": patch
+---
+
+Added the `--tblr-fill-*` tokens and `.bg-fill-*` utilities, used by badges, avatars, code, progress and range tracks and the hover background.
diff --git a/core/scss/_variables.scss b/core/scss/_variables.scss
index 2a7534364..dc44b7357 100644
--- a/core/scss/_variables.scss
+++ b/core/scss/_variables.scss
@@ -575,7 +575,17 @@ $active-border-color: var(--primary) !default;
$selection-bg: light-dark(color-transparent(var(--primary), 0.1), color-transparent(var(--primary), 0.4)) !default;
-$hover-bg: light-dark(color-transparent(var(--body-color), 0.06), color-transparent(var(--body-color), 0.08)) !default;
+// Neutral fills: the text color over transparent, so one tint reads the same
+// on the page, a card, a dropdown or a modal, in both color modes.
+// scss-docs-start fill-variables
+$fill-primary: color-transparent(var(--body-color), 0.16) !default;
+$fill-secondary: color-transparent(var(--body-color), 0.08) !default;
+$fill-tertiary: light-dark(color-transparent(var(--body-color), 0.04), color-transparent(var(--body-color), 0.06)) !default;
+$fill-quaternary: color-transparent(var(--body-color), 0.02) !default;
+
+// scss-docs-end fill-variables
+
+$hover-bg: var(--fill-tertiary) !default;
$disabled-bg: var(--bg-surface-secondary) !default;
$disabled-color: color-transparent(var(--body-color), 0.4) !default;
@@ -744,7 +754,7 @@ $icon-color: var(--gray-400) !default;
// Code
$code-color: light-dark(var(--gray-600), var(--gray-400)) !default;
-$code-bg: light-dark(var(--gray-100), var(--gray-900)) !default;
+$code-bg: var(--fill-tertiary) !default;
$code-font-size: $font-size-reative-sm !default;
$pre-font-size: 0.92857143em !default;
@@ -777,7 +787,7 @@ $avatar-upload-icon-size: 1.5rem !default;
$avatar-upload-text-margin-top: 0.25rem !default;
$avatar-cover-ring-width: 0.25rem !default;
$avatar-brand-offset: -2px !default;
-$avatar-bg: var(--bg-surface-secondary) !default;
+$avatar-bg: var(--fill-tertiary) !default;
$avatar-sizes: (
'xxs': (
size: 1rem,
@@ -1056,7 +1066,7 @@ $badge-padding-x: 0.5em !default;
$badge-border-radius: var(--border-radius) !default;
$badge-empty-size: 0.5rem !default;
$badge-color: var(--secondary) !default;
-$badge-bg-color: var(--bg-surface-secondary) !default;
+$badge-bg-color: var(--fill-tertiary) !default;
$badge-gap: 0.25rem !default;
$badge-padding-x-sm: 0.25rem !default;
$badge-padding-y-lg: 0.25rem !default;
@@ -1501,7 +1511,7 @@ $tooltip-arrow-height: 0.4rem !default;
// Loader
$loader-size: 2.5rem !default;
-$list-group-header-bg: var(--bg-surface-tertiary) !default;
+$list-group-header-bg: var(--fill-tertiary) !default;
$list-group-header-color: var(--secondary-color) !default;
// Modals
@@ -1964,7 +1974,7 @@ $sortable-drag-box-shadow: $box-shadow-lg !default;
// scss-docs-start progress-variables
$progress-height: 0.5rem !default;
$progress-font-size: $font-size-base * 0.75 !default;
-$progress-bg: var(--border-color) !default;
+$progress-bg: var(--fill-secondary) !default;
$progress-border-radius: var(--border-radius) !default;
$progress-box-shadow: var(--box-shadow-inset) !default;
$progress-bar-color: $white !default;
@@ -2140,7 +2150,7 @@ $form-switch-disabled-color: light-dark(var(--gray-400), var(--gray-600)) !defau
$form-range-track-width: 100% !default;
$form-range-track-height: 0.25rem !default;
$form-range-track-cursor: pointer !default;
-$form-range-track-bg: var(--border-color) !default;
+$form-range-track-bg: var(--fill-primary) !default;
$form-range-track-border-radius: 1rem !default;
$form-range-track-box-shadow: var(--box-shadow-inset) !default;
diff --git a/core/scss/layout/_root.scss b/core/scss/layout/_root.scss
index eea0b9ccc..6cce81752 100644
--- a/core/scss/layout/_root.scss
+++ b/core/scss/layout/_root.scss
@@ -50,6 +50,12 @@
--icon-color: #{$icon-color};
+ --fill-primary: #{$fill-primary};
+ --fill-secondary: #{$fill-secondary};
+ --fill-tertiary: #{$fill-tertiary};
+ --fill-quaternary: #{$fill-quaternary};
+ --fill: var(--fill-primary);
+
--active-bg: #{$active-bg};
--hover-bg: #{$hover-bg};
diff --git a/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap b/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap
index 72718e27f..a5692898f 100644
--- a/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap
+++ b/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap
@@ -62,6 +62,7 @@ exports[`css custom-property prefixing > leaves vendor-owned names alone and pre
"--tblr-control-padding-y",
"--tblr-control-placeholder-color",
"--tblr-danger",
+ "--tblr-fill-primary",
"--tblr-focus-ring",
"--tblr-focus-ring-color",
"--tblr-focus-ring-offset",
diff --git a/core/scss/tests/fill-colors.test.mjs b/core/scss/tests/fill-colors.test.mjs
new file mode 100644
index 000000000..19f76f3c1
--- /dev/null
+++ b/core/scss/tests/fill-colors.test.mjs
@@ -0,0 +1,32 @@
+// Guards the neutral fill tokens. They are tuned to match the design file, so
+// a lost light-dark() pair or a changed amount shifts badges, avatars, code,
+// progress tracks and the hover background without any other test noticing.
+import { describe, expect, it } from 'vitest'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { compile as compileSass } from 'sass'
+
+const scssDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
+const { css } = compileSass(path.join(scssDir, 'tabler.scss'), { loadPaths: ['node_modules'], style: 'expanded' })
+
+// Custom properties are bare here: the `--tblr-` prefix is a build-time postcss pass.
+const flat = css.replace(/\s+/g, ' ')
+
+describe('neutral fills', () => {
+ it('declares the four levels and the --fill alias', () => {
+ expect(flat).toContain('--fill-primary: color-mix(in oklab, var(--body-color) 16%, transparent);')
+ expect(flat).toContain('--fill-secondary: color-mix(in oklab, var(--body-color) 8%, transparent);')
+ expect(flat).toContain('--fill-tertiary: light-dark(color-mix(in oklab, var(--body-color) 4%, transparent), color-mix(in oklab, var(--body-color) 6%, transparent));')
+ expect(flat).toContain('--fill-quaternary: color-mix(in oklab, var(--body-color) 2%, transparent);')
+ expect(flat).toContain('--fill: var(--fill-primary);')
+ })
+
+ it('points the hover background at the tertiary fill', () => {
+ expect(flat).toContain('--hover-bg: var(--fill-tertiary);')
+ })
+
+ it('ships .bg-fill as an alias of .bg-fill-primary', () => {
+ expect(flat).toContain('.bg-fill { background-color: var(--fill) !important; }')
+ expect(flat).toContain('.bg-fill-quaternary { background-color: var(--fill-quaternary) !important; }')
+ })
+})
diff --git a/core/scss/ui/_badges.scss b/core/scss/ui/_badges.scss
index a29128215..df9125e84 100644
--- a/core/scss/ui/_badges.scss
+++ b/core/scss/ui/_badges.scss
@@ -33,7 +33,7 @@
@include border-radius(var(--badge-border-radius));
@at-root a#{&} {
- background: var(--bg-surface-secondary);
+ background: var(--fill-secondary);
}
.icon {
diff --git a/core/scss/ui/_chat.scss b/core/scss/ui/_chat.scss
index d9a1633d6..8f9d5dbe9 100644
--- a/core/scss/ui/_chat.scss
+++ b/core/scss/ui/_chat.scss
@@ -17,7 +17,7 @@
.chat-bubble {
position: relative;
padding: var(--chat-bubble-padding);
- background: var(--bg-surface-secondary);
+ background: var(--fill-tertiary);
@include border-radius(var(--border-radius-lg));
}
diff --git a/core/scss/utils/_colors.scss b/core/scss/utils/_colors.scss
index c3d86e062..913f99cb6 100644
--- a/core/scss/utils/_colors.scss
+++ b/core/scss/utils/_colors.scss
@@ -61,6 +61,17 @@
}
}
+// Fills
+.bg-fill {
+ background-color: var(--fill) !important;
+}
+
+@each $level in (primary, secondary, tertiary, quaternary) {
+ .bg-fill-#{$level} {
+ background-color: var(--fill-#{$level}) !important;
+ }
+}
+
@each $color, $value in $theme-colors {
.text-#{'' + $color} {
--text-opacity: 1;
diff --git a/docs/content/ui/base/colors.mdx b/docs/content/ui/base/colors.mdx
index 6657d4791..3b43762e0 100644
--- a/docs/content/ui/base/colors.mdx
+++ b/docs/content/ui/base/colors.mdx
@@ -1,6 +1,6 @@
---
title: Colors
-summary: The Tabler color palette with base colors, light variants, the gray scale and social brand colors, each with background and text utilities.
+summary: The Tabler color palette with base colors, light variants, neutral fills, the gray scale and social brand colors, each with background and text utilities.
description: The Tabler color palette, with base and light shades, the gray scale and social brand colors.
---
@@ -20,6 +20,24 @@ Every base color also has a light shade with the `-lt` suffix. It works as a bac
+## Fill colors
+
+Fills are background tints for controls and small surfaces. They are the text color over a transparent background, so the same fill reads the same on the page, a card, a dropdown or a modal, in both color modes. Use them from the strongest to the faintest:
+
+- `--tblr-fill-primary` (16%) for the inactive track of a slider and other strong neutral shapes. `--tblr-fill` is an alias for it.
+- `--tblr-fill-secondary` (8%) for progress tracks and the hover state of neutral badges.
+- `--tblr-fill-tertiary` (4% in light mode, 6% in dark) for neutral badges, avatars, code and the hover state of quiet controls. `--tblr-hover-bg` points at it.
+- `--tblr-fill-quaternary` (2%) for faint section backgrounds.
+
+
+
+
+
## Gray palette
The gray scale is used for backgrounds, borders and muted text. Tabler ships several gray palettes and switches between them with `data-bs-theme-base`; see [theme base colors](/ui/plugins/theme-base-colors).