Kampa component format 0.3

Status: 0.3, pre-release. The shape may change before a stable release. Nothing here promises compatibility across versions.

This file explains 0.3 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 changed from 0.2

A 0.3 component carries enough, as data, for Kampa to generate the HTML a browser renders and the CSS that styles it, with every state the native element has. The check grades what the browser paints.

  • States are groups in the component file, not a resolver axis. A hovered button's background is hover.bg. A checked, hovered checkbox is hover.checked.
  • Pairs are declared. The component lists the colour pairs the check grades, each with a kind: text, non-text or decorative.
  • The anatomy is data. element is an allowlist and attributes holds the root element's fixed attributes.
  • A push generates components.css and one HTML snippet per component.

0.1 and 0.2 documents are unchanged: they validate, grade and push exactly as before. A document is on 0.3 only when it declares format: "0.3". A 0.2 document with its format changed to "0.3" is not a 0.3 document: its state modifier is an error in 0.3.


2. Where the files are

tokens/
  light.json                    semantic tokens, one file per mode
  dark.json
  tokens.css                    generated: your tokens as custom properties
  components.css                generated: every 0.3 component's CSS
  components/
    button.json                 the component file
    button.resolver.json        the resolver document it names
    button.html                 generated: the snippet for each variant
schemas/component/0.3/          this file and the JSON Schema

components.css and the snippets are written by every push that carries a 0.3 component and are overwritten by the next one. The Using components page of Kampa's documentation explains how to load them.


3. States

A state group is a group inside the component whose name is one of nine reserved names. The names are reserved only in a document that declares 0.3.

State Matches
hover :hover
active :active
focus-visible :focus-visible
checked :checked
invalid [aria-invalid="true"]. Set aria-invalid when the value is wrong; Kampa does not style :invalid or :user-invalid, so an error is never only visual.
open :is(:open, [open])
selected [aria-selected="true"] (no 0.3 element uses it yet)
current [aria-current] other than "false" (no 0.3 element uses it yet)
disabled :disabled, or a link with no href and aria-disabled="true"

The canonical order is the order of that table: hover, active, focus-visible, checked, invalid, open, selected, current, disabled. A compound state nests in that order (hover then checked is hover.checked), with no repeats, at most one of hover, active and focus-visible, and at most three states deep.

Which declaration wins. For the states an element is in, the candidates are the base slot and every group whose states are all active. The deepest group wins; at equal depth, the one later in the canonical order. So a condition beats an interaction: a hovered invalid input keeps its invalid border unless hover.invalid says otherwise. A disabled control drops hover, active and focus-visible first, so disabled wins every tie. This is exactly what the browser does with components.css, because Kampa writes the rules in that order at one specificity.

3.1 Which element has which state

Element States Pseudo-element groups
button hover, active, focus-visible, disabled
a hover, active, focus-visible, disabled
span none
input (type text) hover, active, focus-visible, invalid, disabled placeholder
textarea hover, active, focus-visible, invalid, disabled placeholder
select hover, active, focus-visible, invalid, open, disabled
input type=checkbox hover, active, focus-visible, checked, invalid, disabled before
input type=radio hover, active, focus-visible, checked, invalid, disabled before
input type=checkbox, role switch hover, active, focus-visible, checked, invalid, disabled before
hr none

A state group on an element that cannot be in that state is an error.


4. Slots

Every token in the group, in a state group or in a pseudo-element group is a slot. A slot's $value is exactly one alias (Format §7) to a token of its own $type in your token files, never to another slot of the component.

Slot $type CSS property Where it may appear
bg color background-color anywhere
fg color color anywhere
border color border-color anywhere
border-width, border-radius, padding-inline, padding-block, gap, inline-size, block-size dimension the same name anywhere
typography typography font, and letter-spacing anywhere
opacity number opacity only in a path that contains disabled
outline-color color outline-color only in a path that contains focus-visible
outline-width, outline-offset dimension the same name only in a path that contains focus-visible
  • A state group MAY set a slot only if the base declares it. The exceptions are the three outline slots and opacity, which exist only in their state.
  • A slot's $type is the same in the base, in every state group and in every resolver context.
  • Another slot name is valid and kept, but not drawn.

4.1 Pseudo-element groups

Two more reserved group names carry the paint of a pseudo-element. Their slots are named by the CSS property they paint.

Group Paints Slots
placeholder ::placeholder color
before ::before: the checkbox check, the radio dot, the switch thumb background-color, border-color, border-width, inline-size, block-size

The shape of each mark is fixed by Kampa's CSS. Which before slots a control uses:

  • Checkbox: border-color, border-width, inline-size, block-size. The check is drawn with a border.
  • Radio: border-color, inline-size, block-size. The dot is drawn with a border, half its size wide, so it stays visible in forced-colours mode.
  • Switch: all five. The thumb's distance from the track edge is the switch's own padding-inline.

A pseudo-element group may sit at the base or at the end of a state path (checked.before.border-color). Under a state it may set only what the base group of the same name declares.


5. Declared pairs

The descriptor's pairs lists the colour pairs the check grades:

"pairs": [
  { "slot": "fg",            "on": "bg",   "kind": "text" },
  { "slot": "border",        "on": "page", "kind": "non-text" },
  { "slot": "outline-color", "on": "page", "kind": "non-text" }
]
  • slot is the foreground side and on the background side. Either names a colour slot, or a pseudo-element slot as group.slot (before.border-color). on may also be page.
  • kind sets the threshold:
Kind Threshold WCAG 2.2
text 4.5:1 at AA, 7:1 at AAA 1.4.3, 1.4.6
non-text 3:1 at AA and AAA 1.4.11
decorative not graded; listed as excluded, "declared decorative" 1.4.11 applies only to what identifies the control

pairs absent means one pair, fg on bg, text. An empty pairs is allowed only for role separator, presentation or none.

page is the surfaces your component sits on, which Kampa cannot see. It grades against your project's general surfaces: color.background.default, .subtle and .muted, the same three under color.surface, and any colour token you declare with role background and no group. The result is the worst of them. Brand, inverted and semantic surfaces are not graded, and the pull request says so. A project with none of these tokens cannot be graded against page, and the check fails with "declare a background role".

5.1 Where a pair is graded

The check grades every declared pair in every tile: for each permutation of the resolver (theme, variant and any other axis), the base and each state group the component declares. In each tile both sides take the declaration that would win in that state.

  • In a tile that holds disabled, every pair is listed as excluded, "inactive component". WCAG exempts inactive components.
  • A pair with a state-only side (outline-color) is graded only in tiles where that state is active.
  • The checkbox check and the radio dot are graded only where checked is active, because only then are they painted. The switch thumb and the placeholder are graded in every tile.

5.2 Rules that keep a pair honest

  • Every colour slot is in a pair, of any kind. decorative exists so a border that carries no information is declared, not skipped.
  • A component that renders text (button, link, span, text input, textarea, select) MUST declare fg on bg as text. fg and placeholder.color can never be decorative on it.
  • A state group that changes one side of a pair changes the other side too, and so on across pairs that share a slot. Hover and focus can be active together; this rule means every pair the browser shows comes from one group the check graded.
  • A focusable element declares its ring: a focus-visible group with outline-color, outline-width and outline-offset, and a non-text pair for outline-color. Kampa's CSS removes the browser's own outline, so a component that declared none would have no focus indicator. The ring sits outside the control: width and offset resolve to positive sizes.
  • opacity only appears under disabled, whose pairs are excluded, and resolves to a number from 0 to 1. Opacity anywhere else would change the contrast the check measured.

6. The descriptor

Key Required Value
format yes "0.3".
element yes One of button, a, span, hr, input, textarea, select.
resolver yes Relative path from this file to its resolver document.
attributes no The root element's fixed attributes. button: type "button". input: type "text", "checkbox" or "radio". hr: aria-orientation "horizontal" or "vertical". Other elements: none.
role no A WAI-ARIA role where it differs from the element's own, such as switch on a checkbox.
pairs no §5.
children no Other components this one contains, as absolute dot paths.

A variant cannot change element or attributes.


7. How to read a resolver document

The resolver is a DTCG Resolver Module 2025.10 document, as in 0.2, with two differences:

  • No state axis. States are groups in the component file. A modifier named state, or named like a state or pseudo-element group, is an error.
  • A context may repoint a slot the component file declares, never add one. A variant that wants its own hover sets hover.bg, and only if the component file declares hover.bg.

Kampa adds the theme modifier from your project's modes on every push.

7.1 Class names

Each component's CSS is keyed by class:

  • kampa-<component> for the component, from its group name (kampa-button).
  • kampa-<component>--<context> for each context of each modifier other than theme, except the modifier's default (kampa-button--secondary). The default is the base.

States are never classes: they are the selectors in §3. Component and context names are lowercase letters, digits and single hyphens, starting with a letter, and every class is unique across your components.


8. What the Kampa check enforces

.kampa/check.mjs runs the same rules Kampa runs before it pushes. Every error fails the check and names the component, the path and the permutations it holds in. 0.2's codes (empty-component, resolver-*, modifier-missing-default, child-*, input-*, slot-type-unsupported, slot-alias-unresolved, slot-type-mismatch, component-format-unsupported) keep their meaning.

Error code Meaning
state-group-unknown A group that is neither a state group nor a pseudo-element group, or a group inside a pseudo-element group.
state-order-noncanonical A compound state out of the canonical order, repeated, with two of hover, active and focus-visible, or deeper than three.
state-not-applicable A state or pseudo-element group the element does not have (§3.1).
state-slot-undeclared A state group sets a slot the base does not declare, or a pseudo-element slot the group does not have.
state-only-slot-misplaced An outline slot outside focus-visible.
opacity-outside-disabled, opacity-out-of-range opacity outside disabled, or not a number from 0 to 1.
cell-introduced-by-context A resolver context adds a slot the component file does not declare.
state-axis-reserved The resolver declares a state modifier or one named like a state.
pair-incomplete A state group changes one side of a pair and not the other.
pair-endpoint-unknown, pair-kind-invalid, pair-duplicate A pair names no colour slot, has an unknown or forbidden kind, or is declared twice.
text-pair-missing A text-bearing component without fg on bg as text.
slot-ungraded A colour slot in no pair.
no-gateable-pair No pairs and no fg and bg, or empty pairs on a role that needs some.
focus-indicator-missing, focus-ring-geometry A focusable element without its ring, or a ring whose width or offset is not positive.
element-not-allowed, attribute-not-allowed An element or attribute outside §6.
class-name-unsafe, class-name-collision A component or context name that is not class-safe, or two that become one class.

Three findings are warnings: printed, never failing the check.

Warning Meaning
slot-aliases-primitive A colour slot points at a palette primitive, so it will not change between modes.
cell-never-effective A group holds disabled and an interaction state, so it can never apply.
slot-path-collision Two slots that a Style Dictionary build of the component file would give one name.

9. Worked example

A button with two variants, a press that tightens its radius, a focus ring outside the control, and a disabled state. The resolver carries no state axis. Its resolutionOrder names the theme modifier first, and Kampa writes that modifier from the project's modes at push.

The component file:

{
  "component": {
    "button": {
      "$description": "Primary action control.",
      "$extensions": {
        "com.kampa": {
          "component": {
            "format": "0.3",
            "element": "button",
            "attributes": { "type": "button" },
            "resolver": "./button.resolver.json",
            "pairs": [
              { "slot": "fg", "on": "bg", "kind": "text" },
              { "slot": "border", "on": "page", "kind": "decorative" },
              { "slot": "outline-color", "on": "page", "kind": "non-text" }
            ]
          }
        }
      },
      "bg": { "$type": "color", "$value": "{color.brand.default}" },
      "fg": { "$type": "color", "$value": "{color.foreground.on-brand}" },
      "border": { "$type": "color", "$value": "{color.brand.border}" },
      "border-width": { "$type": "dimension", "$value": "{borderWidth.thin}" },
      "border-radius": { "$type": "dimension", "$value": "{radius.md}" },
      "padding-inline": { "$type": "dimension", "$value": "{spacing.sm}" },
      "padding-block": { "$type": "dimension", "$value": "{spacing.xs}" },
      "block-size": { "$type": "dimension", "$value": "{size.control.sm}" },
      "typography": { "$type": "typography", "$value": "{typography.labelMd}" },
      "hover": {
        "bg": { "$type": "color", "$value": "{color.brand.default-hover}" },
        "fg": { "$type": "color", "$value": "{color.foreground.on-brand}" }
      },
      "active": {
        "bg": { "$type": "color", "$value": "{color.brand.default-active}" },
        "fg": { "$type": "color", "$value": "{color.foreground.on-brand}" },
        "border-radius": { "$type": "dimension", "$value": "{radius.sm}" }
      },
      "focus-visible": {
        "outline-color": { "$type": "color", "$value": "{color.border.focus}" },
        "outline-width": { "$type": "dimension", "$value": "{borderWidth.medium}" },
        "outline-offset": { "$type": "dimension", "$value": "{borderWidth.medium}" }
      },
      "disabled": {
        "opacity": { "$type": "number", "$value": "{opacity.disabled}" }
      }
    }
  }
}

The resolver document:

{
  "version": "2025.10",
  "name": "button",
  "sets": { "slots": { "sources": [{ "$ref": "./button.json" }] } },
  "modifiers": {
    "variant": {
      "default": "primary",
      "contexts": {
        "primary": [],
        "secondary": [
          {
            "component": {
              "button": {
                "bg": { "$type": "color", "$value": "{color.brand.subtle}" },
                "fg": { "$type": "color", "$value": "{color.foreground.on-brand-subtle}" }
              }
            }
          }
        ]
      }
    }
  },
  "resolutionOrder": [
    { "$ref": "#/modifiers/theme" },
    { "$ref": "#/sets/slots" },
    { "$ref": "#/modifiers/variant" }
  ]
}

The check grades this button in 2 themes × 2 variants × 5 tiles (the base, hover, active, focus-visible, disabled). The border is declared decorative, because the button's own fill and text identify it. Secondary repoints only its resting bg and fg; while hovered it takes the base hover pair, and that pair is graded in Secondary's own hover tile.


10. Known limits

  • Surfaces. page means your general surfaces. A component on a brand, inverted or card surface is not graded against that surface.
  • The select popup is painted by the browser and not graded. The closed control is.
  • No :indeterminate. A checkbox in that state paints as unchecked.
  • Opacity, gradients and target size are not modelled. Opacity is limited to disabled controls.
  • Forced colours. components.css restores each mark with system colours under forced-colors: active. Those colours are the user's, and not graded.

11. Every key Kampa adds

Eleven 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.
role No DTCG module carries accessibility semantics.
attributes No DTCG module carries HTML attributes.
pairs No DTCG module relates two tokens by an accessibility requirement.
slot, on, kind The three fields of a pair.

Not counted: the com.kampa namespace key, anything in the resolver document, modifier and context names, slot names, attribute names (they are HTML's), and the state and pseudo-element group names, which are DTCG group names from a closed list. pairs decides what the check grades, so it is more than the optional metadata Format §5.2.3 describes; it changes no token's value. The JSON Schema sets additionalProperties: false on the descriptor and on each pair, so a twelfth key is a validation error.