react-minimal-survey-builder
v0.4.1
Published
The Headless Survey Engine for Modern React Apps — lightweight, customizable, JSON-schema driven survey builder
Maintainers
Readme
react-minimal-survey-builder
The Headless Survey Engine for Modern React Apps.
A lightweight, customizable, JSON-schema driven survey builder for React. Zero heavy dependencies. Headless-first. TypeScript-first.
Why another survey library?
| Feature | SurveyJS | react-minimal-survey-builder | | ------------ | ---------------- | ---------------------------- | | Bundle Size | Heavy (~200kb+) | ~13kb gzipped | | Headless | Limited | Fully headless | | Custom UI | Hard to override | Easy, plug-and-play | | TypeScript | Partial | First-class | | Dependencies | Many | Zero heavy deps | | WCAG A11y | Partial | Full WCAG 2.1 AA | | SSR | Tricky | Full support |
Installation
npm install react-minimal-survey-builderPeer dependencies: react >= 17, react-dom >= 17
Quick Start
1. Define a schema
import type { SurveySchema } from "react-minimal-survey-builder";
const schema: SurveySchema = {
id: "survey-1",
title: "Customer Feedback",
pages: [
{
id: "page-1",
title: "About You",
questions: [
{
id: "name",
type: "text",
label: "What is your name?",
required: true,
},
{
id: "role",
type: "select",
label: "Your role?",
options: [
{ label: "Developer", value: "dev" },
{ label: "Designer", value: "design" },
{ label: "PM", value: "pm" },
],
},
],
},
],
};2. Render with zero config
import { SurveyRenderer } from "react-minimal-survey-builder";
function App() {
return (
<SurveyRenderer
schema={schema}
options={{
onSubmit: (answers) => console.log(answers),
}}
/>
);
}That's it! You get validation, navigation, progress tracking, and conditional logic out of the box.
Architecture
The library has 3 layers — use only what you need:
┌───────────────────────────────────────┐
│ Builder (Drag & Drop UI) │ ← Optional
├───────────────────────────────────────┤
│ React Layer (Hook + Components) │ ← Optional UI
├───────────────────────────────────────┤
│ Core Engine (Framework Agnostic) │ ← Always available
└───────────────────────────────────────┘Import paths
// Everything
import {
useSurvey,
SurveyRenderer,
SurveyBuilder,
} from "react-minimal-survey-builder";
// Core only (framework agnostic, ~3kb)
import {
createSurveyManager,
validateSurvey,
} from "react-minimal-survey-builder/core";
// Builder only
import { SurveyBuilder } from "react-minimal-survey-builder/builder";API Reference
useSurvey(schema, options?)
The main React hook. Fully headless — bring your own UI.
const {
survey, // Parsed schema
answers, // Current answers: Record<string, AnswerValue>
setAnswer, // (questionId, value) => void
errors, // ValidationError[]
getError, // (questionId) => string | undefined
isValid, // boolean
validate, // () => ValidationError[]
getVisibleQuestions, // () => Question[]
getPageQuestions, // (pageIndex) => Question[]
visiblePages, // Page[]
currentPageIndex,
totalPages,
hasNextPage,
hasPrevPage,
nextPage, // () => boolean
prevPage, // () => boolean
goToPage, // (index) => boolean
submit, // () => Promise<{ success, errors }>
isSubmitted, // boolean
reset, // () => void
progress, // 0-100
} = useSurvey(schema, {
initialAnswers: { name: "John" },
onChange: (answers, questionId) => {},
onSubmit: async (answers) => {},
onValidate: (errors) => {},
onPageChange: (pageIndex) => {},
});<SurveyRenderer />
Pre-built survey UI with full override capabilities.
<SurveyRenderer
schema={schema}
options={{ onSubmit: handleSubmit }}
components={{
text: MyCustomTextInput,
radio: MyCustomRadioGroup,
}}
renderHeader={({ title, progress }) => (
<MyHeader title={title} progress={progress} />
)}
renderFooter={({ nextPage, submit, isLastPage }) => <MyFooter />}
renderComplete={() => <ThankYou />}
/>Render prop mode for complete control:
<SurveyRenderer schema={schema}>
{(survey) => (
<div>
{survey.getPageQuestions(survey.currentPageIndex).map((q) => (
<MyQuestion key={q.id} question={q} value={survey.answers[q.id]} />
))}
</div>
)}
</SurveyRenderer><SurveyBuilder />
Drag-and-drop visual builder with live preview.
import { SurveyBuilder } from "react-minimal-survey-builder";
function BuilderPage() {
const [schema, setSchema] = useState(initialSchema);
return (
<SurveyBuilder
value={schema}
onChange={setSchema}
showPreview // Side-by-side live preview
showJson // JSON editor tab
layout="horizontal" // or "vertical"
/>
);
}Schema Reference
SurveySchema
type SurveySchema = {
id: string;
title?: string;
description?: string;
pages: Page[];
settings?: {
showProgress?: boolean; // default: true
allowBack?: boolean; // default: true
validateOnPageChange?: boolean; // default: true
showQuestionNumbers?: boolean; // default: true
submitText?: string; // default: "Submit"
nextText?: string; // default: "Next"
prevText?: string; // default: "Previous"
};
};Question
type Question = {
id: string;
type:
| "text"
| "textarea"
| "radio"
| "checkbox"
| "select"
| "number"
| "email"
| "phone"
| "url"
| "password"
| "date"
| "time"
| "datetime"
| "slider"
| "rating"
| "boolean"
| "file"
| (string & {}); // custom types
label: string;
description?: string;
placeholder?: string;
required?: boolean;
defaultValue?: AnswerValue;
options?: { label: string; value: string }[];
visibleIf?: string; // Conditional logic expression
validation?: ValidationRule[];
meta?: Record<string, unknown>; // Custom metadata
// File upload specific
accept?: string; // e.g. ".pdf,image/*"
maxFiles?: number; // default 1
maxFileSize?: number; // bytes per file
maxTotalSize?: number; // bytes total
// Other type-specific props: sliderMin/Max/Step, rateCount, countryCode,
// minTime/maxTime, minDatetime, controlType, hideLabel, passwordShowToggle…
};Conditional Logic
Use visibleIf to show/hide questions dynamically:
// Simple comparison
{
visibleIf: "{role} === 'other'";
}
// Multiple conditions
{
visibleIf: "{age} >= 18 && {country} === 'US'";
}
// OR logic
{
visibleIf: "{plan} === 'pro' || {plan} === 'enterprise'";
}
// Truthy check
{
visibleIf: "{newsletter}";
}Validation Rules
{
validation: [
{ type: "required" },
{ type: "email", message: "Please enter a valid email" },
{ type: "minLength", value: 3 },
{ type: "maxLength", value: 100 },
{ type: "min", value: 0 },
{ type: "max", value: 10 },
{ type: "pattern", value: "^[A-Z]", message: "Must start with uppercase" },
{
type: "custom",
validator: (value, _question, allAnswers) => {
if (value === "admin") return "Reserved username";
return true;
},
},
];
}File Upload Questions
The file type provides a drag-and-drop upload zone. Use accept, maxFiles, maxFileSize, and maxTotalSize to configure it:
{
id: "resume",
type: "file",
label: "Upload your résumé",
description: "PDF only, max 5 MB",
required: true,
accept: ".pdf",
maxFiles: 1,
maxFileSize: 5 * 1024 * 1024,
}Answers are returned as FileAnswer[]. Access the raw File object for uploads:
import type { FileAnswer } from "react-minimal-survey-builder";
onSubmit: async (answers) => {
const files = answers["resume"] as FileAnswer[];
const body = new FormData();
files.forEach((f) => body.append("resume", f.file, f.name));
await fetch("/api/upload", { method: "POST", body });
};Note: The
fileproperty on eachFileAnsweris a browser-sessionFileobject — upload it inonSubmitbefore the user navigates away. The other properties (name,size,type,lastModified) are plain serialisable values.
Custom Question Types
Override built-in components or register entirely new types:
import type { QuestionComponentProps } from "react-minimal-survey-builder";
// Custom star rating component
const StarRating = ({ question, value, onChange }: QuestionComponentProps) => (
<div className="star-rating">
{[1, 2, 3, 4, 5].map((n) => (
<span
key={n}
onClick={() => onChange(n)}
className={value >= n ? "star active" : "star"}
>
★
</span>
))}
</div>
);
// Use it
<SurveyRenderer schema={schema} components={{ rating: StarRating }} />;Core Engine (Framework Agnostic)
Use the core engine without React:
import { createSurveyManager } from "react-minimal-survey-builder/core";
const manager = createSurveyManager(schema, {
onSubmit: (answers) => saveToServer(answers),
});
manager.setAnswer("name", "John");
manager.nextPage();
const errors = manager.validate();
const state = manager.getState();
// Event system
const unsub = manager.on("answerChanged", (event) => {
console.log(event.payload);
});Running the Example
# From the project root
cd example
npm install
npm run devThe example app demonstrates all three modes:
- Survey Renderer — pre-built UI with conditional logic
- Headless Hook — custom UI powered by
useSurvey() - Drag & Drop Builder — visual schema editor with live preview
Project Structure
src/
├── core/ # Framework-agnostic engine
│ ├── schema-parser.ts # Schema parsing & validation
│ ├── validation-engine.ts # Question validation
│ ├── condition-evaluator.ts # Conditional logic
│ └── state-manager.ts # State management class
├── react/ # React layer
│ ├── useSurvey.ts # Main React hook
│ ├── SurveyRenderer.tsx # Pre-built survey UI
│ └── QuestionRenderer.tsx # Question component system
├── builder/ # Visual builder
│ ├── SurveyBuilder.tsx # Drag & drop builder
│ ├── QuestionEditor.tsx # Question property editor
│ └── builder-reducer.ts # Builder state management
├── types/ # TypeScript types
│ └── index.ts
└── index.ts # Main entry pointDesign Principles
- No UI lock-in — Use headless hooks or swap every component
- Zero heavy dependencies — Only React as a peer dep
- Tree-shakable — Import only what you use
- SSR safe — No browser-only APIs
- Memoization friendly — Minimal re-renders,
memothroughout - WCAG 2.1 accessible — All form fields include proper ARIA attributes, roles, keyboard focus, and screen reader support
- Clean error messages — Descriptive errors with library prefix
License
MIT © 2026
