---
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 `@forward`ed 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

```scss
@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-`, `--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.

The snapshot moves in the other direction too. A vendor override that starts reading a Tabler name (`var(--success)`, `var(--form-valid-border-color)`) adds a correctly prefixed `--tblr-*` entry, and the test fails until the snapshot is refreshed. Read the diff: every new name is one Tabler owns → `pnpm --dir core exec vitest run --config vitest.scss.config.mjs -u` and commit the `.snap`; a name a library owns → `cssVarIgnore`, never `-u`.

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:

```scss
// scss-docs-start alert-variables
$alert-padding-y: … !default;
// scss-docs-end alert-variables
```

`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:

```bash
pnpm --filter @tabler/core test:scss
pnpm --filter @tabler/core test        # js + scss
```

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

```bash
pnpm --filter @tabler/core lint:scss       # stylelint (twbs config; prettier owns formatting)
pnpm run lint:scss                         # + find-unused-sass-variables (`lint:scss:vars` in core)
pnpm run check:tokens             # shared/lib/tokens.ts must match the Sass maps
pnpm run lint:prettier                     # formatting
pnpm run bundlewatch                       # size budgets (tabler.css 80 kB, tabler.min.css 75 kB)
```

- 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 `lint:scss`; 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

- [ ] Partial in the right directory and `@forward`ed from `_core.scss` or its bundle
- [ ] Themeable values as custom properties at the top of the root rule, seeded from `!default` Sass variables
- [ ] Custom properties written bare; new foreign names added to `cssVarIgnore`
- [ ] Dark mode via `light-dark()` or all three dark selectors, behind `$enable-dark-mode`
- [ ] Logical properties, or `--dir` + `/* rtl:ignore */`; no hand-written RTL
- [ ] No class, custom property, Sass variable or mixin parameter removed or renamed without an alias (`backward-compat`)
- [ ] sass-true test for a mixin that can break silently
- [ ] `lint:scss`, `check:tokens`, `lint:prettier`, `test:scss` clean
- [ ] `check:compat` clean after a core build, with no new line in `.build/compat-baseline.txt`
- [ ] Docs page and class table updated (`write-docs`, `class-reference`), changeset written
