Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.subframe.com/llms.txt

Use this file to discover all available pages before exploring further.

Subframe now syncs each component as its own directory instead of a single file. This page explains what changed, why, and how to migrate an existing project.

What changed

Previously, each component synced as a single flat file:
src/ui/components/
└─ Button.tsx
Now each component (and page layout) syncs as a directory:
src/ui/components/
└─ Button/
   ├─ Button.tsx   // generated by Subframe — overwritten on every sync
   └─ index.tsx    // yours to edit — wraps and re-exports Button.tsx
Button.tsx is the component Subframe generates, exactly as before. index.tsx is a thin wrapper that re-exports it:
index.tsx
"use client";
import { Button as ButtonComponent } from "./Button";

/**
 * Add wrapper components and business logic here.
 * If you modify this file, disable Subframe sync for it to prevent overwrites.
 * Learn more: https://docs.subframe.com/concepts/syncing-components#wrapping-components
 */

export const Button = ButtonComponent;
Your imports don’t change. @/ui/components/Button resolves to the directory’s index.tsx, so existing code keeps working.

Why

A per-component directory gives each component a home for everything that should live alongside it in code:
  • index.tsx — a natural, stable place for your own wrapping logic and wrapper components, kept separate from the source Subframe generates.
  • Button.md — component documentation describing what the component is and how it should be used.
  • In the future — generated .stories files for Storybook, test files, and more.
It also makes disabling sync granular. @subframe/sync-disable works per file, so you can freeze your index.tsx while Button.tsx keeps receiving Subframe’s visual updates — instead of having to freeze the whole component.

Adding business logic

index.tsx is where your code goes. To extend a component, edit its index.tsx and add the @subframe/sync-disable marker so the CLI won’t overwrite it on the next sync:
index.tsx
// @subframe/sync-disable
import { Button as ButtonComponent } from "./Button";

export function Button({ onSubmit, ...props }) {
  const [loading, setLoading] = useState(false);

  async function handleClick() {
    setLoading(true);
    await onSubmit();
    setLoading(false);
  }

  return <ButtonComponent {...props} loading={loading} onClick={handleClick} />;
}
Button.tsx has no marker, so it keeps syncing — design changes from Subframe still flow in, while your logic in index.tsx is preserved. Anything importing @/ui/components/Button gets your wrapped version automatically, with no import changes.

Migrating an existing project

The CLI migrates your project automatically as you sync. There are no breaking changes — imports are unchanged, so a component moving into a directory doesn’t affect anything that imports it. The one exception is sync-disabled components, which need a quick import review (covered below). Because nothing breaks, you can migrate incrementally — sync a few components at a time, or run a full sync to do everything at once. Flat and nested components coexist fine while you’re partway through.
The migration runs in recent versions of the CLI. The commands below use @subframe/cli@latest so you always get the newest — if you’ve pinned an older @subframe/cli, update it before migrating.
1

Sync your components

Run a full sync to migrate everything at once:
npx @subframe/cli@latest sync --all
Or sync specific components — npx @subframe/cli@latest sync Button Alert — to migrate just those. Either way, the CLI writes the new directory layout and removes the old flat files for the components it syncs.
2

Update monorepo exports (if applicable)

If you expose Subframe components from a shared package using the exports field in package.json, update the subpath patterns to point to the new directory layout:
package.json
{
  "exports": {
    "./components/*": "./ui/components/*/index.tsx",
    "./layouts/*": "./ui/layouts/*/index.tsx"
  }
}
The old "./ui/components/*.tsx" pattern no longer matches because the flat files have moved into per-component directories. See the monorepo guide for the full setup.
3

Review any sync-disabled files

If you’d added @subframe/sync-disable to a component file under the old layout, the CLI won’t delete it — instead it moves it into the new directory (e.g. components/Button.tsxcomponents/Button/Button.tsx) and prints a warning listing each moved file.Because the file moved one level deeper, two things need a quick check.Relative imports. Paths that were correct when the file was flat are now off by one level — siblings, shared root files, and cross-directory references all shift:
// components/Button/Button.tsx
import * as SubframeUtils from "../utils";    // ❌ correct when flat
import * as SubframeUtils from "../../utils"; // ✅ shared root files

import { Tooltip } from "./Tooltip";  // ❌
import { Tooltip } from "../Tooltip"; // ✅ sibling components

// layouts/DialogLayout/DialogLayout.tsx
import { Dialog } from "../components/Dialog";    // ❌
import { Dialog } from "../../components/Dialog"; // ✅
Exported prop types. The directory’s index.tsx re-exports the component (export const Button = ButtonComponent), so TypeScript has to be able to name the types in its signature. If a moved file declares its prop interfaces without export, you’ll get errors like Exported variable 'Button' has or is using name 'ButtonRootProps' … but cannot be named (TS4023). Add export to those interfaces:
interface ButtonRootProps extends ... {}        // ❌
export interface ButtonRootProps extends ... {} // ✅
Files the CLI regenerates already export their prop types, so this only affects files you’d frozen with @subframe/sync-disable.
4

Commit the change on its own

A full migration touches every component, so the diff is large but mechanical — committing it separately keeps it easy to review.
Last modified on May 27, 2026