S
Documentation

Custom Components

Override default question renderers with custom React components for a unique look, feel, or behaviour.

Custom Text Input

A styled text input with floating label and error state:

FloatingInput.tsxtsx
import type { QuestionComponentProps } from 'react-minimal-survey-builder';

function FloatingInput({ question, value, onChange, error, disabled }: QuestionComponentProps) {
  const val = (value as string) ?? '';

  return (
    <div className="relative mb-6">
      <input
        type={question.type === 'email' ? 'email' : 'text'}
        value={val}
        onChange={(e) => onChange(e.target.value)}
        disabled={disabled}
        placeholder=" "
        className={`peer w-full px-4 pt-5 pb-2 rounded-xl border-2 transition-colors
          bg-white dark:bg-gray-900 text-gray-900 dark:text-white
          ${error
            ? 'border-red-400 focus:border-red-500'
            : 'border-gray-200 focus:border-blue-500'}
          focus:outline-none disabled:opacity-50`}
      />
      <label
        className={`absolute left-4 top-2 text-xs transition-all
          peer-placeholder-shown:top-3.5 peer-placeholder-shown:text-base
          peer-focus:top-2 peer-focus:text-xs
          ${error ? 'text-red-400' : 'text-gray-400 peer-focus:text-blue-500'}`}
      >
        {question.label}
        {question.required && <span className="text-red-500 ml-0.5">*</span>}
      </label>
      {question.description && (
        <p className="text-xs text-gray-400 mt-1 ml-1">{question.description}</p>
      )}
      {error && <p className="text-red-500 text-xs mt-1 ml-1">{error}</p>}
    </div>
  );
}

Custom Radio Group

CardRadio.tsxtsx
function CardRadio({ question, value, onChange, error, disabled }: QuestionComponentProps) {
  return (
    <div className="mb-6">
      <p className="font-medium mb-2">
        {question.label}
        {question.required && <span className="text-red-500 ml-0.5">*</span>}
      </p>
      <div className="grid grid-cols-2 gap-2">
        {question.options?.map((opt) => (
          <button
            key={opt.value}
            type="button"
            disabled={disabled}
            onClick={() => onChange(opt.value)}
            className={`p-3 rounded-xl border-2 text-sm font-medium transition-all
              ${value === opt.value
                ? 'border-blue-500 bg-blue-50 text-blue-700'
                : 'border-gray-200 hover:border-gray-300 text-gray-600'}
              disabled:opacity-50 disabled:cursor-not-allowed`}
          >
            {opt.label}
          </button>
        ))}
      </div>
      {error && <p className="text-red-500 text-xs mt-1">{error}</p>}
    </div>
  );
}

Custom Textarea

StyledTextarea.tsxtsx
function StyledTextarea({ question, value, onChange, error, disabled }: QuestionComponentProps) {
  const val = (value as string) ?? '';
  const maxLength = question.validation?.find((r) => r.type === 'maxLength')?.value as number | undefined;

  return (
    <div className="mb-6">
      <label className="block font-medium mb-1.5">
        {question.label}
        {question.required && <span className="text-red-500 ml-0.5">*</span>}
      </label>
      <textarea
        value={val}
        onChange={(e) => onChange(e.target.value)}
        disabled={disabled}
        placeholder={question.placeholder}
        rows={4}
        className={`w-full px-4 py-3 rounded-xl border-2 transition-colors
          bg-white dark:bg-gray-900 resize-none
          ${error ? 'border-red-400' : 'border-gray-200 focus:border-blue-500'}
          focus:outline-none disabled:opacity-50`}
      />
      <div className="flex justify-between mt-1">
        {error ? (
          <p className="text-red-500 text-xs">{error}</p>
        ) : <span />}
        {maxLength && (
          <span className="text-xs text-gray-400">{val.length}/{maxLength}</span>
        )}
      </div>
    </div>
  );
}

Custom Rating (Emoji)

EmojiRating.tsxtsx
function EmojiRating({ question, value, onChange, error, disabled }: QuestionComponentProps) {
  const emojis = [
    { emoji: '😡', label: 'Terrible', value: 1 },
    { emoji: '😕', label: 'Bad', value: 2 },
    { emoji: '😐', label: 'Okay', value: 3 },
    { emoji: '😊', label: 'Good', value: 4 },
    { emoji: '🤩', label: 'Amazing', value: 5 },
  ];

  return (
    <div className="mb-6">
      <p className="font-medium mb-2">
        {question.label}
        {question.required && <span className="text-red-500 ml-0.5">*</span>}
      </p>
      <div className="flex gap-3 justify-center">
        {emojis.map((e) => (
          <button
            key={e.value}
            type="button"
            disabled={disabled}
            onClick={() => onChange(e.value)}
            className={`flex flex-col items-center p-2 rounded-xl transition-all
              ${value === e.value ? 'scale-125 bg-blue-50' : 'opacity-60 hover:opacity-100'}
              disabled:cursor-not-allowed`}
          >
            <span className="text-3xl">{e.emoji}</span>
            <span className="text-xs text-gray-500 mt-1">{e.label}</span>
          </button>
        ))}
      </div>
      {error && <p className="text-red-500 text-xs mt-2 text-center">{error}</p>}
    </div>
  );
}

Putting It All Together

Complete Exampletsx
import { SurveyRenderer } from 'react-minimal-survey-builder';
import type { SurveySchema } from 'react-minimal-survey-builder';

const schema: SurveySchema = {
  id: 'custom-ui-demo',
  title: 'Feedback',
  pages: [
    {
      id: 'page1',
      questions: [
        { id: 'name', type: 'text', label: 'Name', required: true },
        { id: 'email', type: 'email', label: 'Email', required: true },
        {
          id: 'experience',
          type: 'radio',
          label: 'How was your experience?',
          required: true,
          options: [
            { label: 'Excellent', value: 'excellent' },
            { label: 'Good', value: 'good' },
            { label: 'Fair', value: 'fair' },
            { label: 'Poor', value: 'poor' },
          ],
        },
        { id: 'rating', type: 'rating', label: 'Overall rating', required: true },
        {
          id: 'comments',
          type: 'textarea',
          label: 'Comments',
          placeholder: 'Share your thoughts...',
          validation: [{ type: 'maxLength', value: 500 }],
        },
      ],
    },
  ],
};

function App() {
  return (
    <SurveyRenderer
      schema={schema}
      className="max-w-lg mx-auto p-8 bg-white rounded-2xl shadow-xl"
      components={{
        text: FloatingInput,
        email: FloatingInput,
        radio: CardRadio,
        textarea: StyledTextarea,
        rating: EmojiRating,
      }}
      options={{
        onSubmit: (answers) => console.log('Submitted:', answers),
      }}
      renderComplete={() => (
        <div className="text-center py-8">
          <p className="text-4xl mb-3">🎉</p>
          <h2 className="text-xl font-bold">Thanks for your feedback!</h2>
        </div>
      )}
    />
  );
}

Tips

  • - Custom components receive the same QuestionComponentProps interface.
  • - Always handle disabled and error props for consistency.
  • - You can mix custom and default components — only override what you need.
  • - See Custom Question Types for creating entirely new types.