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 $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: "color". 0.1 has colour slots only.
  • A slot's $type never changes across permutations. A resolver context may repoint a slot. It MUST NOT retype it.
  • fg and bg are required. They are the pair the contrast check measures. border is 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.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.

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

  • 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. 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.