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].modesmaps each mode to its file, sorted by mode name. A mode's file is always<mode>.json, beside the manifest.wcagLevelis the level the check ran at for this push,AAorAAA.componentsis there only when the push wrote component documents. Paths are relative to<tokenPath>.projectis an opaque id for the Kampa project that pushed the entry, a hash, not a database key.generatedis there only when the push wrotecomponents.cssand the snippets, which Kampa generates from format 0.3 and 0.4 components, and listsbase.cssand the icon licence when the push wrote them. Each entry carries the sameprojectid.generatedAtchanges 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,$deprecatedand$extensionscome first, in that order. On a token,$valueis last. A group's own$roottoken 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'svalue,unit; a typography value'sfontFamily,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
hexsits next to itscomponents:"$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 ispxorrem, 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. Anemletter spacing is converted to its font size's unit:0.02emat16pxis written as0.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. alphais written only below 1:"alpha": 0.502is80in hex.hexis always written, six digits, lowercase, and it is the components rounded to bytes. The components are the colour: they are whattokens.cssrenders and what the check measures. Ahexthat says something else fails the check (E007); a colour with nohexis 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 intotokens.cssyet, 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.
- The colour space is
- Typography is a composite. Its
fontSizeandletterSpacingare 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,backgroundorborder, how the check pairs colours.noneis written only when a role is removed from a token whose name would otherwise imply one. An olderexemptrole is removed when Kampa loads the project: exemptions are per pair now (below).group: the values Kampa writes today arebrand,neutral,semanticandsurface. Setting a role by hand removes it.excludeFromPairingandexcludeFromPairingReason: 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.csshas 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 afontshorthand, and the shorthand has no place for letter spacing, so each one also gets--<name>-letter-spacingon 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.tsexports onetokensobject 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'sfontSizeandletterSpacingare strings such as"16px", an aliased one included. The mode files hold objects;tokens.tskeeps 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.jsonandSEMANTICS.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 layerkampa.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.txtwhen 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.cssandtokens.tsfrom 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.tskey. 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... SoLight,Mode 1,dark_modeandhigh-contrastpush; 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:
- 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 fromkampa/corporate-template-1790538554434. The timestamp is in milliseconds. - 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. - 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> modewhen there is only one. - 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>.jsonis deleted. Only that exact file name. - A removed component. The manifest lists the component under this
project's
projectid and the project no longer has it, so its two documents are deleted. Onlycomponents/<name>.jsonandcomponents/<name>.resolver.json, one level deep. - A generated file no longer generated. The manifest lists
components.css, a snippet,base.cssor the icon licence undergeneratedwith 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. Onlycomponents.css,base.css,components/<name>.htmlandcomponents/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.mdin 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.