Author and publish a theme
Use this when you want to ship a Krysalicss theme that other projects can install from npm: a brand kit, a community palette, an accessibility-tuned variant.
This guide assumes you already know the theme factory shape. For the in-app, single-consumer recipe see Author a custom theme; for the contract this page validates against see the Theme contract reference.
1. Pick a palette
A theme is a value for every key in
the theme contract:
bg, fg, link, link-hover, focus-ring, border-radius,
card-bg, card-fg (in $variables), plus a (bg, fg) tuple for
each of default, primary, warning, danger, success
(in $combinations).
WCAG AA targets
Every (bg, fg) pair you author has to clear:
- 4.5:1 for body text: WCAG 2.2 AA, 1.4.3.
- 3:1 for non-text UI (button outlines, focus rings, large text ≥ 18.66 px bold or 24 px regular): WCAG 2.2 AA, 1.4.11.
AAA targets are 7:1 (text) and 4.5:1 (non-text); aim for them when the
theme is positioned as accessibility-first. The bundled
high-contrast.scss clears AAA on every pair: use it as the AAA-grade
reference.
Verifying a pair
Use any APCA-aware checker: for example webaim.org/resources/contrastchecker or your browser's built-in DevTools contrast lens. Enter the two hex values, read the ratio, compare against the target.
Worked example for a (card-bg, card-fg) pair of #1c1f23 / #e6e8eb
(the bundled dark.scss card surface):
| Pair | Ratio | AA body (4.5:1) | AAA body (7:1) |
|---|---|---|---|
#1c1f23 / #e6e8eb | 13.9:1 | pass | pass |
Repeat for all 7+ pairs your map declares. Reject any pair that misses the AA target before moving on: the framework cannot recover contrast that the palette did not bake in.
2. Write the theme file
Single SCSS file, one @include create.theme(...) call. Use the
canonical shape:
// src/theme.scss
@use '@adnap/krysalicss/variables';
@use '@adnap/krysalicss/theme/create';
$variables: (
bg: #fff,
fg: #1a1a1a,
link: #1f6e51,
link-hover: #6b7330,
border-radius: variables.$global-border-radius,
card-bg: #fff,
card-fg: #1a1a1a,
focus-ring: #1f6e51,
);
$combinations: (
default: (#f1f3f5, #1a1a1a),
primary: (#1f6e51, #fff),
warning: (#ecf241, #1a1a1a),
danger: (#c0392b, #fff),
success: (#2a8d69, #fff),
);
@include create.theme(
$name: 'sunset',
$variables: $variables,
$combinations: $combinations,
$is-default: false,
$modifier-class: ':root.theme-sunset',
);
Templates to crib from in the framework source:
src/theme/default.scss: light, OS-default, with class override.src/theme/dark.scss: auto-switching viaprefers-color-schemeplus an explicit class.src/theme/high-contrast.scss: opt-in only, AAA contrast.
3. Validate
The factory does not validate your maps — partial themes compile
without error. A token absent from $variables or $combinations
falls through to the framework's compile-time default, which reflects
the default palette rather than yours. Audit your build output
once, confirm every token you intended to override appears in the
emitted block, and that no var(--kc-*, …) consumer in a rendered
page falls back to a value from the wrong palette.
For palette validation, run every (bg, fg) pair through your contrast
checker as in step 1. There is no automated WCAG check in the framework
build; that responsibility lives with the theme author.
4. Package shape
Recommended npm name: @<scope>/krysalicss-themes-<name> (e.g.
@acme/krysalicss-themes-sunset). The krysalicss-themes- infix makes
your package discoverable on npm search and groups multiple themes from
the same publisher.
Minimum file layout:
.
├── package.json
├── README.md
├── src/
│ └── theme.scss # the @include create.theme(...) call
└── dist/
└── theme.css # compiled, committed by your build script
package.json template:
{
"name": "@acme/krysalicss-themes-sunset",
"version": "0.1.0",
"description": "Sunset theme for Krysalicss.",
"type": "module",
"license": "MIT",
"files": ["src", "dist", "README.md"],
"exports": {
".": {
"sass": "./src/theme.scss",
"style": "./dist/theme.css",
"default": "./dist/theme.css"
},
"./theme.scss": "./src/theme.scss",
"./theme.css": "./dist/theme.css"
},
"sass": "./src/theme.scss",
"style": "./dist/theme.css",
"peerDependencies": {
"@adnap/krysalicss": "^0.1"
},
"devDependencies": {
"@adnap/krysalicss": "^0.1",
"sass-embedded": "^1.77.0"
},
"scripts": {
"build": "sass --no-source-map --style=compressed src/theme.scss dist/theme.css",
"prepublishOnly": "pnpm build"
}
}
The sass export condition lets Sass consumers resolve directly to the
source; style and the default export serve compiled CSS to bundlers
and <link> tags.
5. Publish
pnpm build
pnpm publish --access public
--access public is required for first-time publish of a scoped
package; without it npm rejects the upload as a private package on the
free tier.
Verify in a fresh project:
mkdir /tmp/verify && cd /tmp/verify
pnpm init
pnpm add @adnap/krysalicss @acme/krysalicss-themes-sunset
// app.scss
@use '@adnap/krysalicss';
@use '@acme/krysalicss-themes-sunset/theme.scss';
Build, open in a browser with <html class="theme-sunset">, confirm
var(--kc-bg) resolves to your palette.
6. Optional: design-token interop
The W3C Design Tokens Community Group format
(design-tokens.github.io/community-group)
is the emerging interchange format for cross-tool palettes (Figma,
Style Dictionary, Tokens Studio). Krysalicss consumes plain Sass maps
today and does not ship a tokens.json import path: exporting your
theme to that format is a nice-to-have, not a requirement.
For the bundled themes the framework already emits W3C DTCG JSON via
pnpm tokens:build (auto-discovered from any file under
src/theme/):
pnpm tokens:build
# → dist/tokens/{default,dark,high-contrast}.json (W3C DTCG)
A first-class Style Dictionary build target is being explored and will be documented separately if it ships.
7. Submission guidelines (community gallery)
To get a community theme listed in the docs gallery, the package has to clear five checks:
- Every
(bg, fg)pair clears WCAG 2.2 AA: 4.5:1 for body text, 3:1 for non-text UI. AAA (7:1) preferred when the theme is positioned as accessibility-first. - Run
pnpm test:a11yagainst a demo page using the theme and include the axe-core report in the merge request. - Tag the npm package with the
krysalicss-themekeyword so npm search surfaces it alongside the bundled set. - Include preview screenshots in the package README: at minimum, one shot of the demo page at desktop width.
- Use the recommended scoped name shape (see step 4):
@<scope>/krysalicss-themes-<name>— e.g.@acme/krysalicss-themes-sunset.
Once the package is on npm, open a merge request against the docs gallery with the package name, a one-line description, and a link to the README.
Related
- Author a custom theme: the local-consumer recipe (single project, no npm).
- Theme contract reference: required keys, mixin signature, error semantics.
- Theme architecture: why Krysalicss ships three bundled themes and which contexts each one targets.
- Themes gallery: live preview of every bundled theme, plus the community-themes hub.
--kc-*tokens reference: what each token drives.- Color combinations: how the
(bg, fg)tuples flow into Button and Card.