agentleFS
dylantarredylantarre/design-system-skills/skills/tools/framer/SKILL.md

framer

Skill50 starsChanged 11 months agoMITView on GitHub

Integrates design tokens with Framer for prototyping and production sites. Use when adding CSS custom properties to Framer projects, creating code components, or building Framer sites with design systems.

The file

We split this skill into the rules it gives an agent

5 rules. About 0 are common, 5 are distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈5

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Import in Framer: Add to custom code or component

    line 31
  2. Distinctive:

    Publish and maintain: Keep tokens in sync

    line 34
  3. Distinctive:

    Centralize tokens: Import tokens once in site settings

    line 619
  4. Distinctive:

    Test dark mode: Ensure token switching works properly

    line 623
  5. Distinctive:

    Version control: Keep Framer components in sync with design system

    line 624

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Framer Token Integration

Overview

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.

When to Use

  • Setting up design tokens in Framer
  • Creating Framer code components with tokens
  • Building interactive prototypes with design system
  • Publishing Framer sites with consistent styling

Quick Reference: Integration Methods

MethodUse CaseComplexity
CSS VariablesGlobal token importSimple
Code ComponentsCustom React componentsMedium
OverridesDynamic stylingSimple
Framer LibraryShared component libraryAdvanced

The Process

  1. Export tokens as CSS: Generate CSS custom properties file
  2. Import in Framer: Add to custom code or component
  3. Create code components: Build with token references
  4. Set up overrides: Apply dynamic styles
  5. Publish and maintain: Keep tokens in sync

CSS Variables in Framer

Global CSS Import

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>

External CSS File

Link your token CSS file:

<link rel="stylesheet" href="https://your-cdn.com/tokens.css">

Code Components

Button Component

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 Component

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 Component

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 },
})

Overrides

Dynamic Styling with Tokens

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))",
        }}
      />
    )
  }
}

Theme Toggle Override

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} />
  }
}

Framer Library Setup

Shared Component Package

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"

Token Sync Workflow

Automated Token Updates

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")

Best Practices

  1. Use CSS variables: Framer supports CSS custom properties natively
  2. Centralize tokens: Import tokens once in site settings
  3. Create code components: Build reusable components with property controls
  4. Use consistent naming: Match Framer property names to token names
  5. Document for designers: Explain which tokens map to which controls
  6. Test dark mode: Ensure token switching works properly
  7. Version control: Keep Framer components in sync with design system

Common Patterns

PatternImplementation
Hover statesUse whileHover with token color values
Focus ringsApply boxShadow with token-based focus color
Responsive spacingUse token values in responsive breakpoints
Theme switchingToggle data-theme attribute on root
Loading statesUse token colors for spinner/skeleton

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.