webflow/webflow-code-component:convert-component
Convert an existing React component into a Webflow Code Component. Analyzes TypeScript props, maps to Webflow prop types, generates the .webflow.tsx definition file, and identifies required modifications.
npx skills add https://github.com/webflow/webflow-skills --skill webflow-code-component:convert-component
Convert an existing React component into a Webflow Code Component by analyzing its structure and generating the appropriate .webflow.tsx definition file.
Use when:
Do NOT use when:
children| Pattern | Issue | Resolution |
|---------|-------|------------|
| React Context usage | Context doesn't work across Webflow components | Refactor to props or use nano stores |
| window/document in render | SSR will fail | Wrap in useEffect or set ssr: false |
| localStorage/sessionStorage in render | SSR will fail | Wrap in useEffect or set ssr: false |
| Complex object props | Can't map to Webflow prop types | Break into individual props |
| Function props (callbacks) | Not supported in Webflow | Remove or internalize logic |
| useContext hook | Won't work across components | Use alternative state patterns |
| External CSS imports | May not work in Shadow DOM | Import in .webflow.tsx instead |
| CSS class references to global styles | Won't work in Shadow DOM | Use component-scoped styles |
| styled-components | Needs Shadow DOM decorator | Set up globals.ts with decorator |
| Emotion (@emotion/styled) | Needs Shadow DOM decorator | Set up globals.ts with decorator |
If using styled-components:
npm i @webflow/styled-components-utils styled-components
Create/update globals.ts:
import { styledComponentsShadowDomDecorator } from "@webflow/styled-components-utils";
export const decorators = [styledComponentsShadowDomDecorator];
If using Emotion:
npm i @webflow/emotion-utils @emotion/cache @emotion/react
Create/update globals.ts:
import { emotionShadowDomDecorator } from "@webflow/emotion-utils";
export const decorators = [emotionShadowDomDecorator];
For both CSS-in-JS approaches, update webflow.json:
styled-components:
{
"library": {
"globals": "./src/globals.ts",
"renderer": {
"server": "@webflow/styled-components-utils/server"
}
}
}
Emotion:
{
"library": {
"globals": "./src/globals.ts",
"renderer": {
"server": "@webflow/emotion-utils/server"
}
}
}
| TypeScript Type | Webflow Prop | Notes |
|-----------------|--------------|-------|
| string | props.Text() | Default for short text |
| string (long/HTML content) | props.RichText() | If prop name suggests content/body/description |
| React.ReactNode / children | props.Slot() | For nested content |
| number | props.Number() | Numeric values |
| boolean | props.Boolean() | Toggles |
| "option1" \| "option2" | props.Variant() | String literal unions (requires options array) |
| enum | props.Variant() | Convert enum values to options array (required) |
| { href: string; ... } | props.Link() | Returns { href, target?, preload? } object — may need wrapper if component expects separate href/target props |
| Image-related types | props.Image() | Image src, url, etc. |
| string (canvas-editable text) | props.TextNode() | For text editable directly on canvas; has multiline param |
| boolean (show/hide) | props.Visibility() | Semantic show/hide toggle |
| string (for HTML id) | props.Id() | If prop is named "id" or used for accessibility |
| Complex objects | SPLIT | Break into multiple simple props |
| Functions/callbacks | REMOVE | Not supported |
| Arrays | SPECIAL | May need component redesign |
Complex object props - Break them down:
// Original
interface Props {
author: {
name: string;
avatar: string;
bio: string;
}
}
// Converted to flat props
props: {
authorName: props.Text({ name: "Author Name" }),
authorAvatar: props.Image({ name: "Author Avatar" }),
authorBio: props.RichText({ name: "Author Bio" })
}
Union types with more than simple strings:
// Original - complex union
type Size = "sm" | "md" | "lg" | { width: number; height: number };
// Convert to Variant with only string options
size: props.Variant({
name: "Size",
options: ["sm", "md", "lg", "custom"],
defaultValue: "md"
})
// Note: Custom size would need additional Number props
Optional props - Provide defaultValue for prop types that support it. Note: Link, Image, Slot, and Id do not accept defaultValue.
// Original
interface Props {
title?: string;
}
// Converted - provide default for types that support it
title: props.Text({
name: "Title",
defaultValue: "" // Empty string or sensible default
})
webflow.json in project root.webflow.tsx should be created (same directory)10. Create the .webflow.tsx file:
import { declareComponent } from "@webflow/react";
import { props } from "@webflow/data-types";
import { ComponentName } from "./ComponentName";
// Import styles if they exist
import "./ComponentName.module.css"; // or .css
export default declareComponent(ComponentName, {
name: "ComponentName",
description: "[Generated from component purpose]",
group: "[Appropriate category]",
props: {
// Mapped props here
},
// decorators: [], // Optional — per-component decorators (e.g., for CSS-in-JS Shadow DOM support)
options: {
applyTagSelectors: true, // Default is false. Set to true to apply Webflow's tag selectors (e.g., h1, p styles) inside the component.
ssr: true // or false if browser APIs detected
}
});
11. Provide the complete file with all props mapped
12. List modifications needed to the original component:
## Required Changes to [ComponentName].tsx
### Must Fix (Component won't work without these):
- [ ] Issue 1: [Description and how to fix]
- [ ] Issue 2: [Description and how to fix]
### Recommended (Will improve Webflow integration):
- [ ] Recommendation 1
- [ ] Recommendation 2
### Props Mapping Summary:
| Original Prop | Webflow Prop | Notes |
|---------------|--------------|-------|
| title: string | props.Text() | Direct mapping |
| isOpen: boolean | props.Boolean() | Direct mapping |
| data: object | SPLIT → dataTitle, dataValue | Flattened |
| onClick: function | REMOVED | Not supported |
13. Provide modified component code if changes are needed:
User prompt:
Convert this React component to work with Webflow:
// TestimonialCard.tsx
interface TestimonialCardProps {
quote: string;
authorName: string;
authorTitle?: string;
rating?: number;
}
export const TestimonialCard: React.FC<TestimonialCardProps> = ({
quote,
authorName,
authorTitle = "",
rating = 5
}) => {
return (
<div className="testimonial-card">
<div className="stars">{"★".repeat(rating)}</div>
<blockquote>{quote}</blockquote>
<div className="author">
<strong>{authorName}</strong>
{authorTitle && <span>{authorTitle}</span>}
</div>
</div>
);
};
Response:
## Component Analysis: TestimonialCard
### Props Detected:
| Prop | TypeScript Type | Webflow Mapping |
|------|-----------------|-----------------|
| quote | string | props.Text() |
| authorName | string | props.Text() |
| authorTitle | string (optional) | props.Text() |
| rating | number (optional) | props.Number() |
### Issues Found:
- **CSS class "testimonial-card"** - Uses global class that won't work in Shadow DOM
- Fix: Convert to CSS Modules or import scoped styles
### No blocking issues - ready to convert!
---
## Generated Files
### TestimonialCard.webflow.tsx
import { declareComponent } from "@webflow/react";
import { props } from "@webflow/data-types";
import { TestimonialCard } from "./TestimonialCard";
import "./TestimonialCard.module.css";
export default declareComponent(TestimonialCard, {
name: "TestimonialCard",
description: "Display customer testimonials with rating stars",
group: "Display",
props: {
quote: props.Text({
name: "Quote",
defaultValue: "This product changed my life!"
}),
authorName: props.Text({
name: "Author Name",
defaultValue: "Jane Doe"
}),
authorTitle: props.Text({
name: "Author Title",
defaultValue: "CEO, Company"
}),
rating: props.Number({
name: "Rating",
defaultValue: 5,
min: 1,
max: 5
})
},
options: {
applyTagSelectors: true,
ssr: true
}
});
### Required Changes to TestimonialCard.tsx
1. **Convert to CSS Modules** - Rename `TestimonialCard.css` to `TestimonialCard.module.css` and update imports:
import styles from "./TestimonialCard.module.css";
// Default CSS Modules uses bracket notation:
<div className={styles["testimonial-card"]}>
<div className={styles["stars"]}>
// Dot notation (styles.testimonialCard) requires camelCase class names
// or a webpack override for css-loader.
### Setup Check
- [ ] Verify webflow.json exists
- [ ] Install dependencies if needed: `npm i --save-dev @webflow/webflow-cli @webflow/data-types @webflow/react`
User prompt:
Make this work as a Webflow code component:
// Modal.tsx
import { createContext, useContext, useState } from 'react';
const ModalContext = createContext<{ isOpen: boolean; toggle: () => void } | null>(null);
export const useModal = () => {
const context = useContext(ModalContext);
if (!context) throw new Error('useModal must be used within ModalProvider');
return context;
};
interface ModalProps {
trigger: React.ReactNode;
children: React.ReactNode;
onOpen?: () => void;
onClose?: () => void;
}
export const Modal: React.FC<ModalProps> = ({ trigger, children, onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => {
const newState = !isOpen;
setIsOpen(newState);
if (newState && onOpen) onOpen();
if (!newState && onClose) onClose();
};
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
}, [isOpen]);
return (
<ModalContext.Provider value={{ isOpen, toggle }}>
<div onClick={toggle}>{trigger}</div>
{isOpen && (
<div className="modal-overlay" onClick={toggle}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
{children}
</div>
</div>
)}
</ModalContext.Provider>
);
};
Response:
## Component Analysis: Modal
### Issues Found:
| Issue | Severity | Resolution |
|-------|----------|------------|
| React Context (ModalContext) | Critical | Context won't work across Webflow components. Since this is internal to Modal, it's OK but exported useModal hook won't work. |
| document.body manipulation | Critical | SSR incompatible. Must set `ssr: false` or wrap in useEffect check. |
| onOpen/onClose callbacks | Warning | Function props not supported in Webflow. Must remove. |
| Global CSS classes | Warning | "modal-overlay", "modal-content" won't work in Shadow DOM. |
### Props Detected:
| Prop | TypeScript Type | Webflow Mapping | Status |
|------|-----------------|-----------------|--------|
| trigger | React.ReactNode | props.Slot() | OK |
| children | React.ReactNode | props.Slot() | OK |
| onOpen | function | REMOVE | Not supported |
| onClose | function | REMOVE | Not supported |
---
## Required Changes
### 1. Remove function props (onOpen, onClose)
These cannot be passed from Webflow Designer. Remove from interface and component.
### 2. Fix SSR issue with document.body
Either set `ssr: false` or add typeof check:
useEffect(() => {
if (typeof document === 'undefined') return;
// ... rest of effect
}, [isOpen]);
### 3. Convert to CSS Modules
Rename styles and update class references.
### 4. Remove exported useModal hook
This won't work for external components. Keep ModalContext internal only.
---
## Generated Files
### Modal.webflow.tsx
import { declareComponent } from "@webflow/react";
import { props } from "@webflow/data-types";
import { Modal } from "./Modal";
import "./Modal.module.css";
export default declareComponent(Modal, {
name: "Modal",
description: "Overlay modal dialog with trigger element",
group: "Interactive",
props: {
trigger: props.Slot({
name: "Trigger"
}),
children: props.Slot({
name: "Content"
})
},
options: {
applyTagSelectors: true,
ssr: false // Required due to document.body usage
}
});
### Updated Modal.tsx
import { useState, useEffect } from 'react';
import styles from "./Modal.module.css";
interface ModalProps {
trigger: React.ReactNode;
children: React.ReactNode;
// Removed: onOpen, onClose - not supported in Webflow
}
export const Modal: React.FC<ModalProps> = ({ trigger, children }) => {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
useEffect(() => {
if (typeof document === 'undefined') return;
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
return (
<>
<div onClick={toggle}>{trigger}</div>
{isOpen && (
<div className={styles["overlay"]} onClick={toggle}>
<div className={styles["content"]} onClick={e => e.stopPropagation()}>
{children}
</div>
</div>
)}
</>
);
};
### Setup Required
npm i --save-dev @webflow/webflow-cli @webflow/data-types @webflow/react
User prompt:
Convert to Webflow:
interface Product {
id: string;
name: string;
price: number;
image: string;
category: 'electronics' | 'clothing' | 'home';
}
interface ProductCardProps {
product: Product;
showBadge?: boolean;
}
export const ProductCard = ({ product, showBadge = true }: ProductCardProps) => (
<div>
<img src={product.image} alt={product.name} />
{showBadge && <span>{product.category}</span>}
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
);
Response:
## Component Analysis: ProductCard
### Complex Props Detected:
The `product` prop is a complex object that must be flattened for Webflow:
| Original | Flattened Props |
|----------|-----------------|
| product.id | (Not needed in UI - remove) |
| product.name | productName: props.Text() |
| product.price | productPrice: props.Number() |
| product.image | productImage: props.Image() |
| product.category | productCategory: props.Variant() |
| showBadge | showBadge: props.Boolean() |
---
## Generated Files
### ProductCard.webflow.tsx
import { declareComponent } from "@webflow/react";
import { props } from "@webflow/data-types";
import { ProductCard } from "./ProductCard";
import "./ProductCard.module.css";
export default declareComponent(ProductCard, {
name: "ProductCard",
description: "Product display card with image, name, price, and category",
group: "Display",
props: {
productName: props.Text({
name: "Product Name",
defaultValue: "Product Title"
}),
productPrice: props.Number({
name: "Price",
defaultValue: 29.99
}),
productImage: props.Image({
name: "Product Image"
}),
productCategory: props.Variant({
name: "Category",
options: ["electronics", "clothing", "home"],
defaultValue: "electronics"
}),
showBadge: props.Boolean({
name: "Show Category Badge",
defaultValue: true
})
},
options: {
applyTagSelectors: true,
ssr: true
}
});
### Updated ProductCard.tsx
import styles from "./ProductCard.module.css";
interface ProductCardProps {
productName: string;
productPrice: number;
productImage?: { src: string; alt?: string };
productCategory: 'electronics' | 'clothing' | 'home';
showBadge?: boolean;
}
export const ProductCard = ({
productName,
productPrice,
productImage,
productCategory,
showBadge = true
}: ProductCardProps) => (
<div className={styles["card"]}>
{productImage && (
<img src={productImage.src} alt={productImage.alt || productName} />
)}
{showBadge && <span className={styles["badge"]}>{productCategory}</span>}
<h3>{productName}</h3>
<p>${productPrice.toFixed(2)}</p>
</div>
);
### Migration Notes:
- The `product` object has been flattened into individual props
- Each prop is now directly editable in Webflow Designer
- `product.id` was removed as it's not displayed in the UI
- Image prop now uses Webflow's Image type (provides { src, alt })
User prompt:
Convert this styled-components React component to Webflow:
// Button.tsx
import styled from 'styled-components';
const StyledButton = styled.button<{ variant: 'primary' | 'secondary' }>`
padding: 12px 24px;
border-radius: 8px;
font-weight: 600;
background: ${props => props.variant === 'primary' ? '#3b82f6' : '#e5e7eb'};
color: ${props => props.variant === 'primary' ? 'white' : '#1f2937'};
&:hover {
opacity: 0.9;
}
`;
interface ButtonProps {
label: string;
variant?: 'primary' | 'secondary';
disabled?: boolean;
}
export const Button = ({ label, variant = 'primary', disabled = false }: ButtonProps) => (
<StyledButton variant={variant} disabled={disabled}>
{label}
</StyledButton>
);
Response:
## Component Analysis: Button
### Styling Approach Detected: styled-components
styled-components requires a Shadow DOM decorator to work in Webflow.
### Props Detected:
| Prop | TypeScript Type | Webflow Mapping |
|------|-----------------|-----------------|
| label | string | props.Text() |
| variant | 'primary' \| 'secondary' | props.Variant() |
| disabled | boolean | props.Boolean() |
### No blocking issues - ready to convert!
---
## Required Setup for styled-components
**1. Install dependencies:**
npm i @webflow/styled-components-utils styled-components
npm i -D @types/styled-components
**2. Create/update `src/globals.ts`:**
import { styledComponentsShadowDomDecorator } from "@webflow/styled-components-utils";
export const decorators = [styledComponentsShadowDomDecorator];
**3. Update `webflow.json`:**
{
"library": {
"name": "My Library",
"components": ["./src/**/*.webflow.@(js|jsx|mjs|ts|tsx)"],
"globals": "./src/globals.ts"
}
}
---
## Generated Files
### Button.webflow.tsx
import { declareComponent } from "@webflow/react";
import { props } from "@webflow/data-types";
import { Button } from "./Button";
// No CSS import needed - styled-components handles styles
export default declareComponent(Button, {
name: "Button",
description: "Styled button with primary and secondary variants",
group: "Interactive",
props: {
label: props.Text({
name: "Label",
defaultValue: "Click me"
}),
variant: props.Variant({
name: "Variant",
options: ["primary", "secondary"],
defaultValue: "primary"
}),
disabled: props.Boolean({
name: "Disabled",
defaultValue: false,
trueLabel: "Disabled",
falseLabel: "Enabled"
})
},
options: {
applyTagSelectors: true,
ssr: true
}
});
### No changes needed to Button.tsx
The component can remain as-is. The styled-components decorator in globals.ts will handle Shadow DOM style injection automatically.
### Setup Checklist
- [ ] Install @webflow/styled-components-utils
- [ ] Create globals.ts with decorator
- [ ] Update webflow.json to reference globals
- [ ] Deploy with `npx webflow library share`
Some components fundamentally don't fit the Webflow model:
Always provide sensible defaults:
Make prop names designer-friendly:
buttonText not txtimageSource not imgSrcauthorName, authorAvatar, authorBioSet ssr: false if component:
window, document, navigatorlocalStorage or sessionStorageTake webflow/webflow-code-component:convert-component from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npm, npx.
Without those the skill loads but fails at the first command.