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_containeronly works when parent haslayout: "vertical"or"horizontal"fit_contentonly works on frames withlayout- Cannot have all children as
fill_containerwhile parent isfit_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
- Maximum 25 operations per call — split larger designs into multiple calls
- Bindings are single-use — each Insert/Copy/Replace creates a binding valid only within that call
- IDs regenerate on Copy — use
descendantsin 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
- Use placeholders for screens — always set
placeholder: truewhile working, remove when done:
// Start work
screen=I(document, {type: "frame", name: "Dashboard", placeholder: true, ...})
// ... do design work ...
// Finish
U("screenId", {placeholder: false})
- 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 boxtextAlignVertical: "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
- Use batch_get before modifying — understand current structure
- Screenshot frequently — catch visual issues early
- Work in small batches — easier to identify failures
- 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.