Build a Settings Screen
This recipe builds a standard settings screen: grouped sections with headers and footers, switches and pickers that save as soon as they change, and rows that open other screens.
This recipe runs unchanged in the
showcase app, where
SettingsRecipeTest
exercises it.
Use it for preferences the user changes one at a time. Use the form recipe instead when several fields must be validated and saved together with a button.
How the pieces fit
Settings controls are not List Item rows, so the container is Mobile UI's
native:list with native:list-section, not firstlight:list. Mobile UI
renders that container with a SwiftUI List in the inset-grouped style on iOS
and grouped Material sections on Android. There is no settings tag to learn.
Mobile UI gives each row no padding and, on iOS, no background. List Item pads
itself, but Switch and Select do not, so their labels would touch the card edge
and the first letter can be clipped. Wrap each row in a native:column with
px-4 py-3 bg-theme-surface: the padding gives the native row insets, and the
surface colour draws the grouped card on iOS and matches List Item's surface on
Android. Add separator to the list so iOS draws dividers between rows in a
card; Android always does.
1. Register the route
use App\NativeComponents\SettingsScreen;
use Illuminate\Support\Facades\Route;
Route::native('/settings', SettingsScreen::class)->name('settings');
2. Write the screen
Each setting is a public property. updated{Property} runs when PHP accepts a
new value, so it is where the value is saved. This example stores values with
Laravel's cache; use your own model or storage instead.
<?php
namespace App\NativeComponents;
use FirstlightUI\NativeComponent;
use Illuminate\Support\Facades\Cache;
use Illuminate\View\View;
class SettingsScreen extends NativeComponent
{
public bool $notifications = true;
public bool $marketing = false;
public string $digest = 'weekly';
/** @var array<string, string> */
public array $digestOptions = [
'daily' => 'Daily',
'weekly' => 'Weekly',
'never' => 'Never',
];
public function mount(): void
{
$this->notifications = Cache::get('settings.notifications', true);
$this->marketing = Cache::get('settings.marketing', false);
$this->digest = Cache::get('settings.digest', 'weekly');
}
public function updatedNotifications(bool $value): void
{
Cache::forever('settings.notifications', $value);
}
public function updatedMarketing(bool $value): void
{
Cache::forever('settings.marketing', $value);
}
public function updatedDigest(string $value): void
{
Cache::forever('settings.digest', $value);
}
public function openPrivacy(): void
{
$this->navigate('/settings/privacy');
}
public function navTitle(): string
{
return 'Settings';
}
public function render(): View
{
return view('native.settings');
}
}
3. Write the view
Save this as resources/views/native/settings.blade.php:
<native:list class="w-full flex-1 bg-theme-background" separator>
<native:list-section header="Notifications" footer="Alerts for replies and mentions.">
<native:column class="w-full px-4 py-3 bg-theme-surface">
<firstlight:switch label="Push notifications" native:model="notifications" />
</native:column>
<native:column class="w-full px-4 py-3 bg-theme-surface">
<firstlight:switch label="Product news" native:model="marketing" />
</native:column>
</native:list-section>
<native:list-section header="Email" footer="A summary of activity you missed.">
<native:column class="w-full px-4 py-3 bg-theme-surface">
<firstlight:select label="Digest" :options="$digestOptions" native:model="digest" />
</native:column>
</native:list-section>
<native:list-section header="Account">
<native:column class="w-full bg-theme-surface">
<firstlight:list-item
headline="Privacy"
trailing-icon="chevron-right"
trailing-icon-ios="chevron.right"
trailing-icon-android="chevron_right"
@press="openPrivacy"
/>
</native:column>
</native:list-section>
</native:list>
What each platform shows
| Behaviour | iOS | Android |
|---|---|---|
| Grouping | SwiftUI List in the inset-grouped style; the row columns paint the cards |
Rounded section cards with headers and footers, drawn by Mobile UI |
| Row insets | The row column's px-4 py-3 |
The row column's px-4 py-3 inside the card's 16 dp margin |
| Dividers | Between rows when the list sets separator |
Always between rows in a card |
| Switches | SwiftUI Toggle |
Material 3 Switch |
| Digest | Native menu picker | Material 3 exposed dropdown |
| Privacy row | List Item row with a chevron | Material list item with a chevron |
Rules to keep
- Save in
updated{Property}, not in a Save button. Settings screens apply each change immediately. - Wrap every control row in a padded
native:columnwithbg-theme-surface. Rows placed straight in a section have no insets. - Keep a section footer short. It explains the section, not each control.
- Use
firstlight:listonly when every row is a List Item, such as a list of records. Settings mix controls, so they usenative:list. - Put the screen under a layout that uses native chrome, as described in Build a list and detail flow, so the title and back navigation come from the platform.