| name | core-scss |
| description | Write or change styles in `core/scss/` — the framework itself. Use whenever a component needs a new class, modifier, size or colour variant, when a Sass or CSS custom property is added or renamed, when dark mode or RTL behaviour is involved, and before touching `_variables.scss`, `_props.scss` or anything under `core/scss/ui/`. Covers where a style goes, the custom-property pattern and its build-time `--tblr-` prefix, dark mode, RTL, the docs markers, the SCSS unit tests and the lint/size gates. |
Styles in core/scss
core/scss is the product: a class that ships here is public API for every Tabler user. Components in shared/ui only assemble the class names defined here.
1. Where a style goes
| Path | Holds |
|---|
ui/_*.scss | components — .badge, .card, .steps, one file per component |
layout/_*.scss | page chrome — root, core, navbar, page, footer, dark, animations, accessibility |
utils/_*.scss | utility classes — colors, background, text, sizing, shadow, opacity, scroll, hover |
mixins/, helpers/ | mixins, functions, the utilities API |
bootstrap/ | Tabler's managed copies of Bootstrap's own partials |
vendor/ | overrides for third-party plugin CSS |
tests/ | sass-true unit tests (see section 7) |
Entry points: tabler.scss (which forwards _core.scss, then _extends.scss last), plus the standalone bundles tabler-flags, tabler-marketing, tabler-payments, tabler-props, tabler-socials, tabler-themes, tabler-vendors. A new partial is not compiled until it is @forwarded from _core.scss (or the bundle it belongs to).
The module graph uses @use / @forward: a partial starts with @use '../config' as *, which is the hub forwarding settings, variables, variables-dark, maps, mixins and utilities. Cross-module @extend rules must stay in _extends.scss, which loads last.
2. The component pattern
@use '../config' as *;
.badge {
--badge-padding-x: #{$badge-padding-x};
--badge-font-size: #{$badge-font-size};
--badge-line-height: 1;
display: inline-flex;
padding: var(--badge-padding-y) var(--badge-padding-x);
font-size: var(--badge-font-size);
@include border-radius(var(--badge-border-radius));
}
- Every themeable value becomes a custom property declared at the top of the component's root rule, seeded from a Sass variable (
#{$badge-font-size}). Declarations below read var(--badge-*), never the Sass variable directly — that is what lets users retheme without recompiling.
- A value with no reason to be overridden can be a literal (
--badge-line-height: 1).
- Modifiers set custom properties rather than redeclaring properties:
.badge-sm { --badge-font-size: … }.
- Sass variables go to
_variables.scss with !default, dark-mode counterparts to _variables-dark.scss.
3. Custom properties are authored bare
Write --badge-bg, not --tblr-badge-bg. The public --tblr- prefix is added at build time by .build/css-var-prefix.ts (a postcss pass in build-css.ts).
The consequence to remember: names owned by third-party libraries must not be prefixed. cssVarIgnore lists them (--bs-, --fc-, --gl-, --litepicker-, --plyr-, --ts-, …). Prefixing one detaches the theming with no error anywhere — the library keeps reading its own name and simply never sees the value. When a vendor override introduces a new foreign name, add it to cssVarIgnore; core/scss/tests/css-var-prefix.test.mjs snapshots every custom property of tabler-vendors.scss, so a missing entry shows up as a --tblr--prefixed foreign name in the snapshot diff.
Global properties (--dir, colours, fonts, spacing) live in _props.scss, which emits them on :root, :host.
4. Dark mode
- Colour pairs are expressed with
light-dark() where possible (_variables.scss, layout/_root.scss), so one declaration covers both modes.
- What cannot be expressed that way goes to
_variables-dark.scss, or to layout/_dark.scss for the visibility helpers.
- Dark mode is keyed on
.theme-dark, [data-bs-theme='dark'] and [data-theme='dark'] — match all three when you add a selector, and keep the whole block behind @if $enable-dark-mode.
5. RTL
RTL stylesheets are generated by rtlcss in build-css.ts (--rtl), so do not hand-write RTL overrides. Two rules:
- Prefer logical properties (
padding-inline-start, inset-inline-end) — rtlcss then needs no help.
- A physical transform that must flip uses the
--dir multiplier (translateX(calc(var(--dir) * -50%))), and the declaration is marked /* rtl:ignore */ so rtlcss does not negate an already-correct calc. Both patterns are in _utilities.scss and ui/_steps.scss.
6. Docs markers
Snippets shown on documentation pages are pulled from the source with markers, so the docs cannot drift:
$alert-padding-y: … !default;
docs/components/CodeDocs.astro and docs/lib/llms.ts read these. When you rename or move a marked block, check who references the marker name before deleting it.
7. Unit tests (sass-true)
core/scss/tests/*.test.scss are real unit tests over mixins and functions, auto-discovered by core/scss/tests/scss.test.mjs and run through vitest:
pnpm --filter @tabler/core test:scss
pnpm --filter @tabler/core test
Add a test when you write a mixin whose output is easy to break silently — the _cards.test.scss case (a 0% that must keep its unit or the whole color-mix() drops) is the model. Note that stylelint deliberately ignores core/scss/tests/**: autofix there would rewrite the assertions.
8. Gates
pnpm run lint:scss
pnpm --filter @tabler/core css-lint
pnpm run generate-tokens:check
pnpm run lint-prettier
pnpm run bundlewatch
- A new entry in
$theme-colors, $avatar-sizes and friends must be regenerated into shared/lib/tokens.ts with pnpm run generate-tokens — the check gate fails otherwise.
- An unused Sass variable fails
css-lint; delete it or use it.
- Growth past a bundlewatch limit is a decision, not an accident: raise the number in
core/package.json deliberately and say so in the PR.
9. Checklist