Your repository

What Kampa writes into your repository, how it gets there, and what it deletes. Everything here describes what Kampa does today. Where something is not built yet, the page says "not yet".

What a push writes

Every push is one commit on a new branch, opened as a pull request against the project's base branch. The commit writes these paths:

Path What it is
<tokenPath>/<mode>.json One DTCG 2025.10 file per mode. The files to review.
<tokenPath>/manifest.json The list of modes and component documents this push wrote. Generated.
<tokenPath>/tokens.css CSS custom properties for every mode. Generated.
<tokenPath>/tokens.ts A TypeScript map of every mode. Generated.
<tokenPath>/components/ Component documents, only in a push that carries components.
<tokenPath>/components.css CSS for every format 0.3 and 0.4 component. Generated, only in a push that carries one.
<tokenPath>/components/<name>.html An HTML snippet for each format 0.3 and 0.4 component. Generated, beside its document.
<tokenPath>/base.css Base styles for the page, from your tokens. Generated, only beside a format 0.4 component, and only when every token it names exists in every mode. Nothing loads it unless you do.
<tokenPath>/components/LICENSE-icons.txt The licence of the icons in the snippets (Lucide). Generated, only when a snippet carries an icon.
schemas/component/<version>/ The component format those documents follow, only beside them.
.kampa/check.mjs The Kampa check. Plain Node.js, no dependencies.
.kampa/README.md The setup page: connect, rails, protecting main, blocked pushes.
.gitattributes Only when it is missing a line below, which is then added.

<tokenPath> is the token folder set on the project, tokens unless you chose another. Every path above that starts with it moves with it.

.kampa/check.mjs and .kampa/README.md are rewritten on every push, so they always match the Kampa that pushed. When the checker changed since the last push, the pull request says so.

In a repository set up when Kampa still wrote to .alloy/, the two files are there. They stay there while any file under .github/workflows/ needs .alloy/check.mjs: it names .alloy/check.mjs and not .kampa/check.mjs. The workflow Kampa offers now names both, because it runs whichever the repository has, so it never holds them back. Until then, each pull request opens with the offer to add kampa-check.yml and then delete the old workflow, in that order, both green. The first push after no workflow needs .alloy/check.mjs moves the files to .kampa/ and deletes .alloy/ in the same commit, under Renamed in the pull request. The two folders are never side by side, and Kampa never deletes a check a workflow needs.

Kampa marks its generated files as generated, so GitHub collapses them in the pull request diff and the mode files are what a reviewer reads:

tokens/tokens.css linguist-generated=true
tokens/tokens.ts linguist-generated=true
tokens/manifest.json linguist-generated=true

A push that writes components.css adds two more, and one that writes base.css or the icon licence adds a line for each:

tokens/components.css linguist-generated=true
tokens/components/*.html linguist-generated=true
tokens/base.css linguist-generated=true
tokens/components/LICENSE-icons.txt linguist-generated=true

If .gitattributes exists, Kampa keeps every line in it byte for byte and appends only the lines that are missing. If they are all there, the push does not touch the file. A project with no format 0.3 or 0.4 component never gets the component lines, and one with no 0.4 component never gets the base.css line.

manifest.json

{
  "name": "kampa-tokens",
  "format": "dtcg-2025.10",
  "version": "1.0.0",
  "generatedAt": "2026-09-27T19:49:16.567Z",
  "wcagLevel": "AA",
  "collections": [
    {
      "name": "Kampa",
      "modes": {
        "dark": "dark.json",
        "light": "light.json"
      }
    }
  ],
  "components": [
    {
      "name": "Button",
      "file": "components/button.json",
      "resolver": "components/button.resolver.json",
      "project": "bc15229fd0160a19"
    }
  ],
  "generated": [
    { "file": "components.css", "project": "bc15229fd0160a19" },
    { "file": "components/button.html", "project": "bc15229fd0160a19" }
  ]
}
  • collections[0].modes maps each mode to its file, sorted by mode name. A mode's file is always <mode>.json, beside the manifest.
  • wcagLevel is the level the check ran at for this push, AA or AAA.
  • components is there only when the push wrote component documents. Paths are relative to <tokenPath>. project is an opaque id for the Kampa project that pushed the entry, a hash, not a database key.
  • generated is there only when the push wrote components.css and the snippets, which Kampa generates from format 0.3 and 0.4 components, and lists base.css and the icon licence when the push wrote them. Each entry carries the same project id.
  • generatedAt changes on every push. Nothing else in the file does unless the modes, the components or the level changed.

The manifest is how Kampa knows which files in your repository are its own. Kampa reads its mode set and its component list from the manifest on the base branch, never from a directory listing. A directory can hold files Kampa never wrote. The manifest only names files a Kampa push wrote. That is why a push only deletes what a manifest names (see below), and why reading a repository back into Kampa starts from the manifest when there is one.

The mode files

Each <mode>.json is a standalone DTCG 2025.10 document that any DTCG tool can open. There is no wrapper holding several modes, because the format has none.

An example, shortened:

{
  "$extensions": {
    "kampa": {
      "meta": {
        "contrastExemptions": {
          "color.foreground.disabled|color.background.default|AA": {
            "reason": "Inactive UI component text, exempt per WCAG 2.2 SC 1.4.3"
          }
        }
      }
    }
  },
  "color": {
    "background": {
      "default": {
        "$type": "color",
        "$extensions": {
          "kampa": {
            "meta": {
              "role": "background",
              "group": "surface"
            }
          }
        },
        "$value": "{color.palette.gray.0}"
      }
    },
    "palette": {
      "gray": {
        "0": {
          "$type": "color",
          "$value": { "colorSpace": "srgb", "components": [1, 1, 1], "hex": "#ffffff" }
        }
      }
    }
  },
  "spacing": {
    "md": {
      "$type": "dimension",
      "$value": {
        "value": 16,
        "unit": "px"
      }
    }
  },
  "typography": {
    "body": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Inter, sans-serif",
        "fontSize": {
          "value": 16,
          "unit": "px"
        },
        "fontWeight": 400,
        "letterSpacing": {
          "value": 0,
          "unit": "px"
        },
        "lineHeight": 1.5
      }
    }
  }
}

One canonical shape

Kampa writes every mode file the same way, whatever it was given:

  • Two-space indent.
  • Group children sorted by name.
  • On each token and group, $type, $description, $deprecated and $extensions come first, in that order. On a token, $value is last. A group's own $root token comes after the group's metadata and before its children.
  • Inside a value, members are in the order DTCG 2025.10 gives them: a colour's colorSpace, components, alpha, hex; a dimension's value, unit; a typography value's fontFamily, fontSize, fontWeight, letterSpacing, lineHeight. A file Kampa pushed before wrote some of them in another order; the next push writes them in this one, once, and the pull request says so.
  • A colour value is written on one line, so changing a colour is a one-line diff and its hex sits next to its components: "$value": { "colorSpace": "srgb", "components": [1, 1, 1], "hex": "#ffffff" }. Every other value keeps the plain two-space form.
  • A newline at the end, as on every file Kampa writes. In a repository Kampa pushed to before it wrote one, the next push adds it to each file once.

The same content always produces the same bytes, so changing one value is a one-line diff.

Types

Every token carries its own $type. Kampa does not leave a token to inherit its type from a group (DTCG 2025.10 §6.3). When Kampa reads a file that relies on a group's $type, it writes that type onto each token and keeps the group's $type where it was. The first pull request after that lists the rewrite under Rewritten by Kampa.

Values

  • Dimensions are objects, { "value": 16, "unit": "px" }. The unit is px or rem, the two DTCG defines. Kampa writes no other unit. A string dimension such as "16px" in a file Kampa reads, anywhere in the file, is rewritten as an object, and the pull request says so. An em letter spacing is converted to its font size's unit: 0.02em at 16px is written as 0.32px. A dimension Kampa can't read, such as "medium", is never guessed at: Connect refuses it, naming the token, the mode and the string, and the check fails it (E008).
  • Colours are objects, as DTCG 2025.10 defines them: { "colorSpace": "srgb", "components": [0.1137, 0.3059, 0.8471], "hex": "#1d4ed8" }.
    • The colour space is srgb. Each component is a number from 0 to 1, kept to 4 decimals, which holds every 8-bit colour exactly.
    • alpha is written only below 1: "alpha": 0.502 is 80 in hex.
    • hex is always written, six digits, lowercase, and it is the components rounded to bytes. The components are the colour: they are what tokens.css renders and what the check measures. A hex that says something else fails the check (E007); a colour with no hex is valid, and Kampa adds one on its next push.
    • A colour in another space, such as oklch, is kept exactly as written. Kampa shows it read-only and does not convert it. Kampa can't write it into tokens.css yet, so a push that holds one stops, naming the token (E009). Replace it in the code panel, or remove the token.
    • A hex string, "#ffffff", in a file Kampa reads is rewritten as an object on the next push, and the pull request says so. Until then the check reads it as before.
  • Typography is a composite. Its fontSize and letterSpacing are dimension objects, { "value": 17, "unit": "px" }, or aliases. A string such as "17px" in a file Kampa reads is rewritten as an object, and the pull request says so.
  • Aliases are DTCG references, "{color.palette.gray.0}".

Kampa's metadata

Everything Kampa records beyond DTCG's own keys is under $extensions.kampa.meta. On a token:

  • role: foreground, background or border, how the check pairs colours. none is written only when a role is removed from a token whose name would otherwise imply one. An older exempt role is removed when Kampa loads the project: exemptions are per pair now (below).
  • group: the values Kampa writes today are brand, neutral, semantic and surface. Setting a role by hand removes it.
  • excludeFromPairing and excludeFromPairingReason: a colour held out of the contrast check, and why. The reason is required.

Contrast exemptions sit at the root of each mode file, under $extensions.kampa.meta.contrastExemptions. Each key names the foreground token, the background token and the WCAG level, separated by |. Each value has a reason, which may be empty but is always present. The same block is in every mode file.

Other tools' $extensions namespaces are kept as they are. An older alloy namespace is read and rewritten as kampa.

tokens.css and tokens.ts

Both are generated from the mode files and cover every mode. Kampa builds them on its server at push time, from the project as it is saved; the editor saves any edit still pending before it pushes. Nothing the browser sends is written to your repository.

  • tokens.css has one custom property per token. The project's first mode is under :root. Every other mode is a [data-theme="<mode>"] block. A typography token is a font shorthand, and the shorthand has no place for letter spacing, so each one also gets --<name>-letter-spacing on the next line (--typography-label-md-letter-spacing: 0.14px;). The first push after Kampa started writing these adds those lines and changes no other, and its pull request says so in one sentence.
  • tokens.ts exports one tokens object nested by mode: tokens.light["color-brand-default"] is { value: "#1d4ed8", type: "color" }. A colour is its hex string, eight digits when it is translucent. A typography value's fontSize and letterSpacing are strings such as "16px", an aliased one included. The mode files hold objects; tokens.ts keeps this shape so code that reads it does not change.

The project's modes are in the project's order. Everything inside a mode is in one canonical order, the order the mode files use, so the same content always gives the same bytes. Every mode key in tokens.ts is quoted, "light": {, so a mode such as high-contrast compiles. A token's $description is written into tokens.css as a comment; a */ in it is written as *\/, so it can't end the comment early. The description in the mode file is unchanged.

The first push after Kampa started writing this order may move lines in both files without changing a value. Its pull request says so in one sentence: "no value changes".

What Kampa can write

Type Kampa writes
color An srgb colour object, or a hex string.
dimension { "value", "unit" } with a CSS unit, or a number with a unit.
fontFamily A name or a list of names.
fontWeight A number from 1 to 1000, or a DTCG weight name such as bold.
duration A string such as "200ms" or "0.2s".
cubicBezier Four numbers.
number A number.
string Plain text.
boolean true or false.
strokeStyle A keyword such as solid, or { "dashArray", "lineCap" }.
border Its color, width and style, each as above.
shadow A layer or a list of layers: color, offsetX, offsetY, blur, spread, and inset.
typography fontFamily, fontSize, fontWeight, letterSpacing and lineHeight (a number), each as above.

Any member may be an alias instead. Text is written as it stands, so it can't contain ;, {, }, \, !, <, a comment mark, a line break, or an unclosed quote or bracket. Kampa can't write a duration, transition or gradient object, or a colour in a space other than srgb, yet. A value Kampa can't write fails the check (E009), and the push stops on either rail, naming it.

Edit the mode files, or better, the project in Kampa. An edit to a generated file is overwritten by the next push.

Component documents

Only a push that carries components writes these:

  • <tokenPath>/components/<name>.json, the component, exactly as the project stores it.
  • <tokenPath>/components/<name>.resolver.json, its resolver document. The theme contexts point at ../<mode>.json, built from the project's modes at push time.
  • schemas/component/<version>/component.schema.json and SEMANTICS.md, one folder per format version the pushed documents use.
  • For format 0.3 and 0.4 components: <tokenPath>/components.css, the CSS for all of them, and <tokenPath>/components/<name>.html, a snippet showing the component and each variant. Kampa generates both on its server from the stored documents, the same ones the check graded, and overwrites them on every push. The Using components page explains how to load them.
  • Beside a format 0.4 component: <tokenPath>/base.css, page defaults from your tokens in the cascade layer kampa.base, written only when every token it names exists in every mode (the pull request says why when it is not), and <tokenPath>/components/LICENSE-icons.txt when a snippet carries an icon. A 0.4 snippet holds placeholder ids, names and labels: replace them, and keep each unique on the page.

Every document is validated against the published format before anything is written. One invalid document stops the whole push, with nothing written.

How a push gets there

Kampa refuses the push, with nothing written, when:

  • the project has no tokens;
  • the editor can't save an edit before the push. Push again once it saves;
  • Kampa can't write tokens.css and tokens.ts from the project: a reference that leads nowhere, an unknown $type, a value Kampa can't write, or two tokens that become one CSS name (E001, E002, E008, E009, E010). This holds on both rails, whatever Block push on check failure says;
  • a mode's name can't be written as a file name, a CSS selector and a tokens.ts key. A mode name starts with a letter or a digit, then has only letters, digits, spaces, dots, underscores and hyphens, up to 64 characters, with no trailing space or dot and no ... So Light, Mode 1, dark_mode and high-contrast push; a name with a slash, a quote or a bracket doesn't. The message names the mode and the rule;
  • the check fails and Block push on check failure is on, which is the default. The editor names every failing pair. See .kampa/README.md, section 5;
  • a component document breaks the published format;
  • the base branch changed since Kampa last agreed with it, in a way the project doesn't have. Kampa names each changed token and offers Take the repo's version, or Resolve when the same token changed in Kampa too: a pick per token, Kampa's value or the repository's. A push after a pick of Kampa's value writes it over the repository's on purpose. See .kampa/README.md, "When the files here change";
  • Kampa can't read the token folder on the base branch as a project, for example a token that is in one mode file and not the others.

A project with no recorded commit yet, one last pushed before Kampa recorded it, is not stopped by a change on the base branch. Its pull request lists every repository value it replaced under Replaced in the repository.

If the base branch already holds exactly what the push would write (the mode files, tokens.css, tokens.ts and any component documents), nothing is pushed: no branch, no pull request.

Otherwise Kampa:

  1. Creates a branch named kampa/<project-name>-<timestamp> at the base branch commit it just checked. Every read the push makes is at that commit, even if the base branch moves meanwhile. The project name is lowercased, each run of characters other than letters and digits becomes one -, and it is cut at 40 characters: a project named "Corporate Template" pushes from kampa/corporate-template-1790538554434. The timestamp is in milliseconds.
  2. Writes one commit on that branch with every file above and every deletion below. The message is chore(tokens): update tokens for <project> [kampa], with the exempted pairs appended when there are any.
  3. Opens a pull request from that branch into the base branch, titled chore(tokens): <project>, <modes>, <count> tokens, where <modes> is the mode names separated by commas, or <mode> mode when there is only one.
  4. Records that base commit as the one it last agreed with the repository on.

The pull request description starts with a plain summary of what changed, then the check result, the change note if one was written, every file written and deleted, any file left in place, and whether the check workflow is installed.

Rail 1 and Rail 2

The rail is chosen when the project is created and cannot change.

  • Auto-merge (Rail 1). Kampa merges the pull request straight away with a squash merge. If GitHub refuses the merge, for example because main requires a check that has not reported yet, Kampa shows merge blocked and the pull request stays open for anyone to merge.
  • Review first (Rail 2). Kampa opens the pull request and stops. A person reviews and merges it. A project started from Connect is set to Review first unless you pick otherwise.

Kampa's branches

Kampa removes the branches it creates once they are done with:

  • Auto-merge (Rail 1). Right after Kampa merges the pull request, it deletes the branch. If GitHub refuses the merge, the branch and its open pull request stay for a person to merge.
  • Review first (Rail 2). A person merges, so Kampa can't delete the branch then. At the start of each push, Kampa deletes its own older branches: a branch whose name starts with kampa/, whose pull requests were all opened by the Kampa GitHub App, and none of which is still open. It looks at up to 50 per push, oldest first. To remove each branch as soon as it merges, turn on Automatically delete head branches in the repository's settings.

Kampa never deletes a branch with an open pull request, a kampa/ branch with no pull request, a branch whose pull request someone else opened, or any branch outside kampa/. A push made with a personal access token instead of the GitHub App deletes no older branches. If a delete fails, the push carries on.

An empty repository

A repository with no commits has no branch to open a pull request against. Kampa asks first, then makes one commit directly on the base branch, a stub README.md, and continues with the normal branch and pull request. This is the only commit Kampa ever makes directly on a base branch.

What a push deletes

Kampa deletes a file only when the manifest on the base branch names it as Kampa's, and the project no longer has it:

  • A removed mode. The manifest lists the mode and this push doesn't write it, so <tokenPath>/<mode>.json is deleted. Only that exact file name.
  • A removed component. The manifest lists the component under this project's project id and the project no longer has it, so its two documents are deleted. Only components/<name>.json and components/<name>.resolver.json, one level deep.
  • A generated file no longer generated. The manifest lists components.css, a snippet, base.css or the icon licence under generated with this project's id, and this push does not write it: the last format 0.3 or 0.4 component was removed, or that component was, or nothing needs the file any more. Only components.css, base.css, components/<name>.html and components/LICENSE-icons.txt, one level deep.

The deletions are in the same commit as the writes, and the pull request lists them under Removed modes and in the components section.

A file is never deleted when:

  • no manifest names it, for example a JSON file you added to <tokenPath>;
  • the manifest names it at a path other than the one Kampa writes;
  • Kampa could not check it on the base branch, or could not read the manifest;
  • another Kampa project pushed it. Its entries are carried into the new manifest unchanged;
  • it is a manifest entry written before Kampa recorded which project pushed it.

Each of these is named in the pull request under Left in place, or in the components section, so nothing stays quietly. .kampa/check.mjs checks every JSON file in <tokenPath>, so an unlisted file there keeps being checked until someone deletes it by hand.

A push never deletes manifest.json, tokens.css, tokens.ts, anything under schemas/ or .kampa/, or any path outside <tokenPath>, except the two files it moves from .alloy/ to .kampa/ (above). components.css, base.css, a snippet and the icon licence are deleted only as above. One written by a push from before Kampa recorded them under generated is left in place until a push records it.

What Kampa never does

  • Writes under .github/. The check workflow is yours to add.
  • Commits directly to your base branch, except the one stub README.md in an empty repository, after you confirm it.
  • Deletes a file no Kampa manifest names.
  • Pushes over a change on the base branch it hasn't read, once it has a commit it agreed with.
  • Force-pushes, or rewrites history.