14 KiB
Penpot Button Component Variants - Chat Conversation
User Request
Generate component variants for the selected component in Penpot via MCP (Multi-Component Penpot).
Specifically, create Button Component Variants that include the necessary styles for the following states:
- Default (Current state)
- Hover
- Focus
- Disabled
Ensure the newly generated variants are created as Penpot Component Variants that are available and accessible through the MCP. The new variants must be placed adjacent to the currently selected component within the Penpot file.
Step 1: Analyze Selected Component
First, I checked what was currently selected in Penpot and understood its structure.
Code Executed:
// Get the current selection and understand its structure
const selection = penpot.selection;
if (selection.length === 0) {
return { error: "No shape selected. Please select a button component in Penpot." };
}
const selectedShape = selection[0];
return {
selectionCount: selection.length,
shape: {
id: selectedShape.id,
name: selectedShape.name,
type: selectedShape.type,
x: selectedShape.x,
y: selectedShape.y,
width: selectedShape.width,
height: selectedShape.height,
fills: selectedShape.fills,
strokes: selectedShape.strokes,
opacity: selectedShape.opacity,
borderRadius: selectedShape.borderRadius,
shadow: selectedShape.shadow
},
structure: penpotUtils.shapeStructure(selectedShape, 3)
};
Result:
{
"result": {
"selectionCount": 1,
"shape": {
"id": "0cdc9585-d652-800e-8007-31c097560a1a",
"name": "button",
"type": "board",
"x": 243,
"y": 520,
"width": 169,
"height": 60,
"fills": [
{
"fillColor": "#fbfd82",
"fillOpacity": 1
}
],
"strokes": [],
"opacity": null,
"borderRadius": 32
},
"structure": {
"id": "0cdc9585-d652-800e-8007-31c097560a1a",
"name": "button",
"type": "board",
"children": [
{
"id": "0cdc9585-d652-800e-8007-31c097560a1b",
"name": "The Equalizer",
"type": "text"
}
]
}
},
"log": ""
}
Finding: The selected button is a board with a yellow fill (#fbfd82), border radius of 32, and contains a text element "The Equalizer".
Step 2: Get Text Element Details
Code Executed:
// Get detailed info about the text element inside the button
const selectedShape = penpot.selection[0];
const textShape = penpotUtils.findShape(s => s.type === 'text', selectedShape);
if (!textShape) {
return { error: "No text element found inside the button" };
}
return {
text: {
id: textShape.id,
name: textShape.name,
type: textShape.type,
x: textShape.x,
y: textShape.y,
parentX: textShape.parentX,
parentY: textShape.parentY,
width: textShape.width,
height: textShape.height,
characters: textShape.characters,
fontId: textShape.fontId,
fontFamily: textShape.fontFamily,
fontVariantId: textShape.fontVariantId,
fontSize: textShape.fontSize,
fontWeight: textShape.fontWeight,
fontStyle: textShape.fontStyle,
lineHeight: textShape.lineHeight,
letterSpacing: textShape.letterSpacing,
textTransform: textShape.textTransform,
fills: textShape.fills,
align: textShape.align,
verticalAlign: textShape.verticalAlign
}
};
Result:
{
"result": {
"text": {
"id": "0cdc9585-d652-800e-8007-31c097560a1b",
"name": "The Equalizer",
"type": "text",
"x": 263,
"y": 538,
"parentX": 20,
"parentY": 18,
"width": 129,
"height": 24,
"characters": "The Equalizer",
"fontId": "gfont-dm-sans",
"fontFamily": "DM Sans",
"fontVariantId": "600",
"fontSize": "20",
"fontWeight": "600",
"fontStyle": "normal",
"lineHeight": "1.2",
"letterSpacing": "0",
"textTransform": "none",
"fills": [
{
"fillColor": "#000000",
"fillOpacity": 1
}
],
"align": "left",
"verticalAlign": null
}
},
"log": ""
}
Finding: Text uses DM Sans font, 600 weight, 20px size, black color (#000000).
Step 3: Create Button Variant Shapes
Code Executed:
// Get the original button details
const originalButton = penpot.selection[0];
const buttonWidth = originalButton.width;
const buttonHeight = originalButton.height;
const buttonX = originalButton.x;
const buttonY = originalButton.y;
const spacing = 30; // Space between variants
// Original button properties
const originalFill = originalButton.fills[0];
const borderRadius = originalButton.borderRadius;
// Get text properties from original
const originalText = penpotUtils.findShape(s => s.type === 'text', originalButton);
const textParentX = originalText.parentX;
const textParentY = originalText.parentY;
const textContent = originalText.characters;
const textFontId = originalText.fontId;
const textFontFamily = originalText.fontFamily;
const textFontVariantId = originalText.fontVariantId;
const textFontSize = originalText.fontSize;
const textFontWeight = originalText.fontWeight;
const textFills = originalText.fills;
// Define variant styles
const variants = [
{
name: "Button / Default",
fillColor: "#fbfd82",
fillOpacity: 1,
textColor: "#000000",
textOpacity: 1,
strokeColor: null,
strokeWidth: 0
},
{
name: "Button / Hover",
fillColor: "#e5e76d", // Darker yellow for hover
fillOpacity: 1,
textColor: "#000000",
textOpacity: 1,
strokeColor: null,
strokeWidth: 0
},
{
name: "Button / Focus",
fillColor: "#fbfd82",
fillOpacity: 1,
textColor: "#000000",
textOpacity: 1,
strokeColor: "#3b82f6", // Blue focus ring
strokeWidth: 3
},
{
name: "Button / Disabled",
fillColor: "#d1d5db", // Grey background
fillOpacity: 1,
textColor: "#9ca3af", // Grey text
textOpacity: 1,
strokeColor: null,
strokeWidth: 0
}
];
const createdVariants = [];
// Create each variant
for (let i = 0; i < variants.length; i++) {
const variant = variants[i];
const xPos = buttonX + (i * (buttonWidth + spacing));
// Create the button board/frame
const board = penpot.createBoard();
board.name = variant.name;
board.x = xPos;
board.y = buttonY;
board.resize(buttonWidth, buttonHeight);
board.borderRadius = borderRadius;
// Set fills
board.fills = [{
fillColor: variant.fillColor,
fillOpacity: variant.fillOpacity
}];
// Set strokes for focus state
if (variant.strokeColor) {
board.strokes = [{
strokeColor: variant.strokeColor,
strokeOpacity: 1,
strokeWidth: variant.strokeWidth,
strokeAlignment: "outer"
}];
}
// Create text inside the board
const text = penpot.createText(textContent);
text.x = xPos + textParentX;
text.y = buttonY + textParentY;
text.fontId = textFontId;
text.fontFamily = textFontFamily;
text.fontVariantId = textFontVariantId;
text.fontSize = textFontSize;
text.fontWeight = textFontWeight;
text.fills = [{
fillColor: variant.textColor,
fillOpacity: variant.textOpacity
}];
// Add text to the board
board.appendChild(text);
createdVariants.push({
name: variant.name,
id: board.id,
x: board.x,
y: board.y
});
}
return {
message: "Successfully created 4 button variants",
variants: createdVariants
};
Result:
{
"result": {
"message": "Successfully created 4 button variants",
"variants": [
{
"name": "Button / Default",
"id": "0cdc9585-d652-800e-8007-31c2c638272d",
"x": 243,
"y": 520
},
{
"name": "Button / Hover",
"id": "0cdc9585-d652-800e-8007-31c2c64462ea",
"x": 442,
"y": 520
},
{
"name": "Button / Focus",
"id": "0cdc9585-d652-800e-8007-31c2c64caa58",
"x": 641,
"y": 520
},
{
"name": "Button / Disabled",
"id": "0cdc9585-d652-800e-8007-31c2c654f6a1",
"x": 840,
"y": 520
}
]
},
"log": ""
}
Step 4: Register as Penpot Components
Code Executed:
// Get all the created variant boards by their IDs
const variantIds = [
"0cdc9585-d652-800e-8007-31c2c638272d", // Default
"0cdc9585-d652-800e-8007-31c2c64462ea", // Hover
"0cdc9585-d652-800e-8007-31c2c64caa58", // Focus
"0cdc9585-d652-800e-8007-31c2c654f6a1" // Disabled
];
const createdComponents = [];
for (const id of variantIds) {
const shape = penpotUtils.findShapeById(id);
if (shape) {
// Create a component from this shape
const component = penpot.library.local.createComponent([shape]);
component.name = shape.name;
createdComponents.push({
componentId: component.id,
componentName: component.name,
shapeId: shape.id
});
}
}
return {
message: "Created " + createdComponents.length + " component variants in the local library",
components: createdComponents
};
Result:
{
"result": {
"message": "Created 4 component variants in the local library",
"components": [
{
"componentId": "0cdc9585-d652-800e-8007-31c2cdba7ca5",
"componentName": "Default",
"shapeId": "0cdc9585-d652-800e-8007-31c2c638272d"
},
{
"componentId": "0cdc9585-d652-800e-8007-31c2cdbbd692",
"componentName": "Hover",
"shapeId": "0cdc9585-d652-800e-8007-31c2c64462ea"
},
{
"componentId": "0cdc9585-d652-800e-8007-31c2cdbd2487",
"componentName": "Focus",
"shapeId": "0cdc9585-d652-800e-8007-31c2c64caa58"
},
{
"componentId": "0cdc9585-d652-800e-8007-31c2cdbe1800",
"componentName": "Disabled",
"shapeId": "0cdc9585-d652-800e-8007-31c2c654f6a1"
}
]
},
"log": ""
}
Step 5: Verify Library Components
Code Executed:
// Check all components in the library
const library = penpot.library.local;
const allComponents = library.components;
return {
totalComponents: allComponents.length,
components: allComponents.map(c => ({
id: c.id,
name: c.name
}))
};
Result:
{
"result": {
"totalComponents": 4,
"components": [
{
"id": "0cdc9585-d652-800e-8007-31c2cdba7ca5",
"name": "Default"
},
{
"id": "0cdc9585-d652-800e-8007-31c2cdbbd692",
"name": "Hover"
},
{
"id": "0cdc9585-d652-800e-8007-31c2cdbd2487",
"name": "Focus"
},
{
"id": "0cdc9585-d652-800e-8007-31c2cdbe1800",
"name": "Disabled"
}
]
},
"log": ""
}
Step 6: Clean Up Shape Names
Code Executed:
// Clean up the shape names
const variantData = [
{ id: "0cdc9585-d652-800e-8007-31c2c638272d", cleanName: "Button / Default" },
{ id: "0cdc9585-d652-800e-8007-31c2c64462ea", cleanName: "Button / Hover" },
{ id: "0cdc9585-d652-800e-8007-31c2c64caa58", cleanName: "Button / Focus" },
{ id: "0cdc9585-d652-800e-8007-31c2c654f6a1", cleanName: "Button / Disabled" }
];
const cleaned = [];
for (const item of variantData) {
const shape = penpotUtils.findShapeById(item.id);
if (shape) {
shape.name = item.cleanName;
cleaned.push({
id: shape.id,
name: shape.name,
state: item.cleanName.split(" / ")[1]
});
}
}
return {
message: "Shape names cleaned up",
shapes: cleaned
};
Result:
{
"result": {
"message": "Shape names cleaned up",
"shapes": [
{
"id": "0cdc9585-d652-800e-8007-31c2c638272d",
"name": "Button / Default",
"state": "Default"
},
{
"id": "0cdc9585-d652-800e-8007-31c2c64462ea",
"name": "Button / Hover",
"state": "Hover"
},
{
"id": "0cdc9585-d652-800e-8007-31c2c64caa58",
"name": "Button / Focus",
"state": "Focus"
},
{
"id": "0cdc9585-d652-800e-8007-31c2c654f6a1",
"name": "Button / Disabled",
"state": "Disabled"
}
]
},
"log": ""
}
Final Summary
✅ Button Component Variants Created
| Variant | Background Color | Text Color | Additional Styling | Position |
|---|---|---|---|---|
| Default | #fbfd82 (Bright Yellow) |
#000000 (Black) |
None | x: 243 |
| Hover | #e5e76d (Darker Yellow) |
#000000 (Black) |
None | x: 442 |
| Focus | #fbfd82 (Bright Yellow) |
#000000 (Black) |
Blue outline stroke (#3b82f6, 3px) |
x: 641 |
| Disabled | #d1d5db (Grey) |
#9ca3af (Light Grey) |
None | x: 840 |
What was done:
-
Analyzed the selected button - Identified its structure (board with text child), styling (yellow fill
#fbfd82, border-radius 32), and text properties (DM Sans font, 600 weight, 20px) -
Created 4 button variants placed horizontally adjacent to each other:
- Button / Default - Preserves the original styling
- Button / Hover - Slightly darker yellow background for hover interaction feedback
- Button / Focus - Original yellow with a blue outline (3px) for keyboard focus indication
- Button / Disabled - Greyed out appearance with muted text
-
Registered as Penpot Components - All 4 variants are now available in the local component library and accessible through MCP
Component Library Access
The components are now available in your Penpot file's local library with names:
DefaultHoverFocusDisabled
You can find them in the Assets panel under Components in Penpot, and they can be instantiated anywhere in your project using the MCP tools or directly in Penpot's UI.
Generated: December 1, 2025