Radio
PandaPanel\Forms\Components\Radio takes one choice out of a handful, with every option visible at once. It holds the same data a select holds and trades space for the ability to read all the choices without opening anything — so reach for it when there are three or four options that need comparing, and for a Select when there are more.
The minimal example
use PandaPanel\Forms\Components\Radio;
use PandaPanel\Forms\FormSchema;
FormSchema::make()->schema([
Radio::make('plan')
->options([
'free' => 'Free',
'pro' => 'Pro',
'enterprise' => 'Enterprise',
])
->required(),
]);2
3
4
5
6
7
8
9
10
11
12
The methods
public function options(array $options): self // array<array-key, string>
public function descriptions(array $descriptions): self // array<array-key, string>
public function inline(bool $inline = true): self // default: false2
3
| Method | Default | Effect |
|---|---|---|
options() | [] | the choices, keyed by stored value; also the validation whitelist |
descriptions() | [] | a line under an option's label, keyed the same way |
inline() | false | lays the options out in a wrapping row rather than a column |
use PandaPanel\Forms\Components\Radio;
Radio::make('plan')
->label('Subscription')
->options([
'free' => 'Free',
'pro' => 'Pro',
'enterprise' => 'Enterprise',
])
->descriptions([
'pro' => 'Everything, billed monthly',
'enterprise' => 'Volume pricing and a named contact',
])
->default('free')
->helperText('Changing this takes effect at the next renewal.');2
3
4
5
6
7
8
9
10
11
12
13
14
15
descriptions() is keyed by option key, not by position, and an option with no description simply has none. Both arrays accept int keys, which is what makes an enum-backed column work:
use PandaPanel\Forms\Components\Radio;
Radio::make('priority')
->options([1 => 'Low', 2 => 'Normal', 3 => 'High'])
->inline();2
3
4
5
Validation
The declared options are the whitelist. A value the schema never offered is not merely unexpected, it is invalid:
use PandaPanel\Forms\Components\Radio;
use PandaPanel\Forms\FormSchema;
$rules = FormSchema::make()
->schema([Radio::make('plan')->options(['free' => 'Free', 'pro' => 'Pro'])])
->validationRules();
// ['plan' => ['nullable', Illuminate\Validation\Rules\In]]
// The rule renders as: in:"free","pro"2
3
4
5
6
7
8
9
The rule is built with Illuminate\Validation\Rule::in() over the option keys cast to strings, so [1 => 'Low'] produces in:"1" and a form posting '1' passes.
With no options declared, no in rule is generated at all. A radio group with an empty option list renders nothing and validates nothing beyond nullable/required — which is worth knowing when the options come from a query that returned no rows.
Hydration and the value
protected function castForForm(mixed $value): string|int|nullA string or an int is passed through; anything else — a bool, an enum instance, an array — becomes null.
On the wire, every option value is a string:
use PandaPanel\Forms\Components\Radio;
Radio::make('plan')
->options(['free' => 'Free', 'pro' => 'Pro'])
->descriptions(['pro' => 'Everything, billed monthly'])
->inline()
->toArray(null, 'create');2
3
4
5
6
7
{
"type": "radio",
"inline": true,
"options": [
{ "value": "free", "label": "Free", "description": null },
{ "value": "pro", "label": "Pro", "description": "Everything, billed monthly" }
]
}2
3
4
5
6
7
8
The control compares the selection as a string too, so a key that is 1 in the database and '1' in the form is the same option rather than two.
Working with enums
A backed enum is not accepted directly by options(), which wants array<array-key, string>. Build the map, and cast back on the way out if the column expects the enum:
use App\Enums\Priority;
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Forms\Components\Radio;
Radio::make('priority')
->options(array_combine(
array_map(static fn (Priority $case): string => $case->value, Priority::cases()),
array_map(static fn (Priority $case): string => $case->label(), Priority::cases()),
))
->formatUsing(static fn (mixed $value, ?Model $record): ?string => $value instanceof Priority
? $value->value
: (is_string($value) ? $value : null))
->mutateUsing(static fn (mixed $value, ?Model $record): ?Priority => is_string($value)
? Priority::tryFrom($value)
: null);2
3
4
5
6
7
8
9
10
11
12
13
14
15
formatUsing() is needed because an enum-cast attribute returns a Priority instance, and castForForm() answers null for it.
Driving other fields
A radio group is a natural subject for a declarative condition, and both Equals and In work without a request:
use PandaPanel\Forms\Components\Radio;
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\Enums\ConditionOperator;
Radio::make('plan')->options(['free' => 'Free', 'pro' => 'Pro', 'enterprise' => 'Enterprise']),
TextInput::make('seats')
->visibleWhen('plan', ConditionOperator::In, ['pro', 'enterprise']),
TextInput::make('purchase_order')
->visibleWhen('plan', ConditionOperator::Equals, 'enterprise'),2
3
4
5
6
7
8
9
10
11
If something the server has to compute depends on the choice, mark it live() instead.
What crosses the wire
interface DescribedOption extends SelectOption {
value: string;
label: string;
description: string | null;
}
interface RadioFieldDefinition extends BaseFieldDefinition {
type: 'radio';
options: DescribedOption[];
inline: boolean;
}2
3
4
5
6
7
8
9
10
11
Gotchas
The submitted value is a string. '1' reaches the server, not 1. Laravel will happily write it to an integer column, but a strict comparison in a hook will not match. Cast it in mutateUsing() if the type matters.
No options means no whitelist. An empty options() array skips the in rule entirely. If the list is built from data, guard against it being empty rather than assuming the rule protects you.
Options are not searchable and not lazy. Every option is serialized with the form. A list long enough to want a search box wants a Select with searchable(), which fetches through the options endpoint.
descriptions() does not create options. A description for a key that is not in options() is ignored.
A radio cannot be cleared by the user. The control offers no "none" affordance; once an option is picked there is no way back to null. Declare the empty choice as a real option with its own key, and map it to null on the way out:
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Forms\Components\Radio;
Radio::make('assignee_kind')
->options([
'none' => 'Unassigned',
'user' => 'A person',
'team' => 'A team',
])
->default('none')
->mutateUsing(static fn (mixed $value, ?Model $record): ?string => $value === 'none'
? null
: (is_string($value) ? $value : null));2
3
4
5
6
7
8
9
10
11
12
13
inline() is layout only. It wraps the options into a row; it does not change the value, the rules, or anything the server sees.
See also
- Select — the same data, with search and relationships
- Checkbox — a single boolean
- Toggle
- Visibility —
Equals,In, and the rest - Live Fields
- Validation
- Forms and Schemas