skcr-design-tokens
v1.4.0
Published
Design system SkillCorner : tokens de couleur, utilitaires typographiques et classes de composants. CSS pur, sans dépendance.
Readme
skcr-design-tokens
The SkillCorner design system, in pure CSS with no dependency: color tokens,
typographic utilities and component classes. Single source for the platform's applications,
and the rendering contract of the skcr-autechre component library.
No dependency, no build: the package publishes the .css files as they are. It
is therefore consumable by any front end — Angular, React or vanilla.
Installation
npm install skcr-design-tokensIn the application's styles.css (or styles.scss):
@import 'tailwindcss';
@import 'skcr-design-tokens/tokens.css';
@import 'skcr-design-tokens/utilities.css';
@import 'skcr-design-tokens/components.css';Applications consuming skcr-autechre
Tailwind does not scan node_modules. The classes the library components write
in their templates would therefore never be generated, and the styles would be
missing with no error to signal it. One line is enough:
@source '../node_modules/skcr-autechre/fesm2022/*.mjs';The path targets the built JavaScript, where ng-packagr has inlined the templates: that is where the classes live in the published package.
Light / dark theme
The attribute goes on <html>, never on <body>. Tailwind emits its
@theme block in :root, that is on <html>; and a custom property is
substituted at the element where it is declared. An override placed on <body>
would be invisible to the utilities, which would inherit a value already
resolved with the light palette — hand-written colors would flip, Tailwind
classes would not.
To avoid any flash on load, the attribute must be set before the first
paint. No application mechanism can do it: an Angular initializer runs after
the first render. Paste this script into index.html, in the <head>:
<script>
try {
document.documentElement.dataset.theme = localStorage.getItem('skcr-theme') || 'light';
} catch {
document.documentElement.dataset.theme = 'light';
}
</script>Angular applications then use skcr-autechre's ThemeService for toggling and
cross-tab synchronization. It does not do the initialization: the script above
has already done it.
Contents
| Entry | Contents |
| --- | --- |
| tokens.css | @theme block: primary, neutral, status and dataviz colors, and font families |
| utilities.css | text-body-*, text-caption, info-caption, title-one to title-five |
| components.css | .btn family, .badge family, .link, .step-number, hover tooltip, scrollbar utilities, Angular Material date-picker fix |
| display.css | Opt-in. Presentation classes. No platform application imports it. |
What stays in the application
Three categories do not move up here, deliberately:
- The
@font-facedeclarations. Shentox is a commercial font: redistributing it in a public npm package would be a license violation. The package declares the family (--font-title), the application provides the files. - The global resets.
html, body { height: 100vh; overflow: hidden }is a single-page-application decision, not a design decision. - Domain tokens.
--color-tactical: a business concept that serves a single application. The--color-tooltip-*tokens, on the other hand, do belong here: the tooltip is a library component, not an application concept.
Zones and states
A distinction to know before writing a class, because ignoring it produces a defect that is invisible in the light theme.
A zone token describes a surface: card, popover, muted, track. A
state token describes what happens to a surface when you interact with it:
hover for hovering, primary-subtle for selection, disabled.
The temptation is to express a state with a zone — writing hover:bg-muted
because the grey looks right. It works in light and breaks in dark, because a
state must move relative to its surface: a hover darkens on a light ground
and lightens on a dark one. A zone has an absolute value. The real case:
muted was exactly equal to popover in dark, and every hover:bg-muted on a
menu was strictly invisible — a contrast ratio of 1.00.
The three states read in this order, in both themes:
| On a popover | rest | hover | primary-subtle |
| --- | --- | --- | --- |
| light | 1.00 | 1.10 | 1.17 |
| dark | 1.00 | 1.41 | 2.05 |
Selection is therefore always more pronounced than hover. If you add a state, measure it against the surface that carries it, not in the absolute.
Extension rules
The distinction is sharp, and it is what keeps the design system from re-diverging the way it did across the three applications:
Overriding a value is free. An application may redefine any token in its own
styles.css, after the @import lines:
:root {
--color-primary: #0f766e;
}This is the dark theme's mechanism, and it is what lets a project repaint itself without forking the package. The contract stays the same, only the values change.
Defining a utility is not. Adding an @utility, a .btn-* class or a
presentation class in an application's styles.css is forbidden: that is
exactly how .btn-icon ended up in a single application, and how the .btn family
diverged on five points. A new definition goes through a merge request on this
package.
Adding to display.css. This is the file meant for new needs, in particular
those of projects with no validated design. It targets vibe-coded projects,
to which it gives a uniform facade; the platform applications have their own
needs, decided by product design, and do not import it. Three rules:
- A class only uses tokens from
tokens.css. No hard-coded color — that is what guarantees it will follow the dark theme with no rework. - A class never redefines a class from the base. When in doubt about a name,
check
components.css. - Do not write for the base. These classes are not meant to move up into it: this is not an antechamber. If one were ever to serve a component-based application, that would be an explicit, exceptional decision, not a drift.
The catalog (1.4.0, from the sapinadmin audit of 2026-09-02 and the
mutualisation pass of 2026-09-10) covers eight
families: surfaces (.card, .card-flush/-header/-body/-title,
.card-header-toggle, .collapsible), data
(.stat*, .kv*, .table*, .progress*, .dot*, .code, .code-block,
.output), tags (.eyebrow, .chip, .counter), forms (.field-label,
.field-hint, .input, .switch, .segmented), navigation (.tabs/.tab,
.menu/.menu-item, .nav-item), feedback (.callout*, .empty*, .loader, .skeleton,
.toast*), overlay (.modal-*) and avatar (.avatar*). The class carries the
look; the state (.active, aria-selected, [open], :checked) stays in the
project's JS. One name is reserved: never .spinner — it collides with the
encapsulated spinner of skcr-submit-button; the loading class is named
.loader. Three surface nuances are worth knowing: .card-body stacks —
it is flex flex-col, and the caller adds the gap-* — because 19 of the
first consumer's 24 card bodies were adding that column by hand, with the
consequence that a bare <span> or <a> dropped straight into a body is
blockified and spans its full width, so wrap it when the hit area or a
background has to stop at the text; .card-header
carries NO cursor — a header that actually collapses its card adds .card-header-toggle
on a <button> — and .collapsible wraps the body to unfold it with a
transition instead of a jump, the content staying mounted (a panel whose
content must unmount keeps an @if inside the region, and a collapsed region
takes inert). The doc's "Display" page (npm run doc) renders every family
in both themes.
