Use when integrating design tokens with Framer, creating Framer overrides, or building interactive prototypes with design system tokens.
/plugin marketplace add dylantarre/design-system-skills/plugin install design-system-skills@design-system-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Integrate design tokens with Framer for consistent prototyping and production sites. Use CSS custom properties, Framer overrides, and code components to bring your design system into Framer.
| Method | Use Case | Complexity |
|---|---|---|
| CSS Variables | Global token import | Simple |
| Code Components | Custom React components | Medium |
| Overrides | Dynamic styling | Simple |
| Framer Library | Shared component library | Advanced |
In Framer Site Settings → Custom Code → Head:
<style>
:root {
/* Colors */
--color-primary-50: #eff6ff;
--color-primary-100: #dbeafe;
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-900: #1e3a8a;
--color-gray-50: #f9fafb;
--color-gray-100: #f3f4f6;
--color-gray-500: #6b7280;
--color-gray-900: #111827;
--color-success-500: #22c55e;
--color-warning-500: #f59e0b;
--color-error-500: #ef4444;
/* Spacing */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
/* Typography */
--text-xs: 12px;
--text-sm: 14px;
--text-base: 16px;
--text-lg: 18px;
--text-xl: 20px;
--text-2xl: 24px;
/* Border Radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 16px;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}
/* Dark mode */
[data-framer-theme="dark"] {
--color-background: var(--color-gray-900);
--color-foreground: var(--color-gray-50);
}
</style>
Link your token CSS file:
<link rel="stylesheet" href="https://your-cdn.com/tokens.css">
Button.tsx (Framer Code Component):
import { addPropertyControls, ControlType } from "framer"
import { motion } from "framer-motion"
interface ButtonProps {
text: string
variant: "primary" | "secondary" | "ghost" | "danger"
size: "sm" | "md" | "lg"
fullWidth: boolean
disabled: boolean
onClick?: () => void
}
export function Button({
text = "Button",
variant = "primary",
size = "md",
fullWidth = false,
disabled = false,
onClick,
}: ButtonProps) {
const sizeStyles = {
sm: {
height: 32,
padding: "0 var(--spacing-sm)",
fontSize: "var(--text-sm)",
},
md: {
height: 40,
padding: "0 var(--spacing-md)",
fontSize: "var(--text-base)",
},
lg: {
height: 48,
padding: "0 var(--spacing-lg)",
fontSize: "var(--text-lg)",
},
}
const variantStyles = {
primary: {
backgroundColor: "var(--color-primary-500)",
color: "white",
border: "none",
},
secondary: {
backgroundColor: "transparent",
color: "var(--color-gray-700)",
border: "1px solid var(--color-gray-300)",
},
ghost: {
backgroundColor: "transparent",
color: "var(--color-gray-700)",
border: "none",
},
danger: {
backgroundColor: "var(--color-error-500)",
color: "white",
border: "none",
},
}
const hoverStyles = {
primary: { backgroundColor: "var(--color-primary-600)" },
secondary: { backgroundColor: "var(--color-gray-50)" },
ghost: { backgroundColor: "var(--color-gray-100)" },
danger: { backgroundColor: "var(--color-error-600)" },
}
return (
<motion.button
style={{
...sizeStyles[size],
...variantStyles[variant],
width: fullWidth ? "100%" : "auto",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
gap: "var(--spacing-xs)",
borderRadius: "var(--radius-md)",
fontWeight: 500,
cursor: disabled ? "not-allowed" : "pointer",
opacity: disabled ? 0.5 : 1,
fontFamily: "inherit",
outline: "none",
}}
whileHover={disabled ? {} : hoverStyles[variant]}
whileTap={disabled ? {} : { scale: 0.98 }}
onClick={disabled ? undefined : onClick}
disabled={disabled}
>
{text}
</motion.button>
)
}
addPropertyControls(Button, {
text: {
type: ControlType.String,
title: "Text",
defaultValue: "Button",
},
variant: {
type: ControlType.Enum,
title: "Variant",
options: ["primary", "secondary", "ghost", "danger"],
optionTitles: ["Primary", "Secondary", "Ghost", "Danger"],
defaultValue: "primary",
},
size: {
type: ControlType.Enum,
title: "Size",
options: ["sm", "md", "lg"],
optionTitles: ["Small", "Medium", "Large"],
defaultValue: "md",
},
fullWidth: {
type: ControlType.Boolean,
title: "Full Width",
defaultValue: false,
},
disabled: {
type: ControlType.Boolean,
title: "Disabled",
defaultValue: false,
},
})
Card.tsx:
import { addPropertyControls, ControlType } from "framer"
import type { ReactNode } from "react"
interface CardProps {
children?: ReactNode
padding: "none" | "sm" | "md" | "lg"
elevation: "none" | "sm" | "md" | "lg"
radius: "none" | "sm" | "md" | "lg"
}
export function Card({
children,
padding = "md",
elevation = "sm",
radius = "md",
}: CardProps) {
const paddingMap = {
none: 0,
sm: "var(--spacing-sm)",
md: "var(--spacing-md)",
lg: "var(--spacing-lg)",
}
const shadowMap = {
none: "none",
sm: "var(--shadow-sm)",
md: "var(--shadow-md)",
lg: "var(--shadow-lg)",
}
const radiusMap = {
none: 0,
sm: "var(--radius-sm)",
md: "var(--radius-md)",
lg: "var(--radius-lg)",
}
return (
<div
style={{
padding: paddingMap[padding],
boxShadow: shadowMap[elevation],
borderRadius: radiusMap[radius],
backgroundColor: "white",
width: "100%",
height: "100%",
}}
>
{children}
</div>
)
}
addPropertyControls(Card, {
children: {
type: ControlType.ComponentInstance,
title: "Content",
},
padding: {
type: ControlType.Enum,
title: "Padding",
options: ["none", "sm", "md", "lg"],
optionTitles: ["None", "Small", "Medium", "Large"],
defaultValue: "md",
},
elevation: {
type: ControlType.Enum,
title: "Elevation",
options: ["none", "sm", "md", "lg"],
optionTitles: ["None", "Small", "Medium", "Large"],
defaultValue: "sm",
},
radius: {
type: ControlType.Enum,
title: "Radius",
options: ["none", "sm", "md", "lg"],
optionTitles: ["None", "Small", "Medium", "Large"],
defaultValue: "md",
},
})
Input.tsx:
import { addPropertyControls, ControlType } from "framer"
import { useState } from "react"
interface InputProps {
label: string
placeholder: string
helperText: string
error: string
disabled: boolean
required: boolean
type: "text" | "email" | "password" | "number"
size: "sm" | "md" | "lg"
}
export function Input({
label = "",
placeholder = "Enter text...",
helperText = "",
error = "",
disabled = false,
required = false,
type = "text",
size = "md",
}: InputProps) {
const [value, setValue] = useState("")
const [focused, setFocused] = useState(false)
const sizeStyles = {
sm: { height: 32, fontSize: "var(--text-sm)" },
md: { height: 40, fontSize: "var(--text-base)" },
lg: { height: 48, fontSize: "var(--text-lg)" },
}
const hasError = !!error
return (
<div style={{ display: "flex", flexDirection: "column", gap: "var(--spacing-xs)" }}>
{label && (
<label
style={{
fontSize: "var(--text-sm)",
fontWeight: 500,
color: "var(--color-gray-700)",
}}
>
{label}
{required && <span style={{ color: "var(--color-error-500)" }}> *</span>}
</label>
)}
<input
type={type}
value={value}
onChange={(e) => setValue(e.target.value)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
placeholder={placeholder}
disabled={disabled}
style={{
...sizeStyles[size],
padding: "0 var(--spacing-md)",
border: `1px solid ${
hasError
? "var(--color-error-500)"
: focused
? "var(--color-primary-500)"
: "var(--color-gray-300)"
}`,
borderRadius: "var(--radius-md)",
outline: "none",
boxShadow: focused
? `0 0 0 3px ${hasError ? "rgb(239 68 68 / 0.15)" : "rgb(59 130 246 / 0.15)"}`
: "none",
backgroundColor: disabled ? "var(--color-gray-100)" : "white",
color: "var(--color-gray-900)",
fontFamily: "inherit",
transition: "border-color 150ms, box-shadow 150ms",
}}
/>
{(error || helperText) && (
<span
style={{
fontSize: "var(--text-sm)",
color: hasError ? "var(--color-error-500)" : "var(--color-gray-500)",
}}
>
{error || helperText}
</span>
)}
</div>
)
}
addPropertyControls(Input, {
label: { type: ControlType.String, title: "Label", defaultValue: "Label" },
placeholder: { type: ControlType.String, title: "Placeholder", defaultValue: "Enter text..." },
helperText: { type: ControlType.String, title: "Helper Text" },
error: { type: ControlType.String, title: "Error" },
type: {
type: ControlType.Enum,
title: "Type",
options: ["text", "email", "password", "number"],
defaultValue: "text",
},
size: {
type: ControlType.Enum,
title: "Size",
options: ["sm", "md", "lg"],
optionTitles: ["Small", "Medium", "Large"],
defaultValue: "md",
},
disabled: { type: ControlType.Boolean, title: "Disabled", defaultValue: false },
required: { type: ControlType.Boolean, title: "Required", defaultValue: false },
})
tokenOverrides.ts:
import type { ComponentType } from "react"
// Apply primary button styling
export function withPrimaryButton(Component: ComponentType): ComponentType {
return (props: any) => {
return (
<Component
{...props}
style={{
...props.style,
backgroundColor: "var(--color-primary-500)",
color: "white",
borderRadius: "var(--radius-md)",
padding: "0 var(--spacing-md)",
height: 40,
fontWeight: 500,
}}
/>
)
}
}
// Apply card styling
export function withCard(Component: ComponentType): ComponentType {
return (props: any) => {
return (
<Component
{...props}
style={{
...props.style,
backgroundColor: "white",
borderRadius: "var(--radius-lg)",
boxShadow: "var(--shadow-md)",
padding: "var(--spacing-lg)",
}}
/>
)
}
}
// Responsive text sizing
export function withResponsiveText(Component: ComponentType): ComponentType {
return (props: any) => {
return (
<Component
{...props}
style={{
...props.style,
fontSize: "clamp(var(--text-base), 2.5vw, var(--text-xl))",
}}
/>
)
}
}
themeOverride.ts:
import { useState, useEffect } from "react"
import type { ComponentType } from "react"
export function withThemeToggle(Component: ComponentType): ComponentType {
return (props: any) => {
const [theme, setTheme] = useState<"light" | "dark">("light")
useEffect(() => {
document.documentElement.dataset.framerTheme = theme
}, [theme])
const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light")
}
return <Component {...props} onClick={toggleTheme} />
}
}
package.json:
{
"name": "@acme/framer-components",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"peerDependencies": {
"framer": "*",
"framer-motion": "*",
"react": "^18.0.0"
},
"scripts": {
"build": "tsc",
"publish:framer": "framer-cli publish"
}
}
index.ts:
export { Button } from "./components/Button"
export { Input } from "./components/Input"
export { Card } from "./components/Card"
export { Stack } from "./components/Stack"
// Re-export tokens as JS object for code components
export { tokens } from "./tokens"
sync-to-framer.ts:
import fs from "fs"
interface Token {
value: string
type: string
}
function tokensToCSS(tokens: Record<string, any>, prefix = ""): string {
let css = ""
for (const [key, value] of Object.entries(tokens)) {
const path = prefix ? `${prefix}-${key}` : key
if (typeof value === "object" && "value" in value) {
css += ` --${path}: ${value.value};\n`
} else if (typeof value === "object") {
css += tokensToCSS(value, path)
}
}
return css
}
function generateFramerCSS(tokensPath: string, outputPath: string): void {
const tokens = JSON.parse(fs.readFileSync(tokensPath, "utf-8"))
const css = `:root {\n${tokensToCSS(tokens)}}\n`
fs.writeFileSync(outputPath, css)
console.log(`Generated ${outputPath}`)
}
generateFramerCSS("./tokens/tokens.json", "./framer/tokens.css")
| Pattern | Implementation |
|---|---|
| Hover states | Use whileHover with token color values |
| Focus rings | Apply boxShadow with token-based focus color |
| Responsive spacing | Use token values in responsive breakpoints |
| Theme switching | Toggle data-theme attribute on root |
| Loading states | Use token colors for spinner/skeleton |