Kampa component format 0.2
Status: 0.2, pre-release. The shape may change before a stable release. Nothing here promises compatibility across versions.
This file explains the component documents under tokens/components/: what
they are, how to read one, and what the Kampa check enforces on them.
component.schema.json beside it is the
machine-readable version. You need neither Kampa nor this file to read a
component: every file is plain DTCG.
Key words MUST, MUST NOT, MAY and SHOULD are used in the RFC 2119 sense. Section numbers like "Format §7" point to the DTCG 2025.10 Format and Resolver Module specifications.
1. What the format is
A component is two DTCG 2025.10 documents:
- The component file (
button.json): an ordinary DTCG token file holding one group. Each token in the group is a slot, and each slot is an alias to one of your semantic tokens. - The resolver document (
button.resolver.json): a DTCG Resolver Module 2025.10 document. It holds the component's axes (variant, state, theme) as modifiers, and says which slots each context repoints.
DTCG defines no component-level structure. The few keys Kampa needs live under
$extensions["com.kampa"], which Format §5.2.3 sets aside for vendor data.
§7 lists every one of them.
1.1 Versions
- A group with no
formatkey is on 0.1: colour slots only. - A group with
format: "0.2"is on 0.2: colour, dimension and typography slots.
Every valid 0.1 document is a valid 0.2 document with the same meaning. A
repository can hold schemas/component/0.1/ and schemas/component/0.2/ at
the same time. Each describes the documents on its own version. Kampa writes
the directory for every version in a push and never deletes one.
2. Where the files are
tokens/
light.json semantic tokens, one file per mode
dark.json
components/
button.json the component file
button.resolver.json the resolver document it names
schemas/component/0.2/ this file and the JSON Schema
The component file holds only the base slot set. It does not list variants or states. Every axis lives in the resolver document, so there is exactly one place a variant is defined.
Both files are the source. A resolved permutation is an output: it records the winning value and not which context produced it, so it cannot tell you which file to edit.
3. How to read a component file
// tokens/components/button.json
{
"component": {
"button": {
"$description": "Primary action control.",
"$extensions": {
"com.kampa": {
"component": {
"element": "button",
"resolver": "./button.resolver.json",
"children": ["component.icon"]
}
}
},
"bg": { "$value": "{color.brand.primary}", "$type": "color" },
"fg": { "$value": "{color.brand.on-primary}", "$type": "color" },
"border": { "$value": "{color.brand.primary}", "$type": "color" }
}
}
}
3.1 The descriptor
A group is a component if and only if it carries
$extensions["com.kampa"].component. Nothing else marks one: not the
component. path prefix, not a name. A group under component.* without the
descriptor is an ordinary token group.
| Key | Required | Value |
|---|---|---|
element |
yes | The HTML element the component renders as. A lowercase tag name; a hyphen allows a custom element. |
resolver |
yes | Relative path from this file to its resolver document, e.g. "./button.resolver.json". Never absolute, never a URL. |
children |
no | Other components this one contains, as absolute dot paths (["component.icon"]). Not brace-wrapped: they name groups, not token values. |
format |
no | "0.2", and nothing else. Absent means 0.1. Required on any group with a non-colour slot or a role. |
role |
no, 0.2 only | A WAI-ARIA role such as button, alert or checkbox. Recorded and preserved. It does not change any check result. |
Rules on the group itself:
- A component MUST NOT contain another component. Composition is
children. - A component MUST NOT use
$extends(Format §6.4). - It MAY carry
$root(Format §6.2), referenced as{component.button.$root}. A bare{component.button}names a group and is invalid.
3.2 Slots
Every child of the group whose name does not start with $ is a slot.
- A slot's
$valueis exactly one alias (Format §7) to a token. Not a raw value, not an alias inside a longer string. A slot that names a token is a slot the check can govern; a literal names nothing. - A slot declares
$type, and it MUST becolor,dimensionortypography. Anything else is an error. - A slot's
$typenever changes across permutations. A resolver context may repoint a slot. It MUST NOT retype it. - The alias MUST resolve to a token of the slot's own type. A
dimensionslot pointing at a colour is an error. The target's type is its effective type: a token that inherits$typefrom its group (Format §6.3) counts. fgandbgare required, and are always colour slots. They are the pair the contrast check measures. Everything else is optional.typographyis one alias to a typography composite (Format §9), not five separate slots.- Non-colour slots are not overridden per state. A name like
border-radius-hoveris just another slot.
Any slot name DTCG allows is valid. These are the ones Kampa paints:
| Slot | $type |
CSS property |
|---|---|---|
fg |
color |
color |
bg |
color |
background-color |
border |
color |
border-color |
border-radius |
dimension |
border-radius |
border-width |
dimension |
border-width |
padding-inline |
dimension |
padding-inline |
padding-block |
dimension |
padding-block |
gap |
dimension |
gap |
typography |
typography |
font family, size, weight, line height, tracking |
The non-colour names are CSS property names, one to one. fg and bg are not,
and cannot be renamed, because the contrast check finds its pair by those two
names. Any other slot is kept and checked by the rules above, just not drawn.
4. How to read a resolver document
// tokens/components/button.resolver.json
{
"version": "2025.10",
"name": "button",
"sets": {
"slots": { "sources": [{ "$ref": "./button.json" }] }
},
"modifiers": {
"theme": {
"default": "light",
"contexts": {
"light": [{ "$ref": "../light.json" }],
"dark": [{ "$ref": "../dark.json" }]
}
},
"variant": {
"default": "primary",
"contexts": {
"primary": [],
"secondary": [{ "component": { "button": {
"bg": { "$value": "{color.brand.secondary}", "$type": "color" },
"fg": { "$value": "{color.brand.on-secondary}", "$type": "color" }
} } }]
}
},
"state": {
"default": "rest",
"contexts": {
"rest": [],
"hover": [{ "component": { "button": {
"border": { "$value": "{color.border.strong}", "$type": "color" }
} } }],
"active": [{ "component": { "button": {
"bg": { "$value": "{color.brand.surface}", "$type": "color" }
} } }]
}
}
},
"resolutionOrder": [
{ "$ref": "#/modifiers/theme" },
{ "$ref": "#/sets/slots" },
{ "$ref": "#/modifiers/variant" },
{ "$ref": "#/modifiers/state" }
]
}
sets.slotspoints at the component file.- Each modifier is an axis. Its
contextsare the values on that axis. An empty context ("primary": []) changes nothing. A non-empty one repoints the slots it names. - The
thememodifier points each context at one of your mode files. Kampa writes it from your project's modes on every push. - Every modifier MUST declare
default. The Resolver Module makesdefaultoptional; Kampa requires it, so the default permutation is always known. A document that satisfies Kampa still satisfies the spec. resolutionOrderdecides precedence (Resolver §4.1, §6.2).- Every array item is an object. A bare string such as
"theme"or"tokens/light.json"is rejected by the published resolver schema. - Modifier names are yours. Nothing reserves
variant,stateortheme.
The permutations are every combination of every modifier's contexts. This
example has 2 variants × 3 states × 2 themes = 12 permutations, each a plain
DTCG tree with component.button.fg and component.button.bg. The check
grades all of them, not a sample.
One resolver document per component is the rule to follow. A document shared by two components applies its contexts to both.
4.1 Per-state slots
A modifier is a global axis. If hover repoints bg and secondary also
repoints bg, resolutionOrder picks one and the other does nothing. So a
non-default variant cannot have its own hover.
The pattern Kampa's starter components use avoids that:
- the component declares one slot per state:
bg-rest,bg-hover,bg-active, each an alias to a semantic token; - the base
bgaliases{component.button.bg-rest}; - the state contexts repoint the live slot:
hoversetsbgto{component.button.bg-hover}; - the variant contexts never touch
bg. They repoint the state slots.
The two axes now write to different keys, so neither overwrites the other.
Aliases resolve after ordering (Resolver §6.3), so bg picks up whatever the
variant put in bg-hover. It costs more slots and one extra hop when you trace
bg to its token. It needs no new key and changes nothing in the check.
5. What the Kampa check enforces
.kampa/check.mjs runs the same rules Kampa runs before it pushes. Every error
below fails the check and names the component, the slot and the permutation.
5.1 Contrast
In every permutation, the live fg on the live bg is measured against WCAG
2.2 contrast: 4.5:1 at AA, 7:1 at AAA. A permutation whose pair cannot be
measured (an alias that goes nowhere, a colour that cannot be read) fails.
It is never skipped.
Only fg × bg is paired. border and state slots such as bg-hover are not
measured on their own; their colours are measured when a state context makes
them the live fg or bg. Non-colour slots are never part of contrast: a
bound typography does not unlock the 3:1 large-text threshold.
5.2 Structure
| Error code | Meaning |
|---|---|
no-gateable-pair |
The component has no fg and bg slots. |
empty-component |
The component has no slots at all. |
resolver-missing |
The descriptor names no resolver document. |
resolver-not-found |
A referenced document is not in the repository. |
resolver-unparseable |
A referenced document is not valid JSON. |
resolver-wrong-version |
The resolver's version is not "2025.10". |
resolver-schema-invalid |
The resolver fails the published DTCG resolver schema. |
modifier-missing-default |
A modifier has no default. |
input-missing, input-not-in-contexts |
A modifier gets no input and has no default, or an input names a context the modifier does not declare. |
child-missing |
A children path does not point at a component. |
child-cycle |
Components contain each other, directly or in a loop. |
component-format-unsupported |
format is something other than absent or "0.2", or a 0.2 key such as role is used without format: "0.2". |
slot-format-undeclared |
A non-colour slot on a group without format: "0.2". |
slot-type-unsupported |
A slot $type other than color, dimension or typography. |
slot-alias-unresolved |
A slot holds a raw value, or its alias names a token that does not exist in that permutation. |
slot-type-mismatch |
The alias points at a token of another type, a context retypes a slot, or a context adds a non-colour slot the component file does not declare. |
An unresolved alias is an error, not a warning. Committed, it is a broken reference that fails whatever build reads it.
6. Known limits
childrencarries a path and nothing else. It cannot say where a child goes or which of its variants to use.elementis the only rendering fact. Which slot paints which CSS property is only partly in the document: the non-colour slot names are the property names;fg,bgandbordermap as in the table in §3.2.- Two namespaces in one file. Component keys use
com.kampa. Token-level metadata that Kampa writes (for example a declared role on a semantic token) uses$extensions.kampa. Both are read.
7. Every key Kampa adds
Six keys, all under $extensions["com.kampa"].component. Everything else in
both files is DTCG.
| Key | Why DTCG does not cover it |
|---|---|
component |
Format §6 cannot tell a component group from any other group. |
element |
No DTCG module binds a group to an HTML element. |
children |
No DTCG module defines component composition. |
resolver |
The Resolver Module links a resolver to its sources, not a token file to its resolver. |
format |
DTCG versions its own modules, not a vendor's format. Guessing the version from the slots would make a typo in a $type look like a version change. |
role |
No DTCG module carries accessibility semantics. element is a different fact: a div can be an alert or a badge. |
Not counted: the com.kampa namespace key itself (Format §5.2.3 requires
one), anything in the resolver document (all Resolver-defined), modifier and
context names, slot names, and the $type values color, dimension and
typography (all DTCG). The JSON Schema sets additionalProperties: false on
the descriptor, so a seventh key is a validation error.