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

PairRatioAA body (4.5:1)AAA body (7:1)
#1c1f23 / #e6e8eb13.9:1passpass

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 via prefers-color-scheme plus 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.

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:a11y against a demo page using the theme and include the axe-core report in the merge request.
  • Tag the npm package with the krysalicss-theme keyword 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.