Skip to main content

Builder.io vs Plasmic vs Makeswift: Visual Page Builders (2026)

·PkgPulse Team

TL;DR

Builder.io is the visual headless CMS — drag-and-drop page builder, React/Next.js SDK, A/B testing, targeting, structured data, works with any framework. Plasmic is the visual builder for React — drag-and-drop component builder, generates clean React code, Figma import, slots/variants, integrates with existing codebases. Makeswift is the visual editor for Next.js — inline page editing, component registration, live preview, built specifically for Next.js sites. In 2026: Builder.io for visual CMS with A/B testing, Plasmic for code-generating visual design, Makeswift for Next.js inline editing.

Key Takeaways

  • Builder.io: @builder.io/sdk-react ~40K weekly downloads — visual CMS, A/B testing
  • Plasmic: @plasmicapp/loader-react ~10K weekly downloads — visual builder, code gen
  • Makeswift: @makeswift/runtime ~5K weekly downloads — Next.js visual editor
  • Builder.io has the most CMS features (targeting, scheduling, analytics)
  • Plasmic generates the cleanest React component code
  • Makeswift provides the tightest Next.js integration with inline editing

Builder.io

Builder.io — visual headless CMS:

Setup with Next.js

npm install @builder.io/sdk-react
// app/[[...page]]/page.tsx — catch-all route for Builder pages
import { builder } from "@builder.io/sdk"
import { RenderBuilderContent } from "@/components/builder"

builder.init(process.env.NEXT_PUBLIC_BUILDER_API_KEY!)

export default async function Page({ params }: { params: { page?: string[] } }) {
  const urlPath = "/" + (params.page?.join("/") || "")

  const content = await builder
    .get("page", {
      userAttributes: { urlPath },
      prerender: false,
    })
    .toPromise()

  if (!content) {
    return notFound()
  }

  return <RenderBuilderContent content={content} model="page" />
}

// Generate static paths:
export async function generateStaticParams() {
  const pages = await builder.getAll("page", {
    fields: "data.url",
    options: { noTargeting: true },
  })

  return pages
    .map((page) => ({
      page: page.data?.url?.split("/").filter(Boolean) || [],
    }))
    .filter((p) => p.page.length > 0)
}

Custom components

// components/builder.tsx
"use client"
import { BuilderComponent, useIsPreviewing } from "@builder.io/react"
import { builder, Builder } from "@builder.io/sdk"

builder.init(process.env.NEXT_PUBLIC_BUILDER_API_KEY!)

// Register custom components for the visual editor:
Builder.registerComponent(
  ({ name, downloads, version, tags }) => (
    <div className="rounded-xl border p-6 shadow-sm">
      <h3 className="text-xl font-bold">{name}</h3>
      <p className="text-3xl font-black text-blue-500 mt-2">
        {downloads?.toLocaleString()} <span className="text-sm text-gray-400">/week</span>
      </p>
      <div className="mt-3 flex gap-2">
        <span className="rounded bg-green-100 px-2 py-1 text-xs text-green-700">
          v{version}
        </span>
        {tags?.map((tag: string) => (
          <span key={tag} className="rounded bg-gray-100 px-2 py-1 text-xs text-gray-600">
            {tag}
          </span>
        ))}
      </div>
    </div>
  ),
  {
    name: "PackageCard",
    inputs: [
      { name: "name", type: "string", defaultValue: "react" },
      { name: "downloads", type: "number", defaultValue: 25000000 },
      { name: "version", type: "string", defaultValue: "19.0.0" },
      { name: "tags", type: "list", subFields: [{ name: "tag", type: "string" }] },
    ],
  }
)

Builder.registerComponent(
  ({ packages, title }) => (
    <section className="py-12">
      <h2 className="text-3xl font-bold text-center mb-8">{title}</h2>
      <div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-6xl mx-auto">
        {packages?.map((pkg: any) => (
          <div key={pkg.name} className="rounded-xl border p-6">
            <h3 className="font-bold text-lg">{pkg.name}</h3>
            <p className="text-2xl font-black text-blue-500">
              {pkg.downloads?.toLocaleString()}
            </p>
          </div>
        ))}
      </div>
    </section>
  ),
  {
    name: "PackageGrid",
    inputs: [
      { name: "title", type: "string", defaultValue: "Popular Packages" },
      {
        name: "packages",
        type: "list",
        subFields: [
          { name: "name", type: "string" },
          { name: "downloads", type: "number" },
        ],
      },
    ],
  }
)

export function RenderBuilderContent({ content, model }: any) {
  const isPreviewing = useIsPreviewing()

  if (!content && !isPreviewing) return null

  return <BuilderComponent content={content} model={model} />
}

Structured data and targeting

// Fetch structured data (not pages):
import { builder } from "@builder.io/sdk"

builder.init(process.env.NEXT_PUBLIC_BUILDER_API_KEY!)

// Structured data model:
const featuredPackages = await builder.getAll("featured-packages", {
  limit: 10,
  options: { noTargeting: true },
})

// With targeting:
const heroContent = await builder
  .get("hero-section", {
    userAttributes: {
      urlPath: "/",
      device: "mobile",
      country: "US",
    },
  })
  .toPromise()

// A/B testing — Builder.io handles variants automatically:
// Create variants in the visual editor
// SDK automatically serves the right variant
// Track conversions:
builder.trackConversion(revenue)

// Content scheduling:
// Set publish/unpublish dates in the visual editor
// Content auto-publishes and auto-unpublishes

// Webhooks:
// Builder.io sends webhooks on content publish
// POST /api/webhooks/builder
export async function POST(req: Request) {
  const body = await req.json()

  if (body.operation === "publish") {
    // Revalidate the page:
    await revalidatePath(body.data.url)
  }

  return Response.json({ success: true })
}

Plasmic

Plasmic — visual builder for React:

Setup with Next.js

npm install @plasmicapp/loader-nextjs
// plasmic-init.ts
import { initPlasmicLoader } from "@plasmicapp/loader-nextjs"

export const PLASMIC = initPlasmicLoader({
  projects: [
    {
      id: process.env.PLASMIC_PROJECT_ID!,
      token: process.env.PLASMIC_API_TOKEN!,
    },
  ],
  preview: process.env.NODE_ENV === "development",
})
// app/[[...catchall]]/page.tsx
import { PlasmicComponent, ComponentRenderData, extractPlasmicQueryData } from "@plasmicapp/loader-nextjs"
import { PLASMIC } from "@/plasmic-init"

export default async function Page({ params }: { params: { catchall?: string[] } }) {
  const path = "/" + (params.catchall?.join("/") || "")

  const plasmicData = await PLASMIC.maybeFetchComponentData(path)
  if (!plasmicData) return notFound()

  const queryData = await extractPlasmicQueryData(
    <PlasmicComponent component={plasmicData.entryCompMetas[0].displayName} />
  )

  return (
    <PlasmicComponent
      component={plasmicData.entryCompMetas[0].displayName}
      prefetchedData={plasmicData}
      prefetchedQueryData={queryData}
    />
  )
}

export async function generateStaticParams() {
  const pages = await PLASMIC.fetchPages()
  return pages.map((page) => ({
    catchall: page.path.split("/").filter(Boolean),
  }))
}

Code components (register React components)

// plasmic-init.ts — register code components
import { PLASMIC } from "./plasmic-init"

// Register a code component that Plasmic users can drag in:
PLASMIC.registerComponent(
  ({ name, downloads, version, tags, className }) => (
    <div className={`rounded-xl border p-6 shadow-sm ${className}`}>
      <h3 className="text-xl font-bold">{name}</h3>
      <p className="text-3xl font-black text-blue-500 mt-2">
        {downloads?.toLocaleString()}
        <span className="text-sm text-gray-400 ml-1">/week</span>
      </p>
      <div className="mt-3 flex gap-2 flex-wrap">
        <span className="rounded bg-green-100 px-2 py-1 text-xs text-green-700">
          v{version}
        </span>
        {tags?.map((tag: string) => (
          <span key={tag} className="rounded bg-gray-100 px-2 py-1 text-xs">
            {tag}
          </span>
        ))}
      </div>
    </div>
  ),
  {
    name: "PackageCard",
    props: {
      name: { type: "string", defaultValue: "react" },
      downloads: { type: "number", defaultValue: 25000000 },
      version: { type: "string", defaultValue: "19.0.0" },
      tags: { type: "array", itemType: { type: "string" } },
    },
  }
)

// Component with slots (children):
PLASMIC.registerComponent(
  ({ title, children, columns, className }) => (
    <section className={`py-12 ${className}`}>
      <h2 className="text-3xl font-bold text-center mb-8">{title}</h2>
      <div className={`grid grid-cols-1 md:grid-cols-${columns} gap-6 max-w-6xl mx-auto`}>
        {children}
      </div>
    </section>
  ),
  {
    name: "PackageGrid",
    props: {
      title: { type: "string", defaultValue: "Packages" },
      columns: { type: "choice", options: ["2", "3", "4"], defaultValue: "3" },
      children: { type: "slot" },
    },
  }
)

// Data-fetching component:
PLASMIC.registerComponent(
  ({ children, endpoint }) => {
    const [data, setData] = useState(null)

    useEffect(() => {
      fetch(endpoint).then((r) => r.json()).then(setData)
    }, [endpoint])

    if (!data) return <div>Loading...</div>

    return <DataProvider name="apiData" data={data}>{children}</DataProvider>
  },
  {
    name: "DataFetcher",
    props: {
      endpoint: { type: "string" },
      children: { type: "slot" },
    },
    providesData: true,
  }
)

Figma integration

// Plasmic Figma import workflow:
// 1. Design in Figma
// 2. Use Plasmic Figma plugin to import
// 3. Components become Plasmic components
// 4. Bind data, add interactions in Plasmic
// 5. Export as clean React code or use loader

// Generated component (when using codegen mode):
// components/plasmic/PackageCard.tsx — auto-generated
import * as React from "react"
import { PlasmicPackageCard } from "./plasmic/PlasmicPackageCard"

interface PackageCardProps {
  name?: string
  downloads?: number
  version?: string
  className?: string
}

function PackageCard(props: PackageCardProps) {
  return <PlasmicPackageCard {...props} />
}

export default PackageCard

Makeswift

Makeswift — visual editor for Next.js:

Setup

npm install @makeswift/runtime
// makeswift/client.ts
import { Makeswift } from "@makeswift/runtime/next"

export const client = new Makeswift(process.env.MAKESWIFT_SITE_API_KEY!)
// app/[[...path]]/page.tsx
import { client } from "@/makeswift/client"
import { MakeswiftPage } from "@makeswift/runtime/next"

export default async function Page({ params }: { params: { path?: string[] } }) {
  const path = "/" + (params.path?.join("/") || "")
  const snapshot = await client.getPageSnapshot(path)

  if (!snapshot) return notFound()

  return <MakeswiftPage snapshot={snapshot} />
}

export async function generateStaticParams() {
  const pages = await client.getPages()
  return pages.map((page) => ({
    path: page.path.split("/").filter(Boolean),
  }))
}

Register components

// makeswift/components.tsx
import {
  Style,
  TextInput,
  Number,
  List,
  Shape,
  Select,
  Slot,
} from "@makeswift/runtime/controls"
import { ReactRuntime } from "@makeswift/runtime/react"

// Register PackageCard:
function PackageCard({
  name,
  downloads,
  version,
  tags,
  className,
}: {
  name: string
  downloads: number
  version: string
  tags: Array<{ tag: string }>
  className?: string
}) {
  return (
    <div className={`rounded-xl border p-6 shadow-sm ${className}`}>
      <h3 className="text-xl font-bold">{name}</h3>
      <p className="text-3xl font-black text-blue-500 mt-2">
        {downloads.toLocaleString()}
        <span className="text-sm text-gray-400 ml-1">/week</span>
      </p>
      <div className="mt-3 flex gap-2 flex-wrap">
        <span className="rounded bg-green-100 px-2 py-1 text-xs text-green-700">
          v{version}
        </span>
        {tags?.map((t, i) => (
          <span key={i} className="rounded bg-gray-100 px-2 py-1 text-xs">
            {t.tag}
          </span>
        ))}
      </div>
    </div>
  )
}

ReactRuntime.registerComponent(PackageCard, {
  type: "package-card",
  label: "Package Card",
  props: {
    className: Style(),
    name: TextInput({ label: "Package name", defaultValue: "react" }),
    downloads: Number({ label: "Downloads", defaultValue: 25000000 }),
    version: TextInput({ label: "Version", defaultValue: "19.0.0" }),
    tags: List({
      label: "Tags",
      type: Shape({
        type: { tag: TextInput({ label: "Tag" }) },
      }),
      getItemLabel: (item) => item?.tag || "Tag",
    }),
  },
})

// Register grid layout:
function PackageGrid({
  title,
  columns,
  children,
  className,
}: {
  title: string
  columns: string
  children: React.ReactNode
  className?: string
}) {
  return (
    <section className={`py-12 ${className}`}>
      <h2 className="text-3xl font-bold text-center mb-8">{title}</h2>
      <div className={`grid grid-cols-1 md:grid-cols-${columns} gap-6 max-w-6xl mx-auto px-4`}>
        {children}
      </div>
    </section>
  )
}

ReactRuntime.registerComponent(PackageGrid, {
  type: "package-grid",
  label: "Package Grid",
  props: {
    className: Style(),
    title: TextInput({ label: "Title", defaultValue: "Popular Packages" }),
    columns: Select({
      label: "Columns",
      options: [
        { label: "2 Columns", value: "2" },
        { label: "3 Columns", value: "3" },
        { label: "4 Columns", value: "4" },
      ],
      defaultValue: "3",
    }),
    children: Slot(),
  },
})

Live editing and data

// Dynamic data in Makeswift components:
import { ReactRuntime } from "@makeswift/runtime/react"
import { TextInput, Style } from "@makeswift/runtime/controls"

function LivePackageStats({ packageName, className }: { packageName: string; className?: string }) {
  const [stats, setStats] = useState<any>(null)

  useEffect(() => {
    fetch(`/api/packages/${packageName}`)
      .then((r) => r.json())
      .then(setStats)
  }, [packageName])

  if (!stats) return <div className={className}>Loading...</div>

  return (
    <div className={`rounded-xl bg-slate-900 p-6 text-white ${className}`}>
      <h3 className="text-2xl font-bold">{stats.name}</h3>
      <p className="text-gray-400 mt-1">{stats.description}</p>
      <div className="mt-4 grid grid-cols-3 gap-4">
        <div>
          <div className="text-sm text-gray-500">Downloads</div>
          <div className="text-xl font-bold text-blue-400">
            {stats.downloads.toLocaleString()}
          </div>
        </div>
        <div>
          <div className="text-sm text-gray-500">Version</div>
          <div className="text-xl font-bold text-green-400">v{stats.version}</div>
        </div>
        <div>
          <div className="text-sm text-gray-500">License</div>
          <div className="text-xl font-bold">{stats.license}</div>
        </div>
      </div>
    </div>
  )
}

ReactRuntime.registerComponent(LivePackageStats, {
  type: "live-package-stats",
  label: "Live Package Stats",
  props: {
    className: Style(),
    packageName: TextInput({ label: "Package name", defaultValue: "react" }),
  },
})

Feature Comparison

FeatureBuilder.ioPlasmicMakeswift
TypeVisual CMSVisual builderVisual editor
Framework supportReact, Vue, Svelte, etc.ReactNext.js only
Drag-and-drop✅ (inline)
Code generation❌ (SDK rendering)✅ (codegen mode)❌ (SDK rendering)
Custom components✅ (code components)
Figma import✅ (native)
A/B testing✅ (built-in)
Targeting/personalization
Structured data
Scheduling
Localization
Slots/children
Data fetchingVia integrations✅ (data providers)Via components
Self-hosted
Free tier✅ (generous)
Open sourceSDK onlySDK onlySDK only

When to Use Each

Use Builder.io if:

  • Need a visual CMS with A/B testing, targeting, and scheduling
  • Want framework-agnostic visual editing (React, Vue, Svelte)
  • Building marketing pages where non-developers need to edit content
  • Need structured data models alongside visual page building

Use Plasmic if:

  • Want to generate clean React code from visual designs
  • Need Figma-to-React import workflow
  • Prefer visual design that outputs real, maintainable components
  • Building a design system where designers create production components

Use Makeswift if:

  • Building exclusively with Next.js and want the tightest integration
  • Need inline visual editing with live preview
  • Prefer a focused, Next.js-specific editing experience
  • Want the simplest setup for adding visual editing to a Next.js site

Methodology

Download data from npm registry (weekly average, March 2026). Feature comparison based on @builder.io/sdk-react v3.x, @plasmicapp/loader-nextjs v1.x, and @makeswift/runtime v0.x.

Compare visual builders and developer CMS tools on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.