@letar/forms

MatrixChoice

Matrix choice field for surveys, NPS forms, and questionnaires

Overview

Form.Field.MatrixChoice renders a "question x answer" matrix table — the standard pattern from Google Forms and SurveyMonkey. Supports three variants: radio (single choice), checkbox (multi choice), and rating (stars).

Full example

The complete sandbox example, read directly from the form-develop-app / form-develop-app-shadcn source at build time. The shadcn side demonstrates FieldMatrixChoice inside the broader survey demo (alongside ImageChoice and Likert), not as a standalone page.

apps/form-develop-app/src/app/matrix-choice-demo/page.tsx
'use client'

import { Box, Heading, Text, VStack } from '@chakra-ui/react'
import { Form } from '@letar/forms'
import { useState } from 'react'
import { DemoPageLayout } from '../_components'

export default function MatrixChoiceDemoPage() {
  const [submittedData, setSubmittedData] = useState<Record<string, unknown> | null>(null)

  return (
    <DemoPageLayout
      title="Form.Field.MatrixChoice"
      description="Матричный выбор для опросников — таблица вопрос × вариант ответа"
    >
      <VStack gap={8} align="stretch">
        {/* Пример 1: NPS-опросник (radio) */}
        <Box>
          <Heading size="md" mb={3}>
            NPS-опросник (radio)
          </Heading>
          <Text fontSize="sm" color="fg.muted" mb={4}>
            Один ответ на строку. Как в Google Forms.
          </Text>
          <Form
            debug
            initialValue={{ satisfaction: {} }}
            onSubmit={(data) => setSubmittedData(data as Record<string, unknown>)}
          >
            <Form.Field.MatrixChoice
              name="satisfaction"
              label="Оцените наш сервис"
              rows={[
                { value: 'speed', label: 'Скорость доставки' },
                { value: 'quality', label: 'Качество товара' },
                { value: 'support', label: 'Поддержка клиентов' },
                { value: 'price', label: 'Соотношение цена/качество' },
              ]}
              columns={[
                { value: '1', label: 'Ужасно' },
                { value: '2', label: 'Плохо' },
                { value: '3', label: 'Нормально' },
                { value: '4', label: 'Хорошо' },
                { value: '5', label: 'Отлично' },
              ]}
              variant="radio"
            />
            <Form.Button.Submit>Отправить</Form.Button.Submit>
          </Form>
        </Box>

        {/* Пример 2: Checkbox (множественный) */}
        <Box>
          <Heading size="md" mb={3}>
            Навыки (checkbox)
          </Heading>
          <Text fontSize="sm" color="fg.muted" mb={4}>
            Несколько ответов на строку.
          </Text>
          <Form
            debug
            initialValue={{ skills: {} }}
            onSubmit={(data) => setSubmittedData(data as Record<string, unknown>)}
          >
            <Form.Field.MatrixChoice
              name="skills"
              label="Какими навыками вы владеете?"
              rows={[
                { value: 'frontend', label: 'Frontend' },
                { value: 'backend', label: 'Backend' },
                { value: 'devops', label: 'DevOps' },
              ]}
              columns={[
                { value: 'beginner', label: 'Начинающий' },
                { value: 'intermediate', label: 'Средний' },
                { value: 'advanced', label: 'Продвинутый' },
                { value: 'expert', label: 'Эксперт' },
              ]}
              variant="checkbox"
            />
            <Form.Button.Submit>Отправить</Form.Button.Submit>
          </Form>
        </Box>

        {/* Пример 3: Rating (звёзды) */}
        <Box>
          <Heading size="md" mb={3}>
            Рейтинг (звёзды)
          </Heading>
          <Form
            debug
            initialValue={{ rating: {} }}
            onSubmit={(data) => setSubmittedData(data as Record<string, unknown>)}
          >
            <Form.Field.MatrixChoice
              name="rating"
              label="Оцените блюда"
              rows={[
                { value: 'taste', label: 'Вкус' },
                { value: 'presentation', label: 'Подача' },
                { value: 'portion', label: 'Размер порции' },
              ]}
              columns={[
                { value: '1', label: '★' },
                { value: '2', label: '★★' },
                { value: '3', label: '★★★' },
                { value: '4', label: '★★★★' },
                { value: '5', label: '★★★★★' },
              ]}
              variant="rating"
            />
            <Form.Button.Submit>Отправить</Form.Button.Submit>
          </Form>
        </Box>

        {submittedData && (
          <Box p={4} bg="bg.subtle" borderRadius="md">
            <Heading size="sm" mb={2}>
              Отправленные данные:
            </Heading>
            <pre style={{ fontSize: '12px', overflow: 'auto' }}>{JSON.stringify(submittedData, null, 2)}</pre>
          </Box>
        )}
      </VStack>
    </DemoPageLayout>
  )
}

Basic Usage (Radio)

<Form initialValue={{ satisfaction: {} }} onSubmit={handleSubmit}>
  <Form.Field.MatrixChoice
    name="satisfaction"
    label="Rate our service"
    rows={[
      { value: 'speed', label: 'Delivery speed' },
      { value: 'quality', label: 'Product quality' },
      { value: 'support', label: 'Customer support' },
    ]}
    columns={[
      { value: '1', label: 'Terrible' },
      { value: '2', label: 'Bad' },
      { value: '3', label: 'OK' },
      { value: '4', label: 'Good' },
      { value: '5', label: 'Excellent' },
    ]}
    variant="radio"
  />
  <Form.Button.Submit>Submit</Form.Button.Submit>
</Form>

Value: { speed: '4', quality: '5', support: '3' }

Checkbox Variant (Multi-Select)

<Form.Field.MatrixChoice
  name="skills"
  variant="checkbox"
  rows={[
    { value: 'frontend', label: 'Frontend' },
    { value: 'backend', label: 'Backend' },
  ]}
  columns={[
    { value: 'beginner', label: 'Beginner' },
    { value: 'intermediate', label: 'Intermediate' },
    { value: 'expert', label: 'Expert' },
  ]}
/>

Value: { frontend: ['beginner', 'intermediate'], backend: ['expert'] }

Rating Variant

<Form.Field.MatrixChoice
  name="rating"
  variant="rating"
  rows={[
    { value: 'taste', label: 'Taste' },
    { value: 'presentation', label: 'Presentation' },
  ]}
  columns={[
    { value: '1', label: '1' },
    { value: '2', label: '2' },
    { value: '3', label: '3' },
    { value: '4', label: '4' },
    { value: '5', label: '5' },
  ]}
/>

Props

PropTypeDefaultDescription
namestringrequiredField name
labelstringLabel above matrix
rowsMatrixRow[]requiredQuestions (row labels)
columnsMatrixColumn[]requiredAnswer options (column headers)
variant'radio' | 'checkbox' | 'rating''radio'Selection mode
disabledbooleanfalseDisable all inputs
readOnlybooleanfalseRead-only mode

Keyboard Navigation

KeyAction
Arrow Right/LeftMove between columns
Arrow Up/DownMove between rows
Enter / SpaceSelect current cell

Required Validation

When required={true}, unfilled rows are highlighted in red after form validation triggers. Use Zod schema validation to enforce all rows are filled.

Responsive Behavior

On mobile (< md breakpoint), the table automatically switches to a card-based layout where each question is a separate card with vertically stacked options.


Live Example

Try the interactive example on forms-example.letar.best.

On this page