Skip to main content

Best Boilerplates for Building No-Code/Low-Code Platforms 2026

·StarterPick Team
no-codelow-codedrag-and-dropreact-flowcraft-js2026

TL;DR

Building a no-code/low-code platform is a complex UI engineering challenge — the right choice depends on what type of builder you're making. For workflow/logic builders: React Flow is the standard (150K+ downloads/week, used by Retool, Linear). For drag-and-drop UI builders: Craft.js (page builders) or GrapesJS (website builders). For form builders: React JSON Schema Form or Formik-based custom builders. There's no all-in-one boilerplate — combine these primitives.

Key Takeaways

  • React Flow: Node-based workflow/graph editors (like n8n, Retool logic flow)
  • Craft.js: React drag-and-drop page builder (like landing page or email builders)
  • GrapesJS: Full website/email builder with HTML output (non-React)
  • JSONForms: Form builder from JSON Schema (good for admin panels)
  • No single boilerplate: Pick the right primitive library for your type of builder
  • 2026 trend: AI-powered no-code (natural language → workflow) emerging

Builder Types and Their Libraries

Type               → Library           → Use Case
─────────────────────────────────────────────────
Workflow/Logic     → React Flow        → n8n-like, pipeline builders
Page/UI builder    → Craft.js          → Landing page, email template
Website builder    → GrapesJS          → Full website editor
Form builder       → JSON Forms        → Dynamic form generation
Dashboard builder  → GridStack.js      → Widget/dashboard layout
Query builder      → React QueryBuilder → Filter/search UI builders

React Flow: Workflow Builders

npm install @xyflow/react
// Minimal workflow builder:
'use client';
import { ReactFlow, Background, Controls, MiniMap,
  addEdge, useNodesState, useEdgesState } from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { useCallback } from 'react';

const initialNodes = [
  { id: '1', type: 'input', data: { label: 'Start' }, position: { x: 100, y: 100 } },
  { id: '2', data: { label: 'Process' }, position: { x: 300, y: 100 } },
  { id: '3', type: 'output', data: { label: 'End' }, position: { x: 500, y: 100 } },
];

const initialEdges = [
  { id: 'e1-2', source: '1', target: '2', animated: true },
  { id: 'e2-3', source: '2', target: '3' },
];

export function WorkflowBuilder() {
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
  
  const onConnect = useCallback(
    (params) => setEdges((eds) => addEdge(params, eds)),
    []
  );
  
  return (
    <div style={{ width: '100%', height: '600px' }}>
      <ReactFlow
        nodes={nodes}
        edges={edges}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        onConnect={onConnect}
        fitView
      >
        <Background />
        <Controls />
        <MiniMap />
      </ReactFlow>
    </div>
  );
}
// Custom node types — the key to a real builder:
import { Handle, Position, type NodeProps } from '@xyflow/react';

interface EmailNodeData {
  label: string;
  to: string;
  subject: string;
  onUpdate: (data: Partial<EmailNodeData>) => void;
}

export function EmailNode({ data, selected }: NodeProps<EmailNodeData>) {
  return (
    <div className={`bg-white border-2 rounded-lg p-4 min-w-[200px] ${
      selected ? 'border-blue-500' : 'border-gray-200'
    }`}>
      <Handle type="target" position={Position.Left} />
      
      <div className="flex items-center gap-2 mb-3">
        <span>📧</span>
        <span className="font-semibold">{data.label}</span>
      </div>
      
      <input
        className="w-full border rounded px-2 py-1 text-sm mb-2"
        placeholder="To: {{user.email}}"
        value={data.to}
        onChange={(e) => data.onUpdate({ to: e.target.value })}
      />
      <input
        className="w-full border rounded px-2 py-1 text-sm"
        placeholder="Subject"
        value={data.subject}
        onChange={(e) => data.onUpdate({ subject: e.target.value })}
      />
      
      <Handle type="source" position={Position.Right} />
    </div>
  );
}
// Node palette — drag from sidebar to canvas:
const nodeTypes = {
  email: EmailNode,
  sms: SmsNode,
  delay: DelayNode,
  condition: ConditionNode,
  webhook: WebhookNode,
};

// Full workflow builder with palette:
export function WorkflowBuilderFull() {
  const [nodes, setNodes, onNodesChange] = useNodesState([]);
  const [edges, setEdges, onEdgesChange] = useEdgesState([]);
  
  const onDrop = useCallback((event: DragEvent) => {
    const type = event.dataTransfer.getData('application/reactflow');
    const position = { x: event.clientX, y: event.clientY };
    const newNode = { id: nanoid(), type, data: { label: type }, position };
    setNodes((nds) => [...nds, newNode]);
  }, []);
  
  return (
    <div className="flex h-screen">
      {/* Sidebar palette */}
      <NodePalette />
      
      {/* Canvas */}
      <div className="flex-1" onDrop={onDrop} onDragOver={(e) => e.preventDefault()}>
        <ReactFlow nodes={nodes} edges={edges}
          onNodesChange={onNodesChange} onEdgesChange={onEdgesChange}
          nodeTypes={nodeTypes}>
          <Background />
          <Controls />
        </ReactFlow>
      </div>
    </div>
  );
}

Craft.js: Page Builder

npm install @craftjs/core
// Craft.js page builder — the core pattern:
import { Editor, Frame, Element, useNode, useEditor } from '@craftjs/core';

// Draggable, configurable text component:
const Text = ({ text, fontSize, color }: any) => {
  const { connectors: { connect, drag }, selected } = useNode((state) => ({
    selected: state.events.selected,
  }));
  
  return (
    <p
      ref={(ref) => connect(drag(ref!))}
      style={{ fontSize, color, cursor: 'move', outline: selected ? '2px solid blue' : 'none' }}
    >
      {text}
    </p>
  );
};

// Define the settings panel for this component:
Text.craft = {
  displayName: 'Text',
  props: { text: 'Hello World', fontSize: 16, color: '#000000' },
  related: {
    toolbar: () => (
      <div>
        <label>Text</label>
        <input type="text" />  {/* Connected to component props */}
        <label>Font Size</label>
        <input type="number" />
      </div>
    ),
  },
};

// The builder UI:
export function PageBuilder() {
  return (
    <Editor resolver={{ Text, Container, Image }}>
      <div className="flex h-screen">
        {/* Component palette */}
        <Sidebar />
        
        {/* Canvas */}
        <div className="flex-1 bg-gray-100 p-8">
          <Frame>
            <Element is="div" canvas>
              <Text text="Click to edit" fontSize={16} color="#333" />
            </Element>
          </Frame>
        </div>
        
        {/* Properties panel */}
        <SettingsPanel />
      </div>
    </Editor>
  );
}

JSON Schema Form Builder

npm install @rjsf/core @rjsf/utils @rjsf/validator-ajv8
// Dynamic form from JSON Schema:
import Form from '@rjsf/core';
import validator from '@rjsf/validator-ajv8';

const schema = {
  title: 'Contact Form',
  type: 'object',
  required: ['firstName', 'email'],
  properties: {
    firstName: {
      type: 'string',
      title: 'First Name',
    },
    email: {
      type: 'string',
      title: 'Email',
      format: 'email',
    },
    plan: {
      type: 'string',
      title: 'Plan',
      enum: ['free', 'pro', 'enterprise'],
      enumNames: ['Free', 'Pro', 'Enterprise'],
    },
  },
};

const uiSchema = {
  plan: { 'ui:widget': 'radio' },
  email: { 'ui:placeholder': 'your@email.com' },
};

export function DynamicForm({ schema, onSubmit }: any) {
  return (
    <Form
      schema={schema}
      uiSchema={uiSchema}
      validator={validator}
      onSubmit={({ formData }) => onSubmit(formData)}
    />
  );
}

Architecture: Saving Builder State

// Storing no-code builder state in DB:
// All builders (React Flow, Craft.js, etc.) produce JSON state
// Store as JSONB in Postgres

// Prisma schema:
// model Workflow {
//   id        String   @id @default(cuid())
//   userId    String
//   name      String
//   nodes     Json     ← React Flow nodes array
//   edges     Json     ← React Flow edges array
//   createdAt DateTime @default(now())
// }

// Saving workflow:
async function saveWorkflow(userId: string, name: string, flowData: any) {
  const { nodes, edges } = flowData;
  
  await db.workflow.upsert({
    where: { id: flowData.id },
    create: { userId, name, nodes, edges },
    update: { name, nodes, edges, updatedAt: new Date() },
  });
}

// Loading workflow:
async function loadWorkflow(workflowId: string) {
  const workflow = await db.workflow.findUnique({
    where: { id: workflowId },
  });
  
  return {
    nodes: workflow?.nodes as Node[],
    edges: workflow?.edges as Edge[],
  };
}

Decision Guide

Building a workflow/automation tool (like n8n, Zapier):
  → React Flow (@xyflow/react)
  → 150K+ downloads/week, battle-tested
  → Custom node types for each integration

Building a landing page / email template builder:
  → Craft.js for React-based builder
  → GrapesJS for HTML-output website builder
  → Unlayer for email builder specifically

Building a form builder:
  → React JSON Schema Form (@rjsf/core)
  → Formbuilder.js for drag-and-drop form UX
  → Tripetto or Feathery for complex conditional logic

Building a dashboard builder:
  → GridStack.js for resizable widget layouts
  → Tremor or React Grid Layout for simpler dashboards

Building a database/spreadsheet UI (like Airtable):
  → AG Grid (enterprise) or TanStack Table (free)
  → Baserow (open source Airtable clone) to fork

Find no-code/low-code platform boilerplates at StarterPick.

Comments