nickbreaton

pencil

Design UI in .pen files using Pencil MCP (pencil.dev). Use this skill when creating designs, screens, dashboards, landing pages, components, or editing .pen files. Covers batch_design operations, component patterns, layout systems, style guides, and visual verification workflows.

nickbreaton 0 Updated 4mo ago
GitHub

Install

npx skillscat add nickbreaton/opencode/pencil

Install via the SkillsCat registry.

SKILL.md

Pencil MCP Design Skill

This skill provides comprehensive guidance for designing user interfaces using the Pencil MCP tools. Pencil uses .pen files — a JSON-based design format with flexbox layout, components, variables, and theming support.


Quick Reference: Essential Workflow

1. pencil_get_editor_state       → Get current file, selection, schema
2. pencil_get_guidelines         → Load topic-specific rules (design-system, landing-page, table)
3. pencil_get_style_guide_tags   → Get available style tags (if designing from scratch)
4. pencil_get_style_guide        → Get visual direction with 5-10 tags
5. pencil_get_variables          → Read design tokens ($--colors, $--fonts, etc.)
6. pencil_batch_get              → Inspect existing nodes and components
7. pencil_batch_design           → Create/modify design (max 25 ops per call)
8. pencil_get_screenshot         → Verify visual output

Core Concepts

The .pen File Structure

A .pen file is a JSON document containing:

  • children: Array of top-level frames (screens, components)
  • variables: Design tokens (colors, fonts, radii)
  • themes: Theme axes and values
  • fonts: Custom font definitions

Node Types

Type Description Key Properties
frame Container with layout layout, gap, padding, fill, clip
text Text content content, fontSize, fontFamily, fill, textGrowth
rectangle Shape fill, stroke, cornerRadius
ellipse Oval/circle fill, stroke
icon_font Icon from font set iconFontFamily, iconFontName, fill
ref Component instance ref (points to reusable component), descendants
group Logical grouping children, optional layout

Flexbox Layout System

Frames use flexbox by default. Key properties:

{
  layout: "vertical" | "horizontal" | "none",  // none = absolute positioning
  gap: 16,                                      // spacing between children
  padding: 24 | [24, 32] | [top, right, bottom, left],
  justifyContent: "start" | "center" | "end" | "space_between" | "space_around",
  alignItems: "start" | "center" | "end"
}

Dynamic Sizing

Value Behavior
"fill_container" Expand to fill parent (requires parent layout)
"fit_content" Shrink to content size
"fill_container(200)" Fill with 200px fallback
"fit_content(200)" Fit content with 200px fallback
200 Fixed 200px

Critical Rules:

  • fill_container only works when parent has layout: "vertical" or "horizontal"
  • fit_content only works on frames with layout
  • Cannot have all children as fill_container while parent is fit_content (circular dependency)

batch_design Operations

The primary design tool. Supports Insert, Copy, Update, Replace, Move, Delete, and Generate operations.

Operation Syntax

// Insert - create new node
binding=I(parent, {type: "frame", ...props})

// Copy - duplicate existing node
binding=C("sourceId", parent, {descendants: {...overrides}})

// Update - modify properties (NOT children)
U("nodeId", {property: "value"})
U(binding+"/childId", {content: "Updated"})

// Replace - swap node entirely
binding=R("nodeId", {type: "text", content: "New content"})

// Move - change parent or order
M("nodeId", "newParent", index)

// Delete - remove node
D("nodeId")

// Generate image - apply to frame/rectangle
G(binding, "ai" | "stock", "prompt describing image")

Critical Rules

  1. Maximum 25 operations per call — split larger designs into multiple calls
  2. Bindings are single-use — each Insert/Copy/Replace creates a binding valid only within that call
  3. IDs regenerate on Copy — use descendants in Copy operation, NOT separate Update calls:
// CORRECT - override during copy
copiedBtn=C("btnId", container, {descendants: {"labelId": {content: "New Text"}}})

// WRONG - IDs changed, will fail
copiedBtn=C("btnId", container, {})
U(copiedBtn+"/labelId", {content: "New Text"})  // Error: node not found
  1. Use placeholders for screens — always set placeholder: true while working, remove when done:
// Start work
screen=I(document, {type: "frame", name: "Dashboard", placeholder: true, ...})

// ... do design work ...

// Finish
U("screenId", {placeholder: false})
  1. Never set x/y in flexbox — position properties are ignored when parent has layout

Working with Component Instances

Components are nodes with reusable: true. Instances use type: "ref":

// Insert instance
card=I(container, {type: "ref", ref: "CardComponentId"})

// Override instance properties (root level)
card=I(container, {type: "ref", ref: "CardId", width: "fill_container"})

// Override descendant properties
U(card+"/titleText", {content: "New Title"})
U(card+"/icon", {iconFontName: "settings"})

// Replace slot content entirely
newContent=R(card+"/contentSlot", {type: "frame", layout: "vertical", children: [...]})

// Insert into slots
item=I(card+"/slotId", {type: "ref", ref: "ListItemId"})

Nested Instance Overrides

For deeply nested components, use path notation:

// sidebar contains menuComponent, which contains buttonComponent
U("sidebar/menuComponent/buttonComponent/label", {content: "Updated"})

Guidelines Topics

Call pencil_get_guidelines with specific topics:

Topic When to Use
design-system Building with existing components, dashboards, SaaS apps
landing-page Marketing pages, websites, promotional content
table Data tables, grids
tailwind Generating Tailwind CSS code from designs
code Generating any code from .pen files

Always load relevant guidelines before starting design work.


Style Guides

For creative direction on new designs:

// 1. Get available tags
pencil_get_style_guide_tags()

// 2. Select 5-10 relevant tags
pencil_get_style_guide({
  tags: ["webapp", "dark-mode", "minimal", "professional", "tech"]
})

Style guides provide:

  • Color palettes with hex values
  • Typography scales (fonts, sizes, weights)
  • Spacing systems (gaps, padding)
  • Component patterns with exact properties
  • Design philosophy and dos/don'ts

When to use style guides:

  • Designing from scratch (blank canvas)
  • User requests specific aesthetic
  • Landing pages, marketing sites
  • Exploring visual directions

When to skip:

  • Pure compositional tasks ("add a button here")
  • Using existing design system components

Text Handling

textGrowth Property

Controls text box sizing and wrapping:

Value Width Height Line Wrap
"auto" (default) Calculated Calculated Never
"fixed-width" Must specify Calculated Yes
"fixed-width-height" Must specify Must specify Yes

Critical: Never set width/height on text without also setting textGrowth.

// Single line, auto-sized
{type: "text", content: "Hello"}

// Multi-line with wrapping
{type: "text", content: "Long paragraph...", textGrowth: "fixed-width", width: "fill_container"}

// Fixed box with overflow
{type: "text", content: "Fixed area", textGrowth: "fixed-width-height", width: 200, height: 100}

Text Alignment

  • textAlign: "left" | "center" | "right" | "justify" — horizontal alignment within text box
  • textAlignVertical: "top" | "middle" | "bottom" — vertical alignment

Note: These only have visible effect with textGrowth set. To position the text box itself, use parent flexbox properties.


Icons

Use icon_font type with these font families:

Family Style Example Names
lucide Outline, rounded home, settings, user, search, plus, x
feather Outline, rounded Same as lucide
Material Symbols Outlined Outline home, settings, person, search, add, close
Material Symbols Rounded Rounded Same as outlined
Material Symbols Sharp Sharp corners Same as outlined
icon=I(container, {
  type: "icon_font",
  iconFontFamily: "lucide",
  iconFontName: "settings",
  width: 24,
  height: 24,
  fill: "#333333"
})

Must specify both width and height for icons.


Images

There is NO "image" node type! Images are fills applied to frame/rectangle nodes.

Using the G (Generate) Operation

// Create frame, then apply image
heroFrame=I(container, {type: "frame", width: 400, height: 300})
G(heroFrame, "ai", "modern office workspace, bright natural lighting")

// Stock photo
G("existingFrameId", "stock", "mountain landscape sunset")

Image Types

Type Source Best For
"ai" AI-generated Custom illustrations, specific scenes, brand assets
"stock" Unsplash photos Real photography, authentic imagery

Writing Effective Prompts

AI prompts — describe scene, style, mood:

  • Weak: "A laptop"
  • Better: "Modern laptop on wooden desk, soft morning light, minimal workspace"

Stock queries — use descriptive keywords:

  • "modern office workspace bright"
  • "team collaboration meeting diverse"
  • "abstract gradient blue purple"

Design Variables

Use pencil_get_variables to read tokens, then reference with $ prefix:

{
  type: "text",
  content: "Hello",
  fill: "$--foreground",           // Color variable
  fontFamily: "$--font-primary"    // Font variable
}

Common variable patterns:

  • $--background, $--foreground — base colors
  • $--primary, $--secondary — brand colors
  • $--font-primary, $--font-secondary — typefaces
  • $--radius-m, $--radius-pill — corner radii
  • $--border — border color

Always use variables over hardcoded values when available.


Visual Verification

Always screenshot after significant changes:

pencil_get_screenshot({filePath: "design.pen", nodeId: "screenId"})

Check for:

  • Correct spacing and alignment
  • Text overflow or clipping
  • Color contrast issues
  • Missing content
  • Layout problems

Use pencil_snapshot_layout to check spatial relationships:

pencil_snapshot_layout({
  filePath: "design.pen",
  parentId: "screenId",
  maxDepth: 2
})

Returns positions, sizes, and layout problems (clipping, overlaps).


Common Patterns

Screen with Sidebar

screen=I(document, {type: "frame", name: "Dashboard", layout: "horizontal", width: 1440, height: "fit_content(900)", fill: "$--background", placeholder: true})
sidebar=I(screen, {type: "ref", ref: "sidebarId", height: "fill_container"})
main=I(screen, {type: "frame", layout: "vertical", width: "fill_container", height: "fill_container", padding: 32, gap: 24})

Card with Header/Content/Actions

card=I(container, {type: "ref", ref: "cardId", width: "fill_container"})
header=R(card+"/headerSlot", {type: "frame", layout: "vertical", gap: 4, padding: 24, width: "fill_container", children: [
  {type: "text", content: "Title", fontSize: 18, fontWeight: "600"},
  {type: "text", content: "Description", fontSize: 14, fill: "$--muted-foreground"}
]})
U(card+"/contentSlot", {layout: "vertical", gap: 16, padding: 24})
U(card+"/actionsSlot", {justifyContent: "end", padding: 24})

Table Structure

Tables follow strict hierarchy: Table → Row → Cell (frame) → Content

tableRow=I("tableId", {type: "frame", layout: "horizontal", width: "fill_container"})
cell1=I(tableRow, {type: "frame", width: "fill_container"})
cellContent1=I(cell1, {type: "text", content: "John Doe"})
cell2=I(tableRow, {type: "frame", width: "fill_container"})
cellContent2=I(cell2, {type: "text", content: "john@example.com"})

Never skip the cell frame — content goes inside cells, not directly in rows.

Form Layout

form=I(card+"/contentSlot", {type: "frame", layout: "vertical", gap: 16, width: "fill_container"})
row=I(form, {type: "frame", layout: "horizontal", gap: 16, width: "fill_container"})
firstName=I(row, {type: "ref", ref: "inputGroupId", width: "fill_container", descendants: {"labelId": {content: "First Name"}}})
lastName=I(row, {type: "ref", ref: "inputGroupId", width: "fill_container", descendants: {"labelId": {content: "Last Name"}}})
email=I(form, {type: "ref", ref: "inputGroupId", width: "fill_container", descendants: {"labelId": {content: "Email"}}})

Metric Cards Grid

metrics=I(content, {type: "frame", layout: "horizontal", gap: 16, width: "fill_container"})
metric1=I(metrics, {type: "ref", ref: "metricCardId", width: "fill_container"})
U(metric1+"/label", {content: "total_users"})
U(metric1+"/value", {content: "12,543"})
U(metric1+"/change", {content: "+12.5%"})

Inspection Tools

pencil_batch_get

Read node structure:

// Read specific nodes
pencil_batch_get({
  filePath: "design.pen",
  nodeIds: ["nodeId1", "nodeId2"],
  readDepth: 3
})

// Search for patterns
pencil_batch_get({
  filePath: "design.pen",
  patterns: [{reusable: true}],  // Find all components
  readDepth: 2,
  searchDepth: 5
})

// Read document root
pencil_batch_get({
  filePath: "design.pen"
})

pencil_get_editor_state

Get current context:

pencil_get_editor_state({include_schema: true})

Returns:

  • Active file path
  • Selected nodes
  • Reusable components list
  • .pen schema (if requested)

Error Handling

Common Errors

Error Cause Solution
"Node not found" Wrong ID or ID changed after copy Use descendants in Copy, verify IDs with batch_get
"oldString found multiple times" Ambiguous match Add more context to oldString
"Circular dependency" Parent fit_content with all children fill_container Give at least one child fixed size
"Operations rolled back" Any operation failed Fix the failed operation, re-run entire batch

Debugging Tips

  1. Use batch_get before modifying — understand current structure
  2. Screenshot frequently — catch visual issues early
  3. Work in small batches — easier to identify failures
  4. Check layout problems — use snapshot_layout with problemsOnly: true

Anti-Patterns

Don't

  • Use Update on copied node descendants (IDs change)
  • Set x/y when parent has layout
  • Skip cell frames in tables
  • Hardcode colors when variables exist
  • Create image nodes (use G operation on frames)
  • Set width/height on text without textGrowth
  • Forget placeholder: true when working
  • Forget to remove placeholder when done
  • Exceed 25 operations per batch_design call

Do

  • Use descendants in Copy for overrides
  • Use fill_container/fit_content for sizing
  • Follow Table → Row → Cell → Content hierarchy
  • Reference $--variables for colors and fonts
  • Apply images as fills via G operation
  • Set textGrowth before width/height
  • Screenshot after every major change
  • Remove placeholders when sections complete
  • Split large designs into logical batches

Workflow Example: Dashboard Screen

// 1. Get context
pencil_get_editor_state({include_schema: false})
pencil_get_guidelines({topic: "design-system"})
pencil_get_variables({filePath: "app.pen"})

// 2. Inspect available components
pencil_batch_get({
  filePath: "app.pen",
  patterns: [{reusable: true}],
  readDepth: 2
})

// 3. Create screen structure (first batch)
screen=I(document, {type: "frame", name: "Dashboard", layout: "horizontal", width: 1440, height: "fit_content(900)", fill: "$--background", placeholder: true})
sidebar=I(screen, {type: "ref", ref: "sidebarId", height: "fill_container"})
main=I(screen, {type: "frame", layout: "vertical", width: "fill_container", height: "fill_container", padding: 32, gap: 24})

// 4. Add header section (second batch)
header=I("mainId", {type: "frame", layout: "horizontal", justifyContent: "space_between", alignItems: "center", width: "fill_container"})
title=I(header, {type: "text", content: "Dashboard", fontSize: 32, fontWeight: "600"})
actions=I(header, {type: "frame", layout: "horizontal", gap: 12})
btn=I(actions, {type: "ref", ref: "buttonPrimaryId", descendants: {"labelId": {content: "New Item"}}})

// 5. Add metrics row (third batch)
metrics=I("mainId", {type: "frame", layout: "horizontal", gap: 16, width: "fill_container"})
metric1=I(metrics, {type: "ref", ref: "metricCardId", width: "fill_container"})
metric2=I(metrics, {type: "ref", ref: "metricCardId", width: "fill_container"})
metric3=I(metrics, {type: "ref", ref: "metricCardId", width: "fill_container"})

// 6. Verify
pencil_get_screenshot({filePath: "app.pen", nodeId: "screenId"})

// 7. Remove placeholder when done
U("screenId", {placeholder: false})

Tool Reference Summary

Tool Purpose Key Parameters
pencil_get_editor_state Get current file, selection, schema include_schema
pencil_get_guidelines Load design rules topic
pencil_get_style_guide_tags List available style tags
pencil_get_style_guide Get visual direction tags[] or id
pencil_get_variables Read design tokens filePath
pencil_batch_get Read node structure nodeIds, patterns, readDepth, searchDepth
pencil_batch_design Create/modify design filePath, operations
pencil_get_screenshot Visual verification filePath, nodeId
pencil_snapshot_layout Check spatial layout filePath, parentId, maxDepth, problemsOnly
pencil_find_empty_space_around_node Find placement space nodeId, direction, width, height, padding
pencil_set_variables Update design tokens filePath, variables
pencil_open_document Open/create .pen file filePathOrTemplate

This skill covers the Pencil MCP design system. For authoritative documentation, consult pencil.dev or use pencil_get_guidelines with specific topics.