Skip to main content
Subframe is designed for building interfaces—the visual layer that designers own. Code generation in Subframe is deterministic and purely presentational (no API calls, state management, etc). Developers can later add application logic themselves using IDEs like Cursor or VS Code after export. This guide explains Subframe’s design-to-code workflow in detail.

Components vs pages

As a developer, you should treat components and pages differently:
  • Components are your design system (e.g. buttons, inputs, cards). They are synced via CLI to a folder in your codebase (default: ./ui/components) and are not meant to be modified after export.
  • Pages are screens built from components. They are exported as copyable React code or using the MCP server and are meant to be modified after export.
In a nutshell, components are synced, pages are exported. This is because pages are typically modified with business logic like API calls after export. If your component needs logic after syncing, see our guide on best practices for exporting components.

Design handoff

Suppose your designer creates a sign in page in Subframe:
Sign in page designed in Subframe
When designers use Subframe, they are modifying the underlying code, which uses components that eventually live in your codebase. In this sign in page, the designer used the Button and SocialSignInButton components:
Sign in page with components highlighted

Syncing components

When the designs are ready for handoff, sync the Button and SocialSignInButton code to your codebase by running the following command:
npx @subframe/cli@latest sync Button SocialSignInButton
You can also sync all design system components at once:
npx @subframe/cli@latest sync --all
The CLI sync command pulls Button, SocialSignInButton, and any other components into a specific folder in your codebase. By default this folder is located in the ./src/ui/components folder but can be configured in the project settings.
src/ui/
└─ components/
  ├─ Button.tsx
  └─ SocialSignInButton.tsx
import React from "react"
import * as SubframeCore from "@subframe/core"
import * as SubframeUtils from "../utils"

interface ButtonRootProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  disabled?: boolean
  variant?: "brand-primary" | "brand-secondary" | "destructive-primary"
  size?: "large" | "medium" | "small"
  children?: React.ReactNode
  icon?: SubframeCore.IconName
  iconRight?: SubframeCore.IconName
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void
  className?: string
}

const ButtonRoot = React.forwardRef<HTMLButtonElement, ButtonRootProps>(function ButtonRoot(
  {
    disabled = false,
    variant = "brand-primary",
    size = "medium",
    children,
    icon = null,
    iconRight = null,
    className,
    type = "button",
    ...otherProps
  }: ButtonRootProps,
  ref,
) {
  return (
    <button
      className={SubframeUtils.twClassNames(
        "group/3b777358 flex h-8 cursor-pointer items-center justify-center gap-2 rounded-md border-none bg-brand-600 px-3 text-left hover:bg-brand-500 active:bg-brand-600 disabled:cursor-default disabled:bg-neutral-200 hover:disabled:cursor-default hover:disabled:bg-neutral-200 active:disabled:cursor-default active:disabled:bg-neutral-200",
        {
          "h-6 w-auto flex-row flex-nowrap gap-1 px-2 py-0": size === "small",
          "h-10 w-auto px-4 py-0": size === "large",
          "bg-error-600 hover:bg-error-500 active:bg-error-600": variant === "destructive-primary",
          "bg-brand-50 hover:bg-brand-100 active:bg-brand-50": variant === "brand-secondary",
        },
        className,
      )}
      ref={ref}
      type={type}
      disabled={disabled}
      {...otherProps}
    >
      <SubframeCore.Icon
        className={SubframeUtils.twClassNames(
          "text-body font-body text-white group-disabled/3b777358:text-neutral-400",
          {
            "text-heading-3 font-heading-3": size === "large",
            "text-brand-700": variant === "brand-secondary",
          },
        )}
        name={icon}
      />
      <div
        className={SubframeUtils.twClassNames("hidden h-4 w-4 flex-none items-center justify-center gap-2", {
          "h-3 w-3 flex-none": size === "small",
        })}
      >
        <SubframeCore.Loader
          className={SubframeUtils.twClassNames(
            "font-['Inter'] text-[12px] font-[400] leading-[20px] text-white group-disabled/3b777358:text-neutral-400",
            {
              "text-caption font-caption": size === "small",
              "text-brand-700": variant === "brand-secondary",
            },
          )}
        />
      </div>
      {children ? (
        <span
          className={SubframeUtils.twClassNames(
            "whitespace-nowrap text-body-bold font-body-bold text-white group-disabled/3b777358:text-neutral-400",
            {
              "text-caption-bold font-caption-bold": size === "small",
              "text-brand-700": variant === "brand-secondary",
            },
          )}
        >
          {children}
        </span>
      ) : null}
      <SubframeCore.Icon
        className={SubframeUtils.twClassNames(
          "text-body font-body text-white group-disabled/3b777358:text-neutral-400",
          {
            "text-heading-3 font-heading-3": size === "large",
            "text-brand-700": variant === "brand-secondary",
          },
        )}
        name={iconRight}
      />
    </button>
  )
})

export const Button = ButtonRoot
For more information on syncing components, see the Syncing components guide.

Exporting pages

Subframe generates page code with stubs for business logic that need to be filled in:
SignInPage.tsx
import React from "react"
import { Button } from "@/ui/components/Button"
import { SocialSignInButton } from "@/ui/components/SocialSignInButton"

function SignInPage() {
  return (
    <div className="flex h-full w-full flex-col items-center justify-center bg-white">
      <div className="flex w-64 flex-col items-center justify-center gap-8">
        <div className="flex flex-col items-center justify-center gap-1">
          <img
            className="h-16 flex-none object-cover"
            src="https://res.cloudinary.com/subframe/image/upload/v1767591134/uploads/302/zzfsz5tcwky0pkjkru9d.png"
          />
          <span className="text-heading-2 font-heading-2 text-default-font">Welcome</span>
          <span className="text-body font-body text-subtext-color">Login or sign up below</span>
        </div>
        <div className="flex w-full flex-col items-center gap-2">
          <SocialSignInButton
            variant="google"
            onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
              // TODO: Implement Google sign in
            }}
          />
          <SocialSignInButton
            variant="apple"
            onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
              // TODO: Implement Apple sign in
            }}
          />
        </div>
        <div className="flex h-px w-full flex-none flex-col items-center gap-2 bg-neutral-border" />
        <Button
          className="h-10 w-full flex-none"
          variant="brand-secondary"
          size="large"
          icon="FeatherMail"
          onClick={(event: React.MouseEvent<HTMLButtonElement>) => {
            // TODO: Implement email sign in
          }}
        >
          Continue with email
        </Button>
      </div>
    </div>
  )
}

export default SignInPage
The page code can be exported in two ways:
  • MCP server (recommended) - install Subframe MCP server and ask your AI tool to integrate the page code directly using the page link.
  • Copy/paste - Open Code > Inspect in Subframe and copy the React code.
We recommend using the MCP server because you can also ask AI to add business logic or update the page code based on code changes. Once exported, refactor the code or add any business logic as needed.
SignInPage.tsx
import React from "react"
import { Button } from "@/ui/components/Button"
import { SocialSignInButton } from "@/ui/components/SocialSignInButton"
import { useNavigate } from "react-router-dom"
import { signInWithGoogle, signInWithApple } from "@/lib/auth"

function SignInPage() {
  const navigate = useNavigate()

  return (
    <div className="flex h-full w-full flex-col items-center justify-center bg-white">
      <div className="flex w-64 flex-col items-center justify-center gap-8">
        <div className="flex flex-col items-center justify-center gap-1">
          <img
            className="h-16 flex-none object-cover"
            src="https://res.cloudinary.com/subframe/image/upload/v1767591134/uploads/302/zzfsz5tcwky0pkjkru9d.png"
          />
          <span className="text-heading-2 font-heading-2 text-default-font">Welcome</span>
          <span className="text-body font-body text-subtext-color">Login or sign up below</span>
        </div>
        <div className="flex w-full flex-col items-center gap-2">
          <SocialSignInButton
            variant="google"
            onClick={async () => {
              await signInWithGoogle()
              navigate("/dashboard")
            }}
          />
          <SocialSignInButton
            variant="apple"
            onClick={async () => {
              await signInWithApple()
              navigate("/dashboard")
            }}
          />
        </div>
        <div className="flex h-px w-full flex-none flex-col items-center gap-2 bg-neutral-border" />
        <Button
          className="h-10 w-full flex-none"
          variant="brand-secondary"
          size="large"
          icon="FeatherMail"
          onClick={() => {
            navigate("/sign-in/email")
          }}
        >
          Continue with email
        </Button>
      </div>
    </div>
  )
}

export default SignInPage

Iterating on designs

After the initial export, both designs and code will evolve. Here’s how to handle changes:

Designer makes changes

Subframe lets designers own the visual layer. When a change is made in Subframe, you can re-export the diff into your codebase. Component updates Run the sync command to get the latest component code:
npx @subframe/cli@latest sync --all
If a component has breaking changes, TypeScript will throw errors wherever that component is used. This makes it easy for developers to find and update the affected code. Page updates The recommended way is to prompt your AI tool using the MCP server to update the page code:
Update the existing page to match the Subframe design at
https://app.subframe.com/<YOUR_PROJECT_ID>/design/<DESIGN_ID>/edit.

Preserve all existing functionality unless the new design requires a change.
AI will fetch the latest design and merge it with your existing code, preserving your business logic.

Developer makes changes

Often times, you will need to add or modify the component behavior after handoff. The best practice is to create a wrapper component that adds the necessary logic. In that case, you probably don’t need to sync code back to Subframe, since the visual layer managed by Subframe will be untouched. If creating a wrapper component is not possible, you can disable sync for the component. In the near future, we will add CLI commands to sync component code back to Subframe. If you are interested in this feature, please let us know by joining our Slack community. To import an existing page code back to Subframe, you can take a screenshot of the page and ask Subframe AI to recreate the design in Subframe.

Beyond handoff

Once a feature ships, page designs may drift from code. That’s okay — page designs are artifacts for communication. Once the feature is live, the code is the source of truth. Components are different. They stay synced, so Subframe remains the single source of truth for your design system. When a designer updates a button or input, you re-run sync and every page using that component gets the update.