Kampa component format 0.1
Status: 0.1, 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
This file describes 0.1: every slot is a colour. A group on 0.1 declares
no format key.
Version 0.2 adds dimension and typography slots. A 0.2 group declares
format: "0.2" and is described in schemas/component/0.2/. Every valid 0.1
document is also 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.1/ 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. |
The 0.1 descriptor accepts these three keys and no others. There is no
format: "0.1": leaving format out is how a group says 0.1.
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: "color". 0.1 has colour slots only. - A slot's
$typenever changes across permutations. A resolver context may repoint a slot. It MUST NOT retype it. fgandbgare required. They are the pair the contrast check measures.borderis optional. Everything else is optional too.
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 |
fg and bg 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.
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 present with a value other than "0.2". |
slot-format-undeclared |
A non-colour slot on a group with no format. A non-colour slot needs 0.2. |
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 |
A context retypes a slot, or the alias points at a token of another type. |
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. The document does not say which slot paints which CSS property beyond 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
Four 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. |
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, and slot names (all DTCG). The JSON Schema sets
additionalProperties: false on the descriptor, so a fifth key is a validation
error.