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 format key 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 $value is 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 be color, dimension or typography. Anything else is an error.
  • A slot's $type never 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 dimension slot pointing at a colour is an error. The target's type is its effective type: a token that inherits $type from its group (Format §6.3) counts.
  • fg and bg are required, and are always colour slots. They are the pair the contrast check measures. Everything else is optional.
  • typography is 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-hover is 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.slots points at the component file.
  • Each modifier is an axis. Its contexts are the values on that axis. An empty context ("primary": []) changes nothing. A non-empty one repoints the slots it names.
  • The theme modifier 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 makes default optional; Kampa requires it, so the default permutation is always known. A document that satisfies Kampa still satisfies the spec.
  • resolutionOrder decides 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, state or theme.

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 bg aliases {component.button.bg-rest};
  • the state contexts repoint the live slot: hover sets bg to {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

  • children carries a path and nothing else. It cannot say where a child goes or which of its variants to use.
  • element is 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, bg and border map 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.