penpot-foundations — token & library foundation
1. Title + How it works
is the
load-bearing skill of the kit: nothing downstream is correct without
a governed token layer. Every mutation goes through
; validate visually with
; read structure with
penpotUtils.shapeStructure
(full tool surface:
shared/penpot-mcp-tool-reference.md
). This skill reads tokens with
penpotUtils.tokenOverview()
,
creates them through
penpot.library.local.tokens
(
,
set.addToken({type,name,value})
), and
binds them to shapes with
shape.applyToken(token, properties)
. It works in three tiers — primitive →
semantic → component (see
shared/tokens-schema.json
) — and can also
infer a token system from an
existing hardcoded design.
2. The One Rule That Matters Most
Tokens before everything, and never one-shot. Build the foundation in small, idempotent steps:
primitives first, then semantic aliases that reference them, then (only if needed) component tokens.
Validate after each tier with
. Never invent values silently — propose the scale and
let a human approve it at the checkpoint.
3. Penpot MCP Tool Reference
Full surface:
shared/penpot-mcp-tool-reference.md
. Domain calls this skill leans on:
penpotUtils.tokenOverview()
/
(discovery + inference);
penpot.library.local.tokens.addSet
/
/
(writes);
penpot_api_info('TokenSet'|'Token'|'TokenCatalog')
before relying on member signatures.
4. Plugin API Essentials
Gotcha numbers refer to
shared/plugin-api-gotchas.md
.
- #8 token creation —
penpot.library.local.tokens
; / take OBJECT args; new sets are INACTIVE (); token is always a string (literal or ); read .
- Use the exact token strings from
shared/tokens-schema.json
: , , , , , , , , , , , , , . (Note camelCase/plural — NOT or .)
- #2 async token application — this skill's critical failure mode.
shape.applyToken(token, properties)
is async (~100 ms) and binds by NAME (bindings re-resolve when the active set/theme changes — that is what makes dark mode work). Apply in one call, verify in a LATER call. Never mass-apply in a single call — hundreds of s in one loop race and scramble bindings; apply in chunks of ≈25–40 shapes per call, verify each chunk, retry misses. Text/Rectangle fill bindings are the flakiest (Boards are reliable); bindings are sticky — overwrite a wrong one by applying the correct token, don't try to clear it via .
- Verify any unfamiliar member with
penpot_api_info('TokenSet')
/ first.
5. Token-Aware Brief Contract
Before mutating, restate the request as:
- Context — product, brand, existing system (if any), light/dark needs.
- Objective — single and specific (e.g., "create primitive + semantic color/spacing tokens and a dark theme").
- Inputs — existing tokens, brand palette, type scale ratio (default Minor Third 1.2), spacing grid (default 4px).
- Constraints — no hardcoded values; semantic tokens must reference primitives; spacing on the 4px grid.
- Acceptance Criteria — every token resolves; semantic tier references primitives; all spacing is a 4px multiple; (if requested) a working dark theme.
Act as a senior design-systems engineer who never hardcodes values.
6. Mandatory Workflow
Phase 0 — Discovery (read-only). , then
penpotUtils.tokenOverview()
and a
shapeStructure(penpot.currentPage.root, 3)
read. Record what already exists in the
ledger.
✋ Checkpoint: confirm scope (new system vs. extend vs. infer-from-design).
Phase 1 — Primitives. Create the
set
and activate it (
— new
sets are created inactive), then add raw color ramps, the spacing scale (4px grid), radius scale, and
type scale (font sizes via Minor Third). Use
scripts/createTokenSet.js
.
✋ Checkpoint: review the primitive scales (
+ naming).
Phase 2 — Semantic. Add intent aliases that reference primitives,
routed into two sets (the
referenced
set must be active or references fail validation):
- colour semantics ( → ) → ;
- mode-invariant semantics (, , ) → .
scripts/createSemanticTokens.js
does this split by token type. This is what makes dark mode a clean
parallel colour set. ✋ Checkpoint: review semantic mapping.
Phase 3 — Dark mode (optional). Create the parallel
colour set (same colour names
as
, dark values) with
; leave
active as default
and hand the toggle to the user. Cover
every colour name (the script returns a
list). Dark mode
works — bindings re-resolve by token
name when the user activates
— but the agent cannot render the switch from the plugin (
doesn't persist;
the plugin's read lags). The user toggles
on /
off in Penpot's Tokens panel
(see
references/04-themes-modes.md
). ✋ Checkpoint: verify dark
s, restore light, and
tell the user how to switch.
Phase 4 — Apply / Infer / Bind. Either bind tokens to a selection (
scripts/applyTokensToShapes.js
),
infer tokens from a hardcoded design (
scripts/inferTokensFromShapes.js
), or —
fundamental —
auto-bind tokens to every unbound element by exact value match so the user doesn't have to do it by
hand (
scripts/bindTokensToElements.js
). This is also the
precondition for theming: a design only
follows a light/dark switch if its shapes are bound to token
names (bindings re-resolve by name when
sets/themes change).
Apply in chunks and verify —
is async and mass-application in one
call races/scrambles bindings; Text/Rectangle
fill bindings are flaky and need verify+retry (see
shared/plugin-api-gotchas.md
). Value-only matches are a heuristic: flag inverse-colored chrome for a
semantic-role review. ✋ Checkpoint: review bindings + coverage.
Phase 5 — Validate. scripts/validateTokens.js
— orphans, unresolved references, off-grid spacing.
Produce the structured report.
7. Critical Rules
- Primitives → semantic → component. Shapes bind to semantic tokens, never primitives.
- All spacing/padding/gap/radius values are multiples of 4px.
- Never create a token you can't justify; propose new tokens at a checkpoint, don't auto-create them.
- Idempotent: check for an existing set/token by name before adding (no duplicates).
- Type sizes follow a Minor Third (1.2) scale unless the brand dictates otherwise.
- Do not apply and read back a token in the same call (async).
- Keep the ledger updated so a truncated session can resume.
8. Domain Architecture
Three token tiers (see
shared/tokens-schema.json
):
- primitives — raw, context-free scale (, , , ). Never bound directly to shapes when a semantic token exists.
- semantic — intent aliases referencing primitives ( → , → ). This is what shapes bind to.
- components — optional per-component overrides referencing semantic tokens. Create only when a component needs its own surface.
Set layout (see
shared/modes-and-policies.md
): mode-invariant semantics live in
;
colour
semantics live in
/
(same names in both). Themes are modeled as Penpot token
themes (
addTheme({ group, name })
) that toggle the matching
set; primitives and the
mode-invariant
set stay constant.
9. Modes & Policies
Default
review. Creating a new token, building a theme, or applying tokens to shapes all require a
checkpoint. The only auto-applicable action (safe set,
shared/modes-and-policies.md
) is replacing a
raw value that is
exactly equal to an existing token's resolved value with that token.
10. State Management
Ledger keys under
(see
shared/state-management.md
):
,
created:[{kind:'set'|'token'|'theme', name, id}]
,
,
. Re-derive reality with
after truncation before continuing.
11. User Checkpoints
| After phase | Artifacts shown | What we ask |
|---|
| 0 Discovery | existing tokens summary | New / extend / infer? |
| 1 Primitives | of primitives | Approve scales & naming? |
| 2 Semantic | semantic→primitive mapping | Approve intent mapping? |
| 3 Themes | resolved values per theme | Approve theme switch? |
| 4 Apply/Infer | before/after on a sample | Approve bindings? |
12. Naming Conventions
Dot-notation per
shared/naming-conventions.md
: sets are
/
(mode-invariant) /
+
(colour) / optional
; tokens are lowercase dot paths; themes
grouped
with names
/
/
.
13. Anti-Rationalization Table
| Excuse the LLM makes | Why it's wrong | Deterministic countermeasure |
|---|
| "I'll hardcode this hex now and tokenize later." | Hardcoded values break theming and governance; "later" never comes. | Stop. Read , bind to a semantic token, or propose one for review. |
| "I'll bind shapes straight to a primitive to save a step." | Primitives can't switch per theme; dark mode breaks. | Bind to a semantic token that references the primitive. |
| "This spacing is 18px, close enough." | Off-grid values erode rhythm and fail governance. | Round to the nearest 4px-grid token (16 or 20). Document any exception only with human approval. |
| "I'll create a dozen tokens up front to be safe." | Unused/orphan tokens are debt. | Create only what's needed now; propose the rest at a checkpoint. |
14. Helper Code Snippets
js
// Idempotent set + token creation (Phase 1)
const tok = penpot.library.local.tokens;
let prim = tok.sets.find(s => s.name === "primitives") || tok.addSet({ name: "primitives" });
if (!prim.active) prim.toggleActive(); // new sets are INACTIVE by default
if (!prim.tokens.find(t => t.name === "color.blue.500")) {
prim.addToken({ type: "color", name: "color.blue.500", value: "#0066FF" });
}
return { set: prim.name, count: prim.tokens.length };
js
// Semantic alias referencing a primitive (Phase 2)
const tok = penpot.library.local.tokens;
let sem = tok.sets.find(s => s.name === "semantic") || tok.addSet({ name: "semantic" });
if (!sem.active) sem.toggleActive(); // and the referenced 'primitives' set must be active too
if (!sem.tokens.find(t => t.name === "color.action.primary.bg")) {
sem.addToken({ type: "color", name: "color.action.primary.bg", value: "{color.blue.500}" });
}
return { ok: true };
15. Reference Resources
penpot_api_info('TokenCatalog')
, penpot_api_info('TokenSet')
, .
shared/tokens-schema.json
(taxonomy + real type strings), shared/plugin-api-gotchas.md
.
16. Supporting Files
references/
| File | Loaded for |
|---|
| Phase 0 read-only inventory |
| tiers, scales, type ratio |
| building tokens from a hardcoded design |
| light/dark theme modeling |
| token naming specifics |
06-anti-rationalization.md
| governance excuses + countermeasures |
| common failures & fixes |
scripts/
| File | Purpose |
|---|
| read pages/library/tokens |
| idempotent primitives set + tokens |
| semantic aliases referencing primitives |
| propose tokens from raw values |
| bind tokens to a selection |
| auto-bind tokens to unbound elements by value (chunked + verify) — enables theming |
| light/dark themes |
| orphans, unresolved refs, off-grid |
Doctrine paths. and
resolve inside this bundle in native installs (vendored by the installer); in a Claude Code plugin install they live at the plugin root —
${CLAUDE_PLUGIN_ROOT}/shared/…
, two directories up from this file.