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...
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.
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
A .pen file is a JSON document containing:
| 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 |
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"
}
| 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 layoutfill_container while parent is fit_content (circular dependency)The primary design tool. Supports Insert, Copy, Update, Replace, Move, Delete, and Generate operations.
// 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")
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
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})
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"})
For deeply nested components, use path notation:
// sidebar contains menuComponent, which contains buttonComponent
U("sidebar/menuComponent/buttonComponent/label", {content: "Updated"})
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.
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:
When to use style guides:
When to skip:
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}
textAlign: "left" | "center" | "right" | "justify" — horizontal alignment within text boxtextAlignVertical: "top" | "middle" | "bottom" — vertical alignmentNote: These only have visible effect with textGrowth set. To position the text box itself, use parent flexbox properties.
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.
There is NO "image" node type! Images are fills applied to frame/rectangle nodes.
// 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")
| Type | Source | Best For |
|---|---|---|
"ai" |
AI-generated | Custom illustrations, specific scenes, brand assets |
"stock" |
Unsplash photos | Real photography, authentic imagery |
AI prompts — describe scene, style, mood:
Stock queries — use descriptive keywords:
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 colorAlways use variables over hardcoded values when available.
Always screenshot after significant changes:
pencil_get_screenshot({filePath: "design.pen", nodeId: "screenId"})
Check for:
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).
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=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})
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=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"}}})
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%"})
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"
})
Get current context:
pencil_get_editor_state({include_schema: true})
Returns:
| 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 |
problemsOnly: true// 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 | 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.