Skip to main content
MatrixChoiceField ← Back to Table of Contents

Summary

Multiple choice grid (matrix / survey table): row labels on the left, column headers on top, radio or checkbox in each cell. Gray inset frame with white body panel. Per-row validation and reactive conditional disabling (no live() required).
ClassBjanczak\FilamentFlexFields\Filament\Forms\Components\MatrixChoiceField
State typeRadio: array<string, string|null> · Checkbox: array<string, list<string>>
FieldTypematrix_choice
Playgroundmatrix-choice
StylesheetLazy matrix-choice-field bundle
Model cast'responses' => 'array' or 'responses' => 'json'
Use matrixColumns()not columns() — because columns() is reserved by Filament layout grids.

Full example

use Bjanczak\FilamentFlexFields\Filament\Forms\Components\MatrixChoiceField;

MatrixChoiceField::make('feature_priorities')
    ->label('Feature priorities')
    ->helperText('Assign priority per feature. Dark mode High blocks CSV High.')
    ->mode('checkbox')
    ->size('md')
    ->color('primary')
    ->rows([
        'dark_mode' => [
            'label' => 'Dark mode',
            'description' => 'UI theme support',
            'required' => true,
            'max_selections' => 1,
        ],
        'csv_export' => [
            'label' => 'CSV export',
            'min_selections' => 1,
            'max_selections' => 2,
        ],
        'api_access' => [
            'label' => 'API access',
            'disabled' => true,
        ],
    ])
    ->matrixColumns([
        'low' => 'Low',
        'medium' => 'Medium',
        'high' => [
            'label' => 'High',
            'icon' => 'heroicon-o-bolt',
        ],
    ])
    ->requiredRows(['dark_mode'])
    ->disabledCells([
        // Static: always lock CSV → Low (example)
        // 'csv_export' => ['low'],
    ])
    ->disableCellWhen('csv_export', 'high', 'dark_mode', 'high')
    ->disableRowWhen('api_access', 'dark_mode', 'low')
    ->default([
        'dark_mode' => ['high'],
        'csv_export' => ['medium'],
    ]);
Radio mode (one answer per row — survey / mood matrix):
MatrixChoiceField::make('mood')
    ->label('Tell us about your mood')
    ->mode('radio')
    ->rows([
        'saturday' => ['label' => 'Saturday', 'required' => true],
        'sunday' => ['label' => 'Sunday', 'required' => true],
        'monday' => 'Monday',
    ])
    ->matrixColumns([
        'happy' => 'Happy',
        'neutral' => 'Neutral',
        'sad' => 'Sad',
        'pleading' => 'Pleading',
        'party' => 'Party',
        'zany' => 'Zany',
    ])
    ->default([
        'saturday' => 'happy',
        'sunday' => 'neutral',
    ]);

Row option shape

Each key in rows() is stored in the database. Value can be a plain string (used as label) or a rich array:
KeyTypeDefaultDescription
labelstringrow keyLeft column row title
description / descstring|nullnullOptional subtitle under row label
requiredboolfalseRow must have at least one selection. Overrides requiredRows() when set explicitly
disabledboolfalseEntire row locked (all cells disabled)
min_selections / minint|nullnullCheckbox only — minimum selected columns in this row
max_selections / maxint|nullnullCheckbox only — maximum selected columns in this row
->rows([
    'billing' => 'Billing', // shorthand for ['label' => 'Billing']
    'shipping' => [
        'label' => 'Shipping',
        'description' => 'Delivery options',
        'required' => true,
        'min_selections' => 1,
        'max_selections' => 2,
    ],
])

Column option shape

Each key in matrixColumns() is a selectable column id (stored in state).
FormExample
key => 'Label''happy' => 'Happy'
Rich array'high' => ['label' => 'High', 'icon' => 'heroicon-o-bolt', 'disabled' => true]
KeyTypeDescription
labelstringHeader text (or emoji) shown above cells
iconstring|nullOptional Heroicon above label (alternative to columnIcons())
disabledboolDisables this column in every row
->matrixColumns([
    'low' => 'Low',
    'high' => ['label' => 'High', 'icon' => 'heroicon-o-fire'],
])
->columnIcons([
    'low' => 'heroicon-o-arrow-down',
    'high' => 'heroicon-o-arrow-up',
]);

State format

Radio mode — one column key per row (or omitted if empty):
{
  "saturday": "happy",
  "sunday": "neutral"
}
Checkbox mode — list of column keys per row:
{
  "dark_mode": ["high"],
  "csv_export": ["medium", "low"]
}
  • Default state: []
  • On dehydrate, empty rows and invalid keys are stripped
  • Use Eloquent cast 'field' => 'array' or 'field' => 'json'

Validation

Built-in (per row)

RuleRadioCheckboxDetail
required on rowRow must have a selection
requiredRows([...])Mark rows required by key
required() on fieldAll non-disabled rows required when no requiredRows() set
min_selectionsMin columns selected in row
max_selectionsMax columns selected in row
Static disabled / disabledRowsSelection in locked row fails
disabledCellsSelection in locked cell fails
disableCellWhen / disableRowWhenSame rules enforced server-side
Translation keys (resources/lang/en/default.php):
KeyWhen
validation.matrix_choice.invalidState is not an array
validation.matrix_choice.invalid_optionUnknown or disabled column selected
validation.matrix_choice.row_requiredRequired row empty (:row)
validation.matrix_choice.row_minToo few selections (:row, :count)
validation.matrix_choice.row_maxToo many selections (:row, :count)

Custom cross-row rules

Use standard Filament ->rule() for business logic across rows:
use Closure;

MatrixChoiceField::make('features')
    ->mode('checkbox')
    ->rows([...])
    ->matrixColumns([...])
    ->rule(function (): Closure {
        return function (string $attribute, mixed $value, Closure $fail): void {
            $value = is_array($value) ? $value : [];

            if (in_array('high', $value['dark_mode'] ?? [], true)
                && in_array('high', $value['csv_export'] ?? [], true)) {
                $fail('High priority can only be assigned to one feature.');
            }
        };
    });

Configuration API

mode('radio'|'checkbox')

ValueBehaviour
radio (default)Exactly one column per row
checkboxZero or more columns per row
->mode('radio')    // survey grid
->mode('checkbox') // multi-tag per row

rows(array|Closure $rows)

Row definitions — see Row option shape. Accepts Closure for dynamic rows.
MatrixChoiceField::make('field_name')
    ->rows(['value1', 'value2']);

matrixColumns(array|Closure $columns)

Column headers — see Column option shape.
MatrixChoiceField::make('field_name')
    ->matrixColumns(['value1', 'value2']);

columnIcons(array|Closure $icons)

Per-column icon map merged into column metadata:
->columnIcons([
    'happy' => 'heroicon-o-face-smile',
    'sad' => 'heroicon-o-face-frown',
])

requiredRows(array|Closure $keys)

Mark rows as required without inline required => true:
->requiredRows(['saturday', 'sunday'])

disabledRows(array|Closure $keys)

Lock entire rows by key (static, always on):
->disabledRows(['archived_feature', 'legacy_api'])

disabledCells(array|Closure $map)

Lock specific cells. Map shape: rowKey => [columnKey, ...]:
->disabledCells([
    'csv_export' => ['low'],
    'dark_mode' => ['high', 'medium'],
])
Accepts Closure for server-side dynamic maps (re-evaluated on each render; use with live() for server-driven updates).

disableCellWhen($row, $column, $whenRow, $whenColumns)

Reactive (client-side Alpine) — disable one cell when a trigger row matches column key(s). No live() needed.
// When dark_mode includes High → disable csv_export → High
->disableCellWhen('csv_export', 'high', 'dark_mode', 'high')

// Multiple trigger columns (any match)
->disableCellWhen('csv_export', 'high', 'dark_mode', ['high', 'critical'])
ArgumentDescription
$rowTarget row to disable
$columnTarget column to disable
$whenRowRow to watch
$whenColumnsstring or list<string> — trigger column key(s)
Trigger modeMatch condition
radiowhenRow selected column equals one of whenColumns
checkboxwhenRow selection includes any of whenColumns
Invalid selections in newly disabled cells are removed automatically.

disableRowWhen($row, $whenRow, $whenColumns)

Reactive — disable an entire row when trigger row matches:
// When dark_mode is Low → disable entire api_access row
->disableRowWhen('api_access', 'dark_mode', 'low')

size('sm'|'md'|'lg')

Control scale for row labels, column headers, and radio/checkbox indicators. Default: md.
->size('sm')  // compact tables
->size('lg')  // touch-friendly

color('primary'|'secondary'|'success'|'warning'|'danger'|null)

Filament accent for selected radio/checkbox indicators. Default: primary.
->color('success')

Inherited Filament field API

Also supports standard Inherited Filament field API:
MethodTypical use
label() / helperText()Field title above grid
required()All rows required (unless requiredRows() narrows scope)
disabled()Disable entire field
default() / dehydrated()Initial state and persistence
live()Optional — not needed for disableCellWhen / disableRowWhen
afterStateUpdated()React to changes (autosave, logging)
rule()Custom validation (see above)

Public helper methods

MethodReturnsDescription
getMode()stringradio or checkbox
isCheckboxMode()boolCheckbox mode flag
getRowKeys() / getColumnKeys()list<string>Valid keys
getNormalizedRows()arrayMerged row metadata
getNormalizedColumns()arrayMerged column metadata
getDisabledCellsMap()array<string, list<string>>Static disabled cells
getConditionalDisableRules()list<array>disableCellWhen / disableRowWhen rules
matchesConditionalDisableRule($rule, $state)boolTest rule against state
isRowDisabled($row, $state?)boolStatic + conditional row lock
isCellDisabled($row, $column, $state?)boolStatic + conditional cell lock
dehydrateValue($state)arrayNormalize state for storage
getWrapperClasses()list<string>fff-matrix-choice BEM classes
getMatrixSizeStyles()arrayCSS custom properties

FlexField schema config

Config keyMaps to
modemode()
rowsrows()
columnsmatrixColumns()
column_iconscolumnIcons()
disabled_rowsdisabledRows()
required_rowsrequiredRows()
disabled_cellsdisabledCells()
disable_cell_whendisableCellWhen() — list of rule arrays
disable_row_whendisableRowWhen() — list of rule arrays
sizesize() — default from config('filament-flex-fields.ui.matrix_choice_size', 'md')
colorcolor()
disable_cell_when / disable_row_when rule array:
'disable_cell_when' => [
    [
        'row' => 'csv_export',
        'column' => 'high',
        'when_row' => 'dark_mode',
        'when_columns' => 'high', // or ['high', 'medium']
    ],
],
'disable_row_when' => [
    [
        'row' => 'api_access',
        'when_row' => 'dark_mode',
        'when_columns' => 'low',
    ],
],

CSS classes

ClassRole
fff-matrix-choiceRoot wrapper
fff-matrix-choice--{sm|md|lg}Size modifier
fff-matrix-choice--{radio|checkbox}Mode modifier
fff-matrix-choice__frameGray outer frame
fff-matrix-choice__headerColumn header row
fff-matrix-choice__bodyWhite inset panel
fff-matrix-choice__rowData row
fff-matrix-choice__cellClickable grid cell
fff-matrix-choice__cell.is-selectedSelected cell (animated indicator)
fff-matrix-choice__cell.is-disabledLocked cell

Implementation notes

  • Radio/checkbox indicators reuse Flex Radiolist / Flex Checklist animation tokens (fff-choice-cards-indicator-pop).
  • All clicks are handled on fff-matrix-choice__cell; inner inputs use pointer-events-none to prevent double-toggle.
  • Conditional rules run in Alpine on every state change; pruneDisabledSelections() clears invalid picks.
  • Playground slug: matrix-choice (demos: mood radio grid + feature priorities checkbox).