owenmelbz/nova-radio-field
A Laravel Nova field that renders options as radio buttons instead of a dropdown, giving a clearer single-choice selection UI. Configure it with the same options array you'd use for Nova's built-in select fields, and it drops straight into resource forms with no extra setup.
Install via Composer — Nova auto-discovers the field, so there's no service provider, config publish, or asset publish step:
composer require owenmelbz/nova-radio-field
Before touching a resource, spend ten minutes on a compatibility check. The last release was July 2021, which predates Nova 4's Vite asset pipeline and Vue 3. Run composer show owenmelbz/nova-radio-field -a to see the laravel/nova constraint it declares, then open vendor/owenmelbz/nova-radio-field/ and look at three files in this order: the PHP Radio class (tiny — options() and toggle() are the whole API), composer.json, and dist/js/*. The README won't tell you whether it works on your Nova version; those three will get you most of the way, and a five-minute render test settles it.
The first use case is any small enum you already have as a Select. Open app/Nova/Post.php and swap it:
use OwenMelbz\RadioField\Radio;
Radio::make('Status')
->options([
'draft' => 'Draft',
'published' => 'Published',
'archived' => 'Archived',
])
->default('draft')
->rules('required');
Then prove the three things that actually matter in one sitting, on one throwaway resource:
->toggle(['published' => ['publish_date']]) and confirm the target hides and shows.If render fails, stop there. You're on an unsupported Nova, and the fix is forking the Vue component into nova-components/, not working around it in resource code.
The toggle map is the reason you're adopting this. Map option values to arrays of field attribute names — the first argument to make(), not the label:
Radio::make('Status')
->options(['draft' => 'Draft', 'published' => 'Published', 'archived' => 'Archived'])
->toggle([
'published' => ['publish_date', 'featured_image', 'author_id'],
'archived' => ['archived_at', 'archive_reason'],
])
->rules('required');
The targets must exist in the same fields() array. The frontend resolves them by attribute name at runtime, so a rename in either place silently breaks the pairing.
Treat it as an ordinary Nova field. It subclasses Laravel\Nova\Fields\Field, so ->help(), ->nullable(), ->sortable(), ->hideFromIndex(), ->onlyOnForms(), ->resolveUsing(), and ->fillUsing() all compose as usual. Persistence behaves exactly like a Select — the option key is stored verbatim — so unless you override with resolveUsing/fillUsing, there's nothing new to learn about storage. Keep ->options() last in the chain for readability; there's no ordering requirement, it's just easier to scan.
Centralize repeated status blocks in a factory. The moment a second resource needs the same radio-plus-toggle, extract it — otherwise the toggle map drifts and you get inconsistent visibility across panels:
class PublishStatusRadio
{
public static function make(string $attribute = 'status'): Radio
{
return Radio::make($attribute)
->options(['draft' => 'Draft', 'published' => 'Published', 'archived' => 'Archived'])
->toggle([
'published' => ['publish_date', 'featured_image', 'author_id'],
'archived' => ['archived_at', 'archive_reason'],
])
->default('draft')
->rules('required');
}
}
Because the fluent API returns $this, callers can still layer on ->help('...') or ->sortable() per resource. This factory is also the single place to edit when the option set changes — a real win when three resources share a workflow.
Pair a default with the toggle. The "draft by default, reveal publishing fields only on Published" flow is the canonical pattern and the one most likely to justify the package over a plain Select:
PublishStatusRadio::make()
->toggle([...])
->default('draft'),
New records land in the safe state and the form stays short until the user makes an explicit choice.
Split responsibilities with native dependsOn(). Nova 3.14+ ships value-driven visibility. Use dependsOn() for chained or computed dependencies (field B depends on field A depends on field C) and reserve ->toggle() for the flat "one radio drives a fixed set of siblings" case. Critically, never point both mechanisms at the same target field — they fight over the same visibility style and the last render wins.
Keep it form-side. ->toggle() is a form-view behaviour only. On index and detail the radio renders as its plain value and the toggle map is ignored. If children shouldn't appear on detail, use their own ->hideFromDetail(); don't expect the radio to govern that.
Fork the Vue component when you need custom markup. Per-option descriptions, icons, or inline help won't reach the render path through the field's meta bag. The component is one file — copy resources/js/components/Radio.vue into your own Nova component and own the build. That's a smaller job than it sounds and it's the natural escape hatch.
->toggle([1 => [...]]) won't reliably match when the user selects 1. Use string keys ('yes', 'draft', 'published') or cast on resolve. This bites hardest when you also compare against dependsOn callbacks or index filters.->hideWhenUpdating() or a closure that returns nothing can leave the radio's JS unable to resolve its target — usually a console error, sometimes a silent no-op. Prefer ->onlyOnForms() / ->hideFromIndex() on the child over conditional registration.dependsOn().->toggle(['published' => ['does_not_exist']]) — tells you whether the listener is wired at all: if you see a lookup error, the JS is running; if nothing happens, the bundle isn't loading.->toggle() doing nothing on index/detail is expected. It's form behaviour. Don't file a bug.Select. No surprises, but also no magic — if you need to transform the stored value, layer ->resolveUsing() / ->fillUsing() as you would with any Nova field.How can I help you explore Laravel packages today?