Install
openclaw skills install pencil-skillCreate and edit Pencil (.pen) design files programmatically. Generate UI designs as JSON-based .pen files with themes, variables, components, and layouts. Includes CLI tools for reading, searching, editing, and validating .pen files without requiring the Pencil desktop app. Requires Node.js 18+. Schema version 2.8.
openclaw skills install pencil-skillPencil is a design tool that uses the JSON-based .pen format.
Because .pen is Git-friendly, it is ideal for agents to create and modify directly.
The key is not generic JSON syntax, but Pencil-specific rules (ref, slot, textGrowth, variable binding).
{
"version": "2.8",
"themes": {
"mode": [
"light",
"dark"
]
},
"variables": {
"color.surface": {
"type": "color",
"value": [
{
"theme": {
"mode": "light"
},
"value": "#FFFFFF"
},
{
"theme": {
"mode": "dark"
},
"value": "#111827"
}
]
}
},
"children": []
}
version and children are requiredthemes and variables are optional, but practically essential for a design systemrectangle: basic rectangleellipse: circle/arcline: linepolygon: polygonpath: path geometrytext: textframe: layout container + can declare slotsgroup: group containernote: annotationprompt: prompt memocontext: context memoicon_font: icon-font-based vector iconref: reusable component instanceRemember only the core properties:
layout: none | vertical | horizontalgap, paddingjustifyContent: start | center | end | space_between | space_aroundalignItems: start | center | endGotcha:
x, y values are effectively ignored inside flex (vertical/horizontal) containers| Property | Type | Description |
|---|---|---|
clip | boolean | Clips content outside the frame bounds when true. Default is false |
| Property | Type | Description |
|---|---|---|
enabled | boolean | Hides the node when false (applies to all entities, not frame-specific) |
[
{
"type": "frame",
"id": "modal-overlay",
"clip": true,
"layout": "vertical"
},
{
"type": "frame",
"id": "hidden-panel",
"enabled": false,
"layout": "vertical"
}
]
fit_contentfill_containerfit_content(${number})width and height can use numbers, variables, or the SizingBehavior values above.
fill: color / gradient / image / mesh_gradientstroke: align, thickness, join, cap, dashPattern, filleffect: blur, background_blur, shadowColor values are recommended in hex format (#RGB, #RRGGBB, #RRGGBBAA).
textGrowth first for texttextGrowth: auto | fixed-width | fixed-width-heightwidth/height without textGrowth, behavior may be ignored or inconsistentreusable + ref + descendants + slot)reusable: true to a frame (or another entity)id acts as the component keytype: "ref"ref: "<reusable id>"id values"container/button/label"slot: ["body"] in the componentname: "body"ref.childrenBinding format:
"$variable.name"Example:
{
"fill": "$color.surface",
"padding": "$space.card.padding"
}
Validation points:
$ must exist in variablesthemes axis/valueSupported families:
lucidefeatherMaterial Symbols OutlinedMaterial Symbols RoundedMaterial Symbols Sharpphosphor| Pattern | Result |
|---|---|
/ in id (e.g., "a/b") | ❌ invalid |
Duplicate id values in the same file | ❌ invalid |
ref points to a non-existent reusable id | ❌ invalid |
width/height on text without textGrowth | ⚠️ warning (may be ignored) |
"$unknown.var" reference | ⚠️ warning (unresolved variable) |
These rules prevent the most common mistakes agents make.
If a matching reusable component exists, you must reuse it with ref.
search-nodes.mjs --reusableref instancedescendantsNo hardcoding. Colors, spacing, and typography must use variables.
get-variables.mjs"$variable.name" formatset-variables.mjsFor every text node:
textGrowth first (auto or fixed-width)fill_container for widthvalidate-pen.mjs after editingAfter each modification:
validate-pen.mjsread-tree.mjs --depth 2For logos, images, and icons:
search-nodes.mjs --name "logo|brand|icon"batch-design.mjs copy opversion and children existid is unique and does not use slashesref existsvariables references match real keys ($...)textGrowth is set before using width/height on textslot declaration, name frame, and ref.children injection relationshipThe schema defines both singular and plural forms in $defs.
On node properties, the canonical key is singular:
fill (value can be single fill or array)stroke (value can be single stroke or array)effect (value can be single effect or array)Scripts accept plural forms (fills, strokes, effects) and normalize them to singular keys on write.
AGENTS.md is a concise guide with rule summaries and references to individual rules/*.md files for full details. To regenerate after editing rules: node scripts/compile-agents.mjs
These scripts manipulate .pen files directly. They work without MCP.
| Script | Purpose | Usage |
|---|---|---|
read-tree.mjs | Print node tree structure | node scripts/read-tree.mjs file.pen [--depth 2] [--id nodeId] |
search-nodes.mjs | Search nodes | node scripts/search-nodes.mjs file.pen --type frame [--reusable] [--name regex] |
get-variables.mjs | View variables/themes | node scripts/get-variables.mjs file.pen [--format json] |
find-space.mjs | Find empty-space coordinates | node scripts/find-space.mjs file.pen --width 300 --height 200 |
| Script | Purpose | Usage |
|---|---|---|
batch-design.mjs | Insert/update/delete/move/copy nodes | node scripts/batch-design.mjs file.pen --ops '[{"op":"update","id":"node1","props":{"name":"Header"}}]' |
replace-props.mjs | Bulk property replacement | node scripts/replace-props.mjs file.pen --match '{"fill":"#FF0000"}' --replace '{"fill":"$color.primary"}' |
set-variables.mjs | Add/update variables | node scripts/set-variables.mjs file.pen --var 'name=type:value' |
set-variables.mjs theme syntax (@theme):
# Single value
node scripts/set-variables.mjs file.pen --var 'color.primary=color:#0D9488'
# Per-theme variable setting — @theme syntax
node scripts/set-variables.mjs file.pen --var 'color.bg=color:#FFFFFF@light,#0F172A@dark'
node scripts/set-variables.mjs file.pen --var 'space.md=number:12'
| Script | Purpose | Usage |
|---|---|---|
validate-pen.mjs | Structural validation + value-type checks + lint | node scripts/validate-pen.mjs file.pen |
batch-design.mjs accepts operations as a JSON array:
{
"op": "insert",
"parentId": "frame-1",
"node": {
"type": "text",
"id": "title",
"content": "Hello",
"textGrowth": "auto"
}
}
{
"op": "update",
"id": "title",
"props": {
"content": "Updated",
"fill": "$color.primary"
}
}
{
"op": "delete",
"id": "title"
}
{
"op": "move",
"id": "title",
"toParentId": "frame-2",
"index": 0
}
{
"op": "copy",
"id": "card-1",
"newId": "card-2",
"toParentId": "grid"
}
Notes:
toParentId is optional. If omitted, the copied node is inserted at the root level.newId prefix is recommended.pen.bak)1. read-tree.mjs → Inspect current structure
2. search-nodes.mjs → Search reusable components
3. get-variables.mjs → Check variables/themes
4. find-space.mjs → Calculate frame placement coordinates
5. batch-design.mjs → Create/modify nodes
6. validate-pen.mjs → Validate results
Required rules for production-grade design. 41 rules, 8 categories.
| Priority | Category | Impact | Prefix | Rules |
|---|---|---|---|---|
| 1 | Layout & Overflow | CRITICAL | layout- | 8 |
| 2 | Design Tokens | CRITICAL | token- | 5 |
| 3 | Anti-AI Aesthetic | HIGH | aesthetic- | 6 |
| 4 | Component System | HIGH | component- | 5 |
| 5 | Typography | MEDIUM | typo- | 5 |
| 6 | Color System | MEDIUM | color- | 4 |
| 7 | Spacing System | MEDIUM | spacing- | 4 |
| 8 | Showcase & Style Guide | MEDIUM | showcase- | 4 |
layout-auto-layout — Auto Layout is required for all containerslayout-sizing — width/height rules by scenariolayout-overflow — overflow prevention checklistlayout-responsive — responsive simulation (separate frame)layout-spacing-consistency — gap/padding consistencylayout-z-order — Z-order and overlay placementlayout-no-overlap — prevent element overlaplayout-canvas-placement — Top-level canvas placement, no layer overlaptoken-naming — naming format: $category.purpose.varianttoken-semantic-colors — required semantic color systemtoken-theme-required — light/dark themes requiredtoken-no-hardcode — no hardcodingtoken-workflow — token setup workflowaesthetic-layout — avoid AI-looking layoutsaesthetic-typography — avoid AI-looking type choicesaesthetic-color — avoid AI-looking colorsaesthetic-decoration — avoid AI-looking decorationaesthetic-content — avoid AI-looking contentaesthetic-checklist — self-check checklistcomponent-atomic — apply Atomic Designcomponent-naming — Figma-standard namingcomponent-variant — variant patternscomponent-slot — slot patternscomponent-reuse-first — component reuse firsttypo-scale — define type scaletypo-weight — font-weight usage rulestypo-pairing — type pairing guidetypo-line-height — line-height rulestypo-text-rules — text layout rulescolor-semantic — semantic color systemcolor-accessibility — WCAG 2.1 AA accessibilitycolor-ratio — 60-30-10 color ratiocolor-dark-mode — dark mode rulesspacing-8pt-grid — 8pt grid systemspacing-proximity — proximity principlespacing-padding — padding systemspacing-forbidden — forbidden patternsshowcase-pre-design — required pre-design processshowcase-design-system — design-system-first principleshowcase-frame — showcase frame patternshowcase-final-checklist — final QA checklistCheck each individual rule file for detailed explanations and code examples:
rules/layout-auto-layout.md
rules/token-naming.md
rules/aesthetic-layout.md
Agent guide with rule summaries: AGENTS.md — full rules in rules/*.md