# Firstlight UI > Current guides, concepts, and reference documentation for developers using Firstlight UI. This file contains the full text of every Firstlight UI documentation page. --- # Firstlight UI documentation Source: https://firstlightui.dev/docs - [Installation](https://firstlightui.dev/docs/getting-started/installation) — Install Firstlight and make its native renderers available to a NativePHP application. - [Add your first component](https://firstlightui.dev/docs/getting-started/first-component) — Add a complete Segmented control with server-backed state. - [Validate fields](https://firstlightui.dev/docs/how-to/validate-fields) — Show Laravel validation messages on Firstlight fields with `validate()`, `validateOnly()`, and Form Requests. - [Submit forms](https://firstlightui.dev/docs/how-to/submit-forms) — Validate Firstlight fields, run a PHP action once, and publish success Feedback. - [Authorize actions](https://firstlightui.dev/docs/how-to/authorize-actions) — Hide, disable, or guard Firstlight actions with Laravel Gate and Policy decisions. - [Paginate lists](https://firstlightui.dev/docs/how-to/paginate-lists) — Bind Laravel paginators to List pull-to-refresh and end-reached. - [Destroy list items](https://firstlightui.dev/docs/how-to/destroy-list-items) — Authorize Confirmation Dialog deletion of List rows by stable keys. - [Localize chrome](https://firstlightui.dev/docs/how-to/localize) — Override Firstlight-owned chrome labels and inherit application locale and timezone for Date and Time pickers. - [Theming](https://firstlightui.dev/docs/getting-started/theming) — Configure the semantic NativePHP theme tokens inherited by Firstlight components. - [SuperNative components](https://firstlightui.dev/docs/concepts/supernative-components) — Understand how an EDGE component becomes genuine platform UI. - [Server-authoritative state](https://firstlightui.dev/docs/concepts/server-authoritative-state) — Understand interaction, PHP acceptance, and native reconciliation. - [Firstlight and NativePHP Mobile UI](https://firstlightui.dev/docs/concepts/firstlight-and-mobile-ui) — Understand how Firstlight complements Mobile UI with a focused form and control layer. - [Activity Indicator](https://firstlightui.dev/docs/components/activity-indicator) — Communicate indeterminate native activity with semantic sizes and one polite appearance announcement. - [Button](https://firstlightui.dev/docs/components/button) — Look up labelled actions, semantic variants, loading and disabled states, accessibility, and adapter behaviour. - [Callout](https://firstlightui.dev/docs/components/callout) — Present persistent semantic messages with one optional labelled action. - [Transient Feedback](https://firstlightui.dev/docs/components/transient-feedback) — Publish queued app-level outcomes with semantic tones, optional actions, and automatic or held lifetime. - [Badge](https://firstlightui.dev/docs/components/badge) — Present compact display-only counts or short markers with semantic tones and contextual accessibility. - [Checkbox](https://firstlightui.dev/docs/components/checkbox) — Configure strict Boolean form and checklist state with server-authoritative proposals. - [Choice Group](https://firstlightui.dev/docs/components/choice-group) — Configure visible single-radio or multiple-checkbox choice rows with stable values. - [Media](https://firstlightui.dev/docs/components/media) — Capture one image or document into Storage with optional crop and ValidatesFields. - [Alert Dialog](https://firstlightui.dev/docs/components/alert-dialog) — Present one native acknowledgement with a single action. - [Confirmation Dialog](https://firstlightui.dev/docs/components/confirmation-dialog) — Ask for one native confirmation with explicit cancellation and destructive action semantics. - [Modal](https://firstlightui.dev/docs/components/modal) — Present authored content in a full-screen native overlay with server-controlled dismissal. - [Bottom Sheet](https://firstlightui.dev/docs/components/bottom-sheet) — Present authored content in a native bottom sheet with server-controlled dismissal. - [Date Picker](https://firstlightui.dev/docs/components/date-picker) — Choose strict nullable calendar dates with inclusive bounds and native confirmation. - [Time Picker](https://firstlightui.dev/docs/components/time-picker) — Choose strict nullable wall-clock times with localized native confirmation. - [Icon Button](https://firstlightui.dev/docs/components/icon-button) — Configure compact icon-only actions, platform icon overrides, semantic variants, states, and accessibility. - [List Item](https://firstlightui.dev/docs/components/list-item) — Configure tappable application rows with leading identity, trailing affordances, disabled behaviour, and platform icon overrides. - [List](https://firstlightui.dev/docs/components/list) — Compose finite vertical collections of List Item rows with optional grouped sections and refresh or pagination hooks. - [List Section](https://firstlightui.dev/docs/components/list-section) — Group List Item rows with optional headers and footers inside a parent List. - [Pill Group](https://firstlightui.dev/docs/components/pill-group) — Configure compact single- or multiple-selection native options. - [Progress](https://firstlightui.dev/docs/components/progress) — Communicate determinate or indeterminate work with strict values and native accessibility. - [Segmented](https://firstlightui.dev/docs/components/segmented) — Look up the Segmented control's complete public contract and platform evidence. - [Search Field](https://firstlightui.dev/docs/components/search-field) — Configure native query entry, clear, submission, and synchronisation behaviour. - [Select](https://firstlightui.dev/docs/components/select) — Choose one stable string or integer value with automatic searchable presentation for larger option sets. - [Slider](https://firstlightui.dev/docs/components/slider) — Choose one strictly validated numeric value from an evenly spaced native range. - [Stepper](https://firstlightui.dev/docs/components/stepper) — Increment or decrement an exact bounded number while PHP remains authoritative. - [Status Label](https://firstlightui.dev/docs/components/status-label) — Look up display-only status text, semantic tones, accessibility, and platform evidence. - [Text Field](https://firstlightui.dev/docs/components/text-field) — Configure native single-line editing, input hints, icons, and synchronisation. - [Text Area](https://firstlightui.dev/docs/components/text-area) — Configure native multiline editing, line bounds, validation, accessibility, and synchronisation. - [Switch](https://firstlightui.dev/docs/components/switch) — Configure native boolean settings, server-authoritative state, and accessibility. - [ValidatesFields](https://firstlightui.dev/docs/reference/validates-fields) — Look up the public `validate()` contract, MessageBag binding, and current limitations. - [Compatibility](https://firstlightui.dev/docs/reference/compatibility) — Check the currently supported PHP, NativePHP, iOS, Android, and Swift versions. --- # Install Firstlight UI Source: https://firstlightui.dev/docs/getting-started/installation ## Requirements Start with a NativePHP Mobile 4 application that meets the [current compatibility requirements](https://firstlightui.dev/docs/reference/compatibility). Firstlight is still pre-alpha: no released NativePHP runtime currently passes its state-reconciliation release gate. Review the documented [runtime limitation](https://firstlightui.dev/docs/reference/compatibility#pre-alpha-runtime-limitation) before installing. The development fork and patched Android binary are not supported production dependencies. ## Install and register the plugin Install Firstlight as a normal Composer dependency: ```bash composer require firstlightui/nativephp ``` Laravel discovers `FirstlightUI\FirstlightServiceProvider` from the package metadata. Register the native plugin explicitly so NativePHP adds the renderers declared in `nativephp.json` to the application: ```bash php artisan native:plugin:register ``` The command discovers unregistered plugins. If your application disables Laravel package discovery, register `FirstlightUI\FirstlightServiceProvider` through the application's normal provider configuration as well. ## Rebuild the native projects Native renderer source is compiled into the host application. Regenerate the native projects after installing or updating Firstlight: ```bash php artisan native:install --force ``` Build and run the application through your normal NativePHP workflow. Firstlight's `` tags are collected as native elements only in a native render; an ordinary web render leaves them unchanged. ## Next step Add [your first Firstlight component](https://firstlightui.dev/docs/getting-started/first-component) to your application. --- # Add your first Firstlight component Source: https://firstlightui.dev/docs/getting-started/first-component This tutorial adds a native Segmented control for choosing a queue. The same EDGE markup renders as a platform-appropriate control on iOS and Android. ## Define the state Add the selected value and stable value-to-label options to your EDGE component: ```php public string $queue = 'mine'; public array $queueOptions = [ 'mine' => 'Mine', 'all' => 'All', ]; ``` The array keys are the public values stored in `$queue`; the labels are display text. ## Render the control Bind the property with `native:model`: ```blade ``` Run the application natively. A selection emits its stable value to PHP, and the published `$queue` value determines the visible selection. ## Handle a selection explicitly Use `@change` when the selection should call a method instead of directly synchronising a property: ```blade ``` The method receives the selected string value. See the [Segmented reference](https://firstlightui.dev/docs/components/segmented) for integer values, disabled choices, accessibility, and validation rules. When the screen should show Laravel messages on Firstlight fields, follow [Validate fields](https://firstlightui.dev/docs/how-to/validate-fields). --- # Validate Firstlight fields Source: https://firstlightui.dev/docs/how-to/validate-fields NativePHP EDGE has no built-in `validate()`. Firstlight adds a Livewire-shaped Laravel Validator layer. A screen that opts in can call `$this->validate()` or `$this->validateOnly()`, and participating fields show the first `MessageBag` message in the native `error` slot. Do not place `@nativeError` next to Firstlight fields. That directive emits a sibling text node and skips native field error colour, helper replacement, and accessibility. Use `ValidatesFields`, `error-for`, or an authored `error` instead. The method contract, participating fields, and current limitations are listed in the [ValidatesFields reference](https://firstlightui.dev/docs/reference/validates-fields). ## Opt in Use the trait on a NativePHP `NativeComponent`, or extend the thin `FirstlightUI\NativeComponent` subclass that already includes it: ```php use FirstlightUI\Concerns\ValidatesFields; use Native\Mobile\Edge\NativeComponent; class SignupScreen extends NativeComponent { use ValidatesFields; } ``` ```php use FirstlightUI\NativeComponent; class SignupScreen extends NativeComponent { } ``` ## Declare rules and bind fields Rule keys must match public property names (`email` ↔ `native:model="email"`). Nested and wildcard keys are not supported. ```php public string $name = ''; public string $email = ''; public bool $accepted = false; /** @var array */ protected array $rules = [ 'name' => 'required|min:2', 'email' => 'required|email', 'accepted' => 'accepted', ]; /** @var array */ protected array $validationAttributes = [ 'accepted' => 'terms', ]; public function updatedEmail(): void { $this->validateOnly('email'); } public function save(): void { $this->validate(); } ``` ```blade ``` No `:error` wiring is required. After a failed `validate()` or `validateOnly()`, the next published frame fills each field's `error` from `MessageBag::first($field)`. `required` on a field is display metadata only; Laravel `required`, `accepted`, and other rules remain the enforcement. You do not wrap `@press` handlers in `try`/`catch`. Firstlight stores the exception's bag and republishes the screen with field messages. ## Validate one field on blur or live `updated{Property}` runs when PHP accepts that property. Pair it with the sync mode that should trigger the check. Blur publishes on focus loss, then validates only that key: ```blade ``` ```php public function updatedEmail(): void { $this->validateOnly('email'); } ``` Live publishes while editing: ```blade ``` `validateOnly('email')` replaces only the `email` messages. Other keys already in the bag stay put. A passing `validateOnly()` clears that key. You can call `validateOnly()` from `@change` or `@submit` instead of `updated{Property}` when the field should invoke a named method. ## Use a Form Request Pass a class name to `validate()` or `validateOnly()`. Firstlight reads that object's `rules()`, `messages()`, and `attributes()` and runs them against the screen's public properties. It does not perform an HTTP redirect. ```php use Illuminate\Foundation\Http\FormRequest; class StoreSignupRequest extends FormRequest { public function rules(): array { return [ 'name' => 'required|min:2', 'email' => 'required|email', 'accepted' => 'accepted', ]; } public function messages(): array { return [ 'email.required' => 'Need an email address.', ]; } public function attributes(): array { return [ 'accepted' => 'terms', ]; } } ``` ```php public function save(): void { $this->validate(StoreSignupRequest::class); } ``` `authorize()` and other Form Request HTTP behaviour are unused. You can also pass a plain class that implements those three methods. ## Bind a different MessageBag key When the rule key differs from the model name, set `error-for`: ```blade ``` ```php protected array $rules = [ 'contact_email' => 'required|email', ]; ``` `error-for` wins over `native:model` when both are present. ## Let an authored error win A non-empty authored `error` attribute wins over the MessageBag. Use it for application-specific copy that should not come from Laravel rules: ```blade ``` An empty authored `error` is treated as unset, so the bag can still fill the slot. You can also bind explicitly from the `$errorBag` the trait shares with the view: ```blade ``` ## Add and clear messages Use the same bag for server-side checks that are not Laravel rules: ```php public function save(): void { $this->validate(); if ($this->emailIsTaken()) { $this->addError('email', 'This account is already registered.'); return; } $this->resetValidation(); } ``` `resetValidation('email')` forgets one key. `resetValidation()` clears the bag. `hasError('email')` and `getErrorBag()` inspect the current messages. ## Do not validate during mount or render `mount()` and `render()` still sit on NativePHP's generic `Throwable` overlay path. Throwing `ValidationException` there paints the red error overlay instead of field messages. Call `validate()` and `validateOnly()` from actions, `@submit` / `@change` handlers, and `updated{Property}` hooks. `addError()` does not throw and is safe from `render()` when a fixture needs a stable failed frame. ## Screenshots These development screenshots were captured from the dedicated `/captures/validate-fields` showcase route. The interactive dogfood screen is `/validation`. | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Failed Laravel field validation on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/validate-fields/ios-light.png) | ![Failed Laravel field validation on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/validate-fields/ios-dark.png) | | Android | ![Failed Laravel field validation on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/validate-fields/android-light.png) | ![Failed Laravel field validation on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/validate-fields/android-dark.png) | --- # Submit Firstlight forms Source: https://firstlightui.dev/docs/how-to/submit-forms Firstlight submits forms through an ordinary Button press and a PHP screen method. There is no `` container, client-side validator, or form schema. ## Declare the screen Extend `FirstlightUI\NativeComponent` to use the form submission and field validation helpers. Declare Laravel rules against the public properties bound by your fields: ```php 'required', 'email' => 'required|email', ]; public function save(): void { $this->submit( action: fn () => Profile::query()->updateOrCreate( ['user_id' => auth()->id()], ['name' => $this->name, 'email' => $this->email], ), successMessage: 'Profile saved', ); } } ``` Screens that extend `Native\Mobile\Edge\NativeComponent` directly can opt in with both `FirstlightUI\Concerns\ValidatesFields` and `FirstlightUI\Concerns\SubmitsForms`. ## Bind the fields and Button Use the existing fields, `native:model`, and Button: ```blade Save profile ``` `submit()` runs `$this->validate()` first. A validation failure returns `false`, skips the callable and success Feedback, and lets `ValidatesFields` publish each first Laravel message through the matching field's native `error` slot. See [Validate Firstlight fields](https://firstlightui.dev/docs/how-to/validate-fields) for rule sources, Form Requests, custom messages, and explicit error binding. The [ValidatesFields reference](https://firstlightui.dev/docs/reference/validates-fields) lists the method contract and participating fields. After successful validation, `submit()` runs the callable and sends `Feedback::success($successMessage)->send()` when the message is non-empty. It then returns `true`. Pass `validate: false` only when the action does not need field validation: ```php $sent = $this->submit( action: fn () => $this->sendVerificationEmail(), successMessage: 'Verification email sent', validate: false, ); ``` Unexpected exceptions from the callable are not converted to Feedback or swallowed. ## Loading and duplicate submissions `$submitting` is `true` while validation and the callable run. Re-entering `submit()` on the same PHP component during that interval returns `false` without validating, calling the action, or sending Feedback. `submit()` always clears `$submitting` in `finally`, so binding Button `loading` or `disabled` to `$this->submitting` does not flash during the default one-request path. NativePHP handles a press and then compiles one Element Tree afterward. A device-visible loading frame requires application code to set `$this->submitting = true` outside `submit()`, publish that tree, and clear the flag on a later request. The helper does not publish an intermediate tree, call `publishPlaceholder()`, or add a client-side loading mechanism. `publishPlaceholder()` belongs to lazy screen mounting, not form submission. --- # Authorize Firstlight actions Source: https://firstlightui.dev/docs/how-to/authorize-actions Extend `FirstlightUI\NativeComponent` (or `use AuthorizesActions`) so a screen can evaluate the current Laravel Gate or Policy before publishing or performing an action: ```php use App\Models\Post; use FirstlightUI\NativeComponent; class PostScreen extends NativeComponent { public Post $post; public bool $confirmingDeletion = false; public function updatePost(): void { if (! $this->authorize('update', $this->post)) { return; } $this->post->save(); } } ``` `allows()` and `denies()` return booleans. `authorize()` also returns a boolean; unlike Laravel's throwing Gate method, a denial publishes danger Feedback with `This action is unauthorized.` and returns `false`. Keep the guard at the start of every PHP action, even when the published control is already hidden or disabled, because authorization can change after a frame is published. ## Choose hide or disable Hide an ordinary action when its absence is unsurprising: ```blade @if ($this->allows('edit', $post)) Edit post @endif ``` Disable an action when preserving its location helps explain the available workflow: ```blade ``` Button, Icon Button, and List Item all accept the existing `disabled` prop. Firstlight does not add a `can` attribute or evaluate authorization in a native renderer. ## Keep destructive actions visible Never silently omit a destructive action. Keep the Button, Icon Button, or List Item visible and disable it when denied. Supporting copy should explain why the action is unavailable when the surrounding interface does not make that clear. ```blade Delete post ``` Confirmation Dialog has no `disabled` or `loading` prop. Authorize before setting its `visible` state, and authorize again when confirmation arrives: ```php public function requestDeletion(): void { if (! $this->authorize('delete', $this->post)) { $this->confirmingDeletion = false; return; } $this->confirmingDeletion = true; } public function deletePost(): void { $this->confirmingDeletion = false; if (! $this->authorize('delete', $this->post)) { return; } $this->post->delete(); } public function cancelDeletion(): void { $this->confirmingDeletion = false; } ``` ```blade ``` The denied request does not open the dialog, and `authorize()` supplies danger Feedback instead of allowing an uncaught `AuthorizationException` to reach NativePHP's error overlay. For deleting rows from an accumulated `$listItems` collection by stable key, use [Destroy list items](https://firstlightui.dev/docs/how-to/destroy-list-items). --- # Paginate Firstlight lists Source: https://firstlightui.dev/docs/how-to/paginate-lists Firstlight paginates lists in PHP. List already exposes `@refresh` and `@end-reached`. The helper accumulates Laravel paginator pages into `$this->listItems` so Blade can render ordinary List Item rows. There is no `` tag and no second scroll-event API. ## Declare the screen Extend `FirstlightUI\NativeComponent` (or `use PaginatesLists`): ```php reload(); } public function reload(): void { $this->refreshList( fn (ListPage $page) => Conversation::query()->latest()->paginate( perPage: 20, page: $page->page, ), ); } public function loadMore(): void { $this->loadMoreList( fn (ListPage $page) => Conversation::query()->latest()->paginate( perPage: 20, page: $page->page, ), ); } } ``` `refreshList()` replaces `$this->listItems` with the first page. `loadMoreList()` appends the next page. Both return `false` when a fetch is already running. `loadMoreList()` also returns `false` without querying when `$this->listHasMore` is `false`. Pass the paginator's `page` from `ListPage`. NativePHP has no HTTP `?page=` query string for these events. ## Render the rows ```blade @if (count($this->listItems) === 0) @endif @foreach ($this->listItems as $conversation) @endforeach ``` Keep `@end-reached` bound even on the last page. Extra events are ignored. Empty copy is ordinary Blade around List. Compose Status Label, Callout, and Button when you need a retry action. ## Cursor pagination `ListPage::$cursor` is `null` on refresh and the previous paginator's next cursor afterward. Pass it through to `cursorPaginate()`: ```php use Illuminate\Pagination\Cursor; $this->refreshList( fn (ListPage $page) => Conversation::query()->latest()->cursorPaginate( perPage: 20, cursor: $page->cursor !== null ? Cursor::fromEncoded($page->cursor) : null, ), ); ``` Use the same callable with `loadMoreList()`. Unexpected exceptions from the query are not converted to Feedback. Screens that extend NativePHP's component directly can opt in with `FirstlightUI\Concerns\PaginatesLists`. To delete a row after Confirmation Dialog authorization, see [Destroy list items](https://firstlightui.dev/docs/how-to/destroy-list-items). --- # Destroy Firstlight list items Source: https://firstlightui.dev/docs/how-to/destroy-list-items Firstlight deletes list rows in PHP. Pass a stable domain key into the helper, confirm with Confirmation Dialog, authorize again, destroy the model, and let `$listItems` republish without that row. There is no swipe-delete API, trailing action slot, or renderer index. See [Authorize actions](https://firstlightui.dev/docs/how-to/authorize-actions) for hide-versus-disable rules and [Paginate lists](https://firstlightui.dev/docs/how-to/paginate-lists) for `$listItems`. ## Declare the screen Extend `FirstlightUI\NativeComponent` (or use `AuthorizesActions`, `DestroysListItems`, and a `$listItems` array): ```php requestDestructiveListAction($id, 'delete'); } public function cancelDeletion(): void { $this->cancelDestructiveListAction(); } public function deletePost(): void { $this->confirmDestructiveListAction( destroy: fn (Post $post) => $post->delete(), ability: 'delete', successMessage: 'Post deleted', ); } } ``` `requestDestructiveListAction()` resolves the row by key from `$listItems`, calls `authorize()`, and sets `$confirmingListDestruction` when allowed. `confirmDestructiveListAction()` closes the dialog, authorizes again, runs the destroy callable, removes the row from `$listItems`, and optionally sends success Feedback. Keys must be stable identities such as model ids. Do not pass loop indexes. ## Keep denied rows visible ```blade @foreach ($this->listItems as $post) @endforeach ``` Never omit a destructive List Item when the Policy denies it. Disable the row and keep supporting copy that explains the unavailable action when the surrounding interface does not already make that clear. ## Custom keys or database resolve Override `destructiveListItemKey()` when rows do not expose `getKey()`, `id`, or an `id` array key. Override `resolveDestructiveListItem()` to reload the model from the database instead of trusting the in-memory `$listItems` copy: ```php protected function resolveDestructiveListItem(mixed $key): mixed { return Post::query()->find($key); } ``` When you resolve from the database, `$listItems` is still updated after a successful destroy so the published List matches the mutation. --- # Localize Firstlight chrome Source: https://firstlightui.dev/docs/how-to/localize Firstlight-owned chrome (Confirm, Cancel, OK, dismiss, clear, skip, Done, crop, zoom, media source chooser, and password reveal) comes from one Laravel language file. Authored labels on Confirmation Dialog and Alert Dialog still win. Date Picker and Time Picker keep rejecting Blade `confirm-label` and `cancel-label`; their sheet buttons are package chrome only. The package ships English. There is no translation UI. Applications add other locales through Laravel vendor language publishing. ## Publish the language files ```bash php artisan vendor:publish --tag=firstlight-lang ``` Laravel copies the files to `lang/vendor/firstlight`. Add a locale next to English, for example `lang/vendor/firstlight/fr/chrome.php`, and keep the same keys: ```php 'Confirmer', 'cancel' => 'Annuler', 'ok' => 'OK', 'dismiss' => 'Fermer', 'dismiss_feedback' => 'Fermer le message', 'clear' => 'Effacer', 'clear_search' => 'Effacer la recherche', 'clear_text' => 'Effacer le texte', 'skip' => 'Ignorer', 'done' => 'OK', 'crop' => 'Recadrer', 'zoom_in' => 'Zoom avant', 'zoom_out' => 'Zoom arrière', 'choose_media' => 'Choisir un média', 'photo_library' => 'Photothèque', 'camera' => 'Appareil photo', 'browse_files' => 'Parcourir les fichiers', 'show_password' => 'Afficher le mot de passe', 'hide_password' => 'Masquer le mot de passe', ]; ``` Missing keys fall back to the packaged English strings. ## Date and Time picker locale and timezone When `locale` or `timezone` is omitted, Date Picker and Time Picker inherit the application: - locale from the translator (`en_AU` and `en_AU.UTF-8` become `en-AU`) - timezone from `config('app.timezone')` when it is a real IANA identifier Explicit picker props still win. Invalid inherited values are omitted so the device calendar and clock are used; they do not throw. Authored malformed `locale` or `timezone` attributes still fail before publication. Authored field labels, helpers, errors, dialog titles, and Feedback messages are ordinary Blade strings; translate those with `__()` as you already would. --- # Theme Firstlight components Source: https://firstlightui.dev/docs/getting-started/theming Firstlight components inherit the semantic theme from `nativephp/mobile-ui`. There is no separate Firstlight theme file: configure the host application once, and the same tokens reach the native SwiftUI and Jetpack Compose renderers. The theme changes colour and typography while each platform retains its native geometry, motion, and interaction. ## Publish the theme configuration Publish `config/native-ui.php` into the application: ```shell php artisan vendor:publish --tag=native-ui-config ``` ## Set the brand colour pairs Set both the background token and its `on-*` content token. This example uses the Firstlight showcase palette; replace the values with accessible pairs from your own brand: ```php 'theme' => [ 'light' => [ 'primary' => '#AE1515', 'on-primary' => '#FFF5EA', ], 'dark' => [ 'primary' => '#ED4E0C', 'on-primary' => '#110805', ], ], ``` The `primary` pair colours filled actions, selected states, and key accents. Surface, outline, destructive, success, accent, and disabled-state colours use their corresponding semantic tokens from the published configuration. Dark values are optional because NativePHP can derive them from the light theme. Define explicit dark overrides when brand perception or contrast needs a tuned pair. Keep text and icons at a contrast ratio of at least 4.5:1 against their background token. Theme colours accept CSS hex values, Tailwind palette names such as `violet-600`, and opacity modifiers such as `violet-600/15`. ## Use theme tokens in surrounding native UI Firstlight controls consume the tokens automatically. Use the same tokens for surrounding `nativephp/mobile-ui` elements with theme-aware utility classes: ```blade Account ``` ## Apply a runtime brand For tenant or user-specific branding, merge only the tokens that change from a service provider: ```php use Native\Mobile\UI\Theme; public function boot(): void { Theme::merge([ 'light' => [ 'primary' => '#AE1515', 'on-primary' => '#FFF5EA', ], 'dark' => [ 'primary' => '#ED4E0C', 'on-primary' => '#110805', ], ]); } ``` `Theme::merge()` deep-merges the new values, synchronises the effective Laravel configuration, and publishes the updated tokens to the native renderers. --- # SuperNative components Source: https://firstlightui.dev/docs/concepts/supernative-components Firstlight is a NativePHP SuperNative UI plugin. It participates in the same EDGE, Element Tree, native renderer, and wire-event lifecycle as NativePHP's own native UI components. ## From EDGE to an Element Tree The Firstlight precompiler maps an authored tag such as `` to its Blade component during a native render. The component declares the `firstlight.segmented` element type. Its PHP element validates authored input, normalises options, and publishes a primitive Element Tree payload. The payload carries stable values, labels, enabled state, field metadata, accessibility text, and callback identifiers. It does not carry SwiftUI or Compose objects and does not introduce a separate JSON bridge. ## Genuine platform renderers The plugin manifest maps the same element type to `SegmentedRenderer` on iOS and `dev.firstlightui.plugins.firstlight_ui.ui.SegmentedRenderer` on Android. The iOS renderer creates a native SwiftUI/UIKit control. The Android renderer creates a Material 3 Jetpack Compose control. The shared API guarantees the component's meaning and behaviour. Each platform retains its own geometry, materials, typography, state layers, motion, and accessibility conventions. ## Semantic events and reconciliation A native interaction sends a semantic change event with the stable public value, not a renderer index or display label. PHP processes that event and publishes the next Element Tree. The renderer reconciles its control from the newly published value. That lifecycle keeps application decisions in PHP while ordinary native interaction and presentation remain on the platform UI thread. Read [server-authoritative state](https://firstlightui.dev/docs/concepts/server-authoritative-state) for the selection timing contract. --- # Server-authoritative state Source: https://firstlightui.dev/docs/concepts/server-authoritative-state Firstlight selection controls treat PHP's published value as authoritative. Native interaction is immediate, but it does not create an independent source of truth. ## Interaction and acceptance When a person selects an enabled option, the native renderer immediately sends the option's stable value through the component callback. PHP may accept it by publishing that value, transform it according to application rules, or reject it by retaining the existing value. The renderer reconciles from every resulting Element Tree publication. An accepted value becomes selected. A rejected value returns to, or remains at, the last published selection. Reconciliation itself never emits another change event. ## Repeated attempts and programmatic changes Each user attempt is semantic. If PHP rejects a choice, selecting that choice again sends another event rather than being suppressed as a duplicate of the rejected attempt. A programmatic PHP update changes the published selection without masquerading as a user interaction. It therefore updates the native control without firing `@change` back to PHP. ## No selection `null` means no selection. It is distinct from an authored empty-string value. A same-typed value that is not present in the current options also renders with no selected segment; Firstlight never silently selects the first option. This timing is why Segmented accepts plain `native:model` and `native:model.live`, but rejects deferred `blur` and `debounce` sync modes. --- # How Firstlight complements NativePHP Mobile UI Source: https://firstlightui.dev/docs/concepts/firstlight-and-mobile-ui NativePHP Mobile UI is the broad native UI foundation for NativePHP applications. It supplies layout, navigation, lists, presentation, media, gestures, form controls, semantic theming, and the official SwiftUI and Jetpack Compose integration that Firstlight builds upon. Firstlight is not a replacement for Mobile UI or an attempt to reproduce its catalogue. It depends on Mobile UI, follows its EDGE conventions, inherits its semantic theme, and uses the same SuperNative Element Tree and event lifecycle. Firstlight focuses on a smaller set of form and action controls that benefit from a more opinionated product-level contract. ## The difference at a glance | NativePHP Mobile UI | Firstlight UI | | --- | --- | | A broad toolkit of native primitives and application building blocks | A curated layer of form, selection, display, and action controls | | Flexible primitives that can support many compositions | Purpose-specific components with narrower, consistent contracts | | The official native renderer, theme, and extension foundation | Stable values, strict validation, server-authoritative state, field semantics, and cross-component consistency | | Platform-native SwiftUI and Material 3 implementation | The same native-first approach, with paired renderers where a higher-level contract needs them | Mobile UI makes Firstlight possible. Firstlight adds a design-system layer for teams that want common form behaviour to be decided once rather than assembled separately in every application. ## Additional dedicated components Mobile UI provides primitives from which many interfaces can be composed. Firstlight adds dedicated components where one reusable semantic contract is valuable: - **Icon Button** uses the compact native icon-button family, requires an accessible action name, guarantees minimum interaction targets, and supports disabled and loading states. Mobile UI provides capable Button and Icon primitives, but does not expose this complete contract as a standalone manifest component. - **Search Field** makes native search behaviour an invariant. It owns the platform search and clear affordances, submit flushing, focused query state, and accessible clear action instead of requiring an application to configure a general text input each time. - **Stepper** provides exact bounded increment and decrement over integer or floating-point grids. It uses SwiftUI `Stepper` on iOS and an idiomatic Material 3 decrement/value/increment composition on Android. - **Status Label** provides a display-only status capsule with semantic tones, accessible static-text semantics, text scaling, and contrast protection. It avoids giving non-interactive status metadata the behaviour of a button or selectable chip. ## Higher-level components over existing primitives Some Firstlight components package several Mobile UI capabilities into one stable application-facing value: - **Choice Group** presents single-choice radio rows or multiple-choice checkbox rows through one option-array API and one complete scalar-or-list value. - **Pill Group** composes native chip intents into one single- or multiple-selection field. The group publishes one stable scalar or list rather than requiring a separate Boolean model for every chip. - **Text Area** turns multiline editing into a dedicated, deliberately narrow field with native focused-state preservation, line bounds, validation feedback, and a genuine SwiftUI `TextEditor` expression on iOS. - **Time Picker** separates nullable `HH:mm` selection from a combined date/time/datetime API and gives it the same explicit draft, confirmation, validation, and accepted-value rules as the rest of Firstlight. These are not claims that Mobile UI cannot build the corresponding interface. Firstlight supplies the reusable product contract so each consuming application does not need to design that contract independently. ## Richer contracts around familiar controls Firstlight also provides opinionated versions of controls that have clear Mobile UI counterparts: | Firstlight component | Additional Firstlight contract | | --- | --- | | **Segmented** | Stable string or integer values instead of renderer indexes, nullable selection, rich options, individual disabled choices, field metadata, strict diagnostics, and PHP-authoritative acceptance. | | **Badge** | Exactly one count or label, hidden zero, contextual accessibility for numeric counts, five semantic tones, strict authoring diagnostics, and a Material 3 `Badge` on Android. | | **Text Field** | A single semantic field API with content and autofill hints, platform icon overrides, accessible trailing actions, clear and reveal affordances, and focused editing reconciliation that preserves selection and text composition. | | **Date Picker** | A strict nullable `YYYY-MM-DD` value, inclusive bound validation, a temporary native draft, explicit confirmation, and a closed trigger that continues to display PHP's accepted value. | | **Select** | Stable string or integer values, rich and individually disabled options, automatic searchable presentation for larger collections, field validation semantics, and PHP-authoritative selection. | | **Slider** | Strict finite-number and step-grid validation, helper and error treatment, an optional spoken accessibility value, and explicit live, blur, or debounce synchronisation. | | **Switch** | Consistent helper and error semantics, strict Boolean values, one accessible setting row, and visible state reconciled from the value accepted by PHP. | ## Server-authoritative application state For discrete Firstlight controls, a native interaction proposes a semantic value and PHP publishes the accepted state. PHP may accept, transform, or reject the proposal. Reconciliation updates the control without echoing another event, and programmatic updates never masquerade as user interaction. This differs from treating a local optimistic change as accepted application state. It is useful when permissions, validation, dependent fields, or business rules can reject or replace a choice. Focused text and continuous gestures still retain the native draft state needed for responsive editing and movement; Firstlight applies the state model appropriate to each type of control. ## Consistency and diagnostics Across the catalogue, Firstlight applies the same rules: - selection binds stable domain strings or integers rather than display labels or renderer indexes; - `null` consistently represents no selection; - rich options can separate values, labels, and disabled state; - malformed options, duplicate or mixed values, impossible ranges, contradictory props, and unsupported sync modes fail before publication; - labels, helper text, errors, required metadata, accessibility hints, disabled state, and minimum interaction targets follow familiar conventions; - the same authored component works on iOS and Android while each platform retains its native geometry, motion, presentation, and accessibility behaviour. Firstlight deliberately exposes fewer visual escape hatches and platform-only options than a general-purpose primitive library. This narrower surface keeps application code portable and allows the components to behave consistently as the native platforms evolve. ## Laravel-shaped PHP extensions Some Firstlight behaviour is not a new control. Field validation (`ValidatesFields`) runs Laravel's `Validator` in PHP and publishes the first `MessageBag` message into each field's existing `error` slot. Form submission (`SubmitsForms`) validates, runs a PHP callable once, and optionally sends success Feedback. Action authorization (`AuthorizesActions`) evaluates Laravel Gate/Policy decisions for hide, disable, and action-time guards. List pagination (`PaginatesLists`) binds Laravel paginators to List `@refresh` and `@end-reached`. Destructive list actions (`DestroysListItems`) authorize Confirmation Dialog deletion by stable keys and republish `$listItems`. Package locale chrome publishes Firstlight-owned chrome strings from one Laravel language file, and Date/Time pickers inherit omitted locale and timezone from the application. Transient Feedback is an application-level outcome queue, not a Blade field. See [Validate fields](https://firstlightui.dev/docs/how-to/validate-fields), [Submit forms](https://firstlightui.dev/docs/how-to/submit-forms), [Authorize actions](https://firstlightui.dev/docs/how-to/authorize-actions), [Paginate lists](https://firstlightui.dev/docs/how-to/paginate-lists), [Destroy list items](https://firstlightui.dev/docs/how-to/destroy-list-items), and [Localize chrome](https://firstlightui.dev/docs/how-to/localize). Layout, typography, navigation chrome, and generic media stay in Mobile UI. Firstlight does not ship columns, bottom tabs, input masks, or a schema/form builder. Compose screens with Blade, Mobile UI layout, and Firstlight fields. ## Reusing Mobile UI when it already fits Firstlight does not maintain duplicate native code merely to put every control under its own namespace. **Button** and **Progress** currently keep public Firstlight element types while delegating directly to the official Mobile UI SwiftUI and Material 3 renderers. Firstlight adds strict authoring boundaries, documentation, tests, and catalogue consistency around those primitives, but does not claim new native rendering functionality. If a durable cross-platform requirement later outgrows an adapter, Firstlight can introduce paired renderers without changing consumer markup. Generally useful improvements remain good candidates for contribution to Mobile UI itself. ## One native ecosystem Firstlight components use ordinary NativePHP UI component extension points. They publish Element Tree data, receive standard semantic events, inherit Mobile UI theme tokens, and require no custom imperative bridge functions or WebView-backed control layer. The relationship is intentionally complementary: **Mobile UI provides the comprehensive native foundation; Firstlight provides a curated, evidence-backed form and control system on top of it.** Review the current [compatibility requirements](https://firstlightui.dev/docs/reference/compatibility) before releasing an application, particularly where server-authoritative controls depend on unchanged-tree publication acknowledgements from NativePHP Mobile. --- # Activity Indicator Source: https://firstlightui.dev/docs/components/activity-indicator Activity Indicator communicates that work is active when no meaningful completion value is available. It renders a circular, indeterminate native indicator with no visible text inside the component. ## Complete example Render the component only while the work is active: ```blade @if ($loading) @endif ``` Presence is the activity state. Removing the element stops the presentation; the component has no separate `loading`, `active`, or `visible` prop. Use the same conditional server state that decides whether the work is active rather than attaching `wire:loading` to this native element. ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `size` | `sm`, `md`, or `lg` | Semantic native size. Defaults to `md`. | | `a11y-label` | non-empty `string` | Required accessible name describing the active work. The `a11yLabel` authoring alias is also accepted. | | `class` | `string` | External EDGE layout utilities. | Size is semantic, not a shared point or dp measurement. Firstlight maps each value to an appropriate native size on each platform. Colour comes from the host NativePHP theme's primary token. Activity Indicator has no `color`, `tone`, or `variant` escape prop. ## Visible text `a11y-label` is not displayed. Compose separate visible text when sighted users also need context: ```blade Refreshing appointments… ``` The visible text and accessible name may use the same wording, but they remain separate responsibilities. Activity Indicator is self-closing and has no label or content slot. ## Events and state timing Activity Indicator has no event, callback, or model binding. PHP publishes the element while work is active, and the native control owns its indeterminate animation without timer callbacks or animation frames crossing the bridge. On a new mount, assistive technology receives one polite announcement using the authored `a11y-label`. Ordinary server reconciliation, size changes, label changes, and body recomputation do not repeat that announcement. Removing the element and later adding a new mounted element begins a new appearance. ## Disabled, loading, and failure behaviour The component is display-only, so disabled, selected, pressed, error, helper, required, and validation states do not apply. Its presence already means loading; a second loading flag would create contradictory state. A missing, empty, whitespace-only, or non-string `a11y-label` throws an `InvalidArgumentException`. Unsupported sizes and content, state, event, styling, hint, value, or synchronization attributes also fail before the Element Tree is published. Firstlight does not silently coerce `small`, `large`, `xs`, `xl`, integers, booleans, arrays, or `null` into a size. ## Accessibility The required label names the work rather than the generic visual, for example `Loading appointments` instead of `Spinner`. The indicator has no accessibility hint, percentage, click action, or interactive role. The polite appearance announcement does not move screen-reader focus. VoiceOver receives the SwiftUI control's accessible label plus one guarded appearance announcement. TalkBack receives the label as its content description and the indicator as a polite live region. Both retain native contrast, right-to-left behaviour, Increased Contrast, and Reduced Motion or system animation policy. ## Platform behaviour iOS uses a genuine circular SwiftUI `ProgressView`, native `ControlSize` mapping, and the host theme's primary colour. Android uses Material 3 `CircularProgressIndicator` at Material-appropriate `20.dp`, `32.dp`, and `48.dp` dimensions for `sm`, `md`, and `lg`. The platforms retain their own geometry and animation. The shared guarantee is purpose, semantic size order, theme intent, accessibility, and lifecycle—not pixel parity. ## Activity Indicator or Progress? Use Activity Indicator for circular, indeterminate activity whose completion cannot be measured. Use [Progress](https://firstlightui.dev/docs/components/progress) when you have a fraction from `0.0` through `1.0`, or when a linear progress treatment better communicates the task. Neither component starts or controls the underlying work. ## Compatibility Activity Indicator supports the versions listed in the current [compatibility reference](https://firstlightui.dev/docs/reference/compatibility). The host application must compile both Firstlight-owned native renderers declared by the package manifest. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Activity Indicator on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/activity-indicator/ios-light.png) | ![Activity Indicator on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/activity-indicator/ios-dark.png) | | Android | ![Activity Indicator on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/activity-indicator/android-light.png) | ![Activity Indicator on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/activity-indicator/android-dark.png) | --- # Button Source: https://firstlightui.dev/docs/components/button Button performs one immediate, labelled action. Firstlight includes it because a coherent UI library is expected to provide its foundational action control under the same `` namespace and semantic conventions as the rest of the catalogue. The current component deliberately wraps the adequate `nativephp/mobile-ui` Button. It keeps a stable Firstlight API while reusing the official SwiftUI and Material 3 renderers instead of maintaining duplicate native code. ## Complete example ```blade Save changes ``` The same authored tag works on iOS and Android. A self-closing form with a `label` prop is also accepted, but the text slot is preferred for ordinary Blade markup. ## Props | API | Accepted type | Purpose | | --- | --- | --- | | Text slot / `label` | non-empty `string` | Visible button text. Required. | | `variant` | `primary`, `secondary`, `destructive`, `success`, or `ghost` | Semantic action intent. Defaults to `primary`. | | `size` | `sm`, `md`, or `lg` | Native control size. Defaults to `md`. | | `disabled` | `bool` | Prevents interaction and exposes native disabled state. Defaults to `false`. | | `loading` | `bool` | Shows native progress presentation and prevents interaction. Defaults to `false`. | | `icon` | `string` | Optional leading cross-platform icon name. | | `icon-trailing` | `string` | Optional trailing cross-platform icon name. | | `a11y-label` | `string` | Replaces the visible text as the accessible name. | | `a11y-hint` | `string` | Adds supplementary VoiceOver or TalkBack context. | | `class` | `string` | External EDGE layout utilities. | Button always has visible text. Use Icon Button for an icon-only action. ## Events and values `@press` invokes the named PHP action once for a native user press. Button has no bound value, options, `native:model`, or `@change` event. ## State timing A press emits immediately. The native button keeps only transient platform press feedback; PHP owns the action result and publishes any subsequent `loading`, `disabled`, label, or layout state. Programmatic publications do not emit a press. ## Disabled, loading, and failure behaviour Disabled and loading buttons do not emit. Loading replaces the ordinary content with native progress presentation while retaining the action's accessible context. A missing, `null`, empty, or whitespace-only label throws an `InvalidArgumentException`. `disabled` and `loading` require real booleans. Unsupported variants or sizes also throw. Attached menus, custom typography, Liquid Glass classes, navigation directives, field props or bindings, long press, double tap, press-down, and press-up callbacks are outside this component's contract and fail rather than silently widening the API. ## Accessibility The visible text is the accessible name by default. `a11y-label` replaces it, and `a11y-hint` adds context. The native renderers expose the button role, disabled state, press interaction, Dynamic Type or system font scaling, platform focus behaviour, and platform target sizing. Android also announces loading as a state description. ## Platform behaviour iOS uses the official Mobile UI SwiftUI `Button` renderer with native button styles, control sizes, progress presentation, icon layout, VoiceOver metadata, and semantic theme tokens. Android uses the official Mobile UI Material 3 `Button`, `FilledTonalButton`, or `TextButton` renderer with native state layers, progress presentation, icon layout, TalkBack metadata, and semantic theme tokens. The shared variants express the same intent without forcing pixel parity. `destructive` is currently a semantic visual variant; Firstlight does not expose an Apple-only action-role prop. ## Why an adapter? The official Mobile UI primitive already satisfies this contract. Firstlight therefore wraps it for catalogue and namespace consistency instead of rebuilding it. The public type remains `firstlight.button`, so Firstlight can move to package-owned renderers later without changing consumer markup if a durable cross-platform requirement genuinely outgrows Mobile UI. Generally useful renderer improvements should be contributed upstream where practical. ## Compatibility Button supports the versions listed in the current [compatibility reference](https://firstlightui.dev/docs/reference/compatibility). The host application must compile the installed `nativephp/mobile-ui` iOS and Android renderers that the Firstlight adapter declares. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Button on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/button/ios-light.png) | ![Button on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/button/ios-dark.png) | | Android | ![Button on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/button/android-light.png) | ![Button on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/button/android-dark.png) | --- # Callout Source: https://firstlightui.dev/docs/components/callout Callout keeps an important semantic message visible in the page layout. It may include one labelled action, but it does not dismiss itself, expire, or enter a feedback queue. ## Complete example ```blade ``` The same tag renders as an idiomatic rounded SwiftUI message surface on iOS and a Material 3 surface on Android. ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `message` | non-empty `string` | Required visible message. | | `tone` | `neutral`, `info`, `success`, `warning`, or `danger` | Semantic intent. Defaults to `info`. | | `action-label` | non-empty `string` | Optional visible action label. Requires `@press`. The `actionLabel` alias is also accepted. | | `a11y-label` | `string` | Replaces the generated accessible message. | | `a11y-hint` | `string` | Adds supplementary context to the accessible message. | | `class` | `string` | External EDGE layout utilities. | Callout owns its semantic symbol. There are no `icon`, `icon-ios`, or `icon-android` props because changing that affordance could contradict the tone. ## Events and state timing `@press` is the one optional standard action event. It must be paired with `action-label` and fires once for each completed native button activation. Callout has no value or model. PHP controls whether it is present and may publish new message, tone, action, or accessibility metadata. Native reconciliation updates the existing stable node without emitting an event. ## Persistence and dismissal Callout remains in its authored position until the next Element Tree omits it. It has no timeout, close button, swipe gesture, `dismissible` prop, or dismissal event. Use Transient Feedback for brief queued outcomes, Alert Dialog when the user must acknowledge one message, and Confirmation Dialog when work must pause for a confirm/cancel decision. ## Disabled, loading, and error behaviour Disabled and loading states do not apply. Render or omit the action according to application state, and publish the semantic tone that describes the message. Although `danger` can communicate an error outcome, Callout is not a field-validation control and does not accept `error`, `helper`, or `required`. ## Accessibility Tone is conveyed by a distinct native symbol and by the generated accessible name, such as `Warning: Your changes have not been submitted.` The icon is decorative to VoiceOver and TalkBack. `a11y-label` replaces the generated name, while `a11y-hint` adds context. The optional action is a separate native button whose visible text is its accessible name. Its target is at least 44 points on iOS and 48 dp on Android. Message text wraps and surfaces grow for Dynamic Type, Android font scaling, and long localisation. Callout is not a live region; persistent content does not automatically interrupt the screen reader. ## Validation and failure behaviour A missing, empty, whitespace-only, or non-string `message` throws an `InvalidArgumentException`. Unsupported or non-string tones fail and list the accepted values. `action-label` without `@press`, `@press` without `action-label`, and blank action labels fail before publication. Firstlight also rejects model, change, dismissal, title, icon, disabled, loading, validation, navigation, long-press, and visual escape attributes instead of silently inventing behavior. If malformed data reaches a native renderer, it falls back to `info` and suppresses an incomplete action without crashing. ## Platform behaviour iOS composes SwiftUI `Image`, `Text`, and `Button` with platform typography, a continuous rounded rectangle, VoiceOver containment, and native Dynamic Type. Android composes a Material 3 `Surface`, icon, text, and `TextButton` with Material typography and TalkBack semantics. Both inherit NativePHP semantic theme tokens and preserve native dark mode, increased contrast, focus, activation feedback, and right-to-left layout rather than forcing pixel parity. ## Compatibility Callout supports the versions listed in the current [compatibility reference](https://firstlightui.dev/docs/reference/compatibility). The host application must compile both Firstlight-owned native renderers declared by the package manifest. ## Screenshots These development screenshots were captured from the dedicated `/captures/callout` showcase route on an iPhone 16 Pro Simulator running iOS 18.6 and the `Pixel_9_Pro` Android emulator. | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Callout on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/callout/ios-light.png) | ![Callout on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/callout/ios-dark.png) | | Android | ![Callout on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/callout/android-light.png) | ![Callout on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/callout/android-dark.png) | --- # Transient Feedback Source: https://firstlightui.dev/docs/components/transient-feedback ```php use FirstlightUI\Facades\Feedback; $feedbackId = Feedback::success('Appointment saved') ->action('Undo', 'undo-save') ->send(); ``` This publishes one app-level native message and returns its stable string ID. There is no Blade tag and no host installation step: installing Firstlight registers the package-owned feedback host automatically. Transient Feedback is for brief outcomes that may cross screen navigation. Use [Callout](https://firstlightui.dev/docs/components/callout) for persistent content in the authored layout, [Alert Dialog](https://firstlightui.dev/docs/components/alert-dialog) when the user must acknowledge one message, and [Confirmation Dialog](https://firstlightui.dev/docs/components/confirmation-dialog) when work must pause for a confirm/cancel decision. ## Factories and builder The public identity is `FirstlightUI\Facades\Feedback`. | API | Return | Contract | | --- | --- | --- | | `Feedback::message(string $message)` | `PendingFeedback` | Creates default-tone feedback. | | `Feedback::success(string $message)` | `PendingFeedback` | Creates success feedback. | | `Feedback::warning(string $message)` | `PendingFeedback` | Creates warning feedback. | | `Feedback::danger(string $message)` | `PendingFeedback` | Creates danger feedback. | | `->id(string $id)` | new `PendingFeedback` | Uses an application-owned stable ID. | | `->action(string $label, string $key)` | new `PendingFeedback` | Adds one visible labelled action and its application key. | | `->hold()` | new `PendingFeedback` | Keeps the item until action, manual dismissal, or programmatic removal. | | `->send()` | `string` | Publishes the item and returns its authored or generated ID. | | `Feedback::dismiss(string $id)` | `bool` | Removes a pending or visible item; returns whether it existed. | The builder is immutable. Calling a modifier returns a new builder and does not change the instance it was called on. When `id()` is omitted, `send()` generates a UUID string. ## Tones The four factories are the complete tone API: - `message()` uses `default` for neutral application information. - `success()` communicates a completed outcome. - `warning()` communicates a recoverable risk or interruption. - `danger()` communicates a failed or destructive outcome. Tone controls native colour and a decorative semantic symbol. It does not change queue, timing, or event behaviour. ## Application events Register normal Laravel listeners in a service provider when the application needs to react to user outcomes: ```php use FirstlightUI\Events\FeedbackActionPressed; use FirstlightUI\Events\FeedbackDismissed; use Illuminate\Support\Facades\Event; public function boot(): void { Event::listen(function (FeedbackActionPressed $event): void { // $event->id and $event->actionKey }); Event::listen(function (FeedbackDismissed $event): void { // $event->id and $event->reason }); } ``` `FeedbackActionPressed` has public readonly `string $id` and `string $actionKey` properties. `FeedbackDismissed` has public readonly `string $id` and `FeedbackDismissReason $reason`. The dismissal reasons are `Timeout`, `Manual`, and `Action`, with wire values `timeout`, `manual`, and `action`. Pressing an action removes the item, dispatches `FeedbackActionPressed`, then dispatches `FeedbackDismissed` with `FeedbackDismissReason::Action`. The dismissal event is still dispatched if an action listener throws, and the listener exception is not swallowed. An automatic timeout dispatches only `FeedbackDismissed` with `FeedbackDismissReason::Timeout`. A manual native dismissal dispatches only `FeedbackDismissed` with `FeedbackDismissReason::Manual`. `Feedback::dismiss($id)` is programmatic reconciliation. It dispatches neither event, including when it removes the visible item. ## Queue and stable-ID updates Feedback is displayed one item at a time in first-in, first-out order. Sending a new ID appends it. Sending an existing ID replaces that semantic record in place, so it keeps its current queue position: ```php Feedback::message('Connecting')->id('connection')->send(); Feedback::warning('Connection interrupted')->id('connection')->hold()->send(); ``` An update can change message, tone, action, and held state. Updating the visible ID does not restart elapsed automatic time; the native host recalculates the allowed duration from the current content and accessibility policy. Completed IDs are protected from stale native frames and cannot dismiss or act on the next item. Use a stable authored ID when later code must update or remove an item. Use the generated ID returned by `send()` for one-off feedback. ## Lifetime and dismissal Automatic feedback starts with a four-second minimum. Longer text may increase message time up to ten seconds, and an action adds two seconds. Platform accessibility policy may extend that duration. Automatic feedback has no visible dismiss control. `hold()` disables automatic timeout. A held item presents an explicit native dismiss control with the accessible label `Dismiss feedback`; its optional application action remains available. Either user action advances the queue. `Feedback::dismiss()` removes held and automatic items alike without an application event. There is no public duration, position, animation, swipe, close-label, or stacking option. ## Navigation, background, and process lifecycle The feedback store and native host belong to the installed package, not the screen that called the facade. Pending and visible items therefore survive normal NativePHP screen navigation. Each publication refreshes package-owned native callback IDs, so an outcome routes to the feedback service rather than to a screen that has been replaced. Automatic time counts only while the app is active and its feedback controls do not hold accessibility focus. Moving the app to the background pauses the remaining duration; returning to the foreground resumes it. Held items do not accrue timeout time. The store is process-local memory. Process termination clears the queue; Transient Feedback is not durable storage and does not restore messages after relaunch. ## Accessibility Each newly visible semantic ID is announced once. Updating the same ID changes the visible content without repeating the announcement. Tone symbols are hidden from VoiceOver and TalkBack, while message text and labelled actions retain native semantics. Actions meet the platform baseline of at least 44 points on iOS and 48 dp on Android. Long text wraps, controls reflow under constrained width or large text, and both platforms preserve dark mode, contrast, and right-to-left layout. Automatic timing pauses while an action owns accessibility focus. iOS doubles the automatic duration while VoiceOver or Switch Control is active and uses an opacity-only transition with Reduce Motion. Android passes the content type and base duration to `AccessibilityManager.getRecommendedTimeoutMillis`, available at the package's API 29 floor, and retains Material snackbar accessibility semantics. ## Validation and failure behaviour Messages passed to a factory and values passed to `id()` or `action()` must be non-empty after trimming. Invalid values throw `InvalidArgumentException` before publication; surrounding non-blank whitespace is preserved. The action label and key are always authored together through `action()`. `Feedback::dismiss()` returns `false` for an unknown ID, including a blank ID, rather than publishing an event. Defensive native decoding treats incomplete native action metadata as no action; an otherwise eligible item remains visible without an action. Blank or invalid identity, message, or tone, duplicate IDs in one frame, and a missing required lifetime callback make an item ineligible. Repeated or stale callbacks produce no duplicate events. Disabled, loading, model binding, rich content, multiple actions, icons, per-platform copy, arbitrary colours, and renderer-specific styling do not apply. There is no consumer-authored feedback-center host and no supported direct construction of the internal wire elements. ## Platform behaviour iOS presents a bottom SwiftUI material notice with semantic SF Symbols, native buttons, safe-area spacing, Dynamic Type, and a reduced-motion-aware transition. Android presents a bottom Material 3 `Snackbar` with semantic tone treatment, navigation-bar and IME padding, responsive action layout, and TalkBack live-region behaviour. Both platforms own one visible item and the same FIFO/event contract. Native expression differs by platform; Firstlight guarantees behavioural parity rather than identical geometry. Held items expose a dismiss control whose visible and accessibility labels are package chrome; see [Localize chrome](https://firstlightui.dev/docs/how-to/localize). ## Compatibility Transient Feedback supports the package versions and platform floors in the current [compatibility reference](https://firstlightui.dev/docs/reference/compatibility): PHP 8.4, NativePHP Mobile 4, NativePHP Mobile UI 0.3, iOS 18 or later, and Android API 29 or later. Both Firstlight native hosts must be compiled into the application. ## Screenshots The screenshot manifest reserves `/captures/transient-feedback` and the four paths below. `bin/check-transient-feedback --development` reports missing showcase, image, and review evidence without blocking documentation work. Release mode requires clean package and showcase Git worktrees, the exact Task 6 route/component/view/test fixture, structural assertions for the capture screen and deterministic feedback record, and a passing manifest-focused test. That test runs with a short timeout and continuously drained 4,000-byte diagnostic ceiling. The gate incrementally decodes each differentiated PNG, including bounded files and chunks, CRCs, streamed IDAT zlib data, exact non-interlaced or Adam7 phone-sized scanlines, and IEND at end of file. The current `spec/reviews/transient-feedback-alpha.md` must name a real reviewed package commit that is an ancestor of package HEAD; only the four screenshot files and that review may change afterward. Its showcase revision must equal the clean showcase HEAD. The review records date, reviewer, both release targets, all four exact image paths, and substantive PASS or APPROVED details for capture, platform runtime, lifecycle, VoiceOver, TalkBack, physical-device checks, and visual approval. Placeholder words embedded anywhere in those identity or evidence fields block release. | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Transient Feedback on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/transient-feedback/ios-light.png) | ![Transient Feedback on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/transient-feedback/ios-dark.png) | | Android | ![Transient Feedback on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/transient-feedback/android-light.png) | ![Transient Feedback on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/transient-feedback/android-dark.png) | --- # Badge Source: https://firstlightui.dev/docs/components/badge Badge presents a compact display-only count or short marker. Use Status Label for longer status text and an interactive control when the interface must respond to a press. ## Complete examples ```blade ``` The same authored tags render on iOS and Android. ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `count` | non-negative `int` | Numeric marker. Counts above 99 display as `99+`; zero is hidden. Exactly one of `count` or `label` is required. | | `label` | non-empty `string` | Short visible marker text. Exactly one of `count` or `label` is required. | | `tone` | `neutral`, `info`, `success`, `warning`, or `danger` | Semantic colour intent. Defaults to `neutral`. | | `a11y-label` | `string` | Accessible name. Required and non-empty for count badges; replaces the visible label for text badges. | | `a11y-hint` | `string` | Supplementary VoiceOver or TalkBack context. | | `class` | `string` | External EDGE layout utilities. | ## Events and values Badge has no model binding or events. A later server publication may replace its count, label, tone, or accessibility metadata without producing a callback. Do not use `native:model`, `@change`, `@press`, `value`, `disabled`, `loading`, `helper`, `error`, `required`, icons, variants, or colour escape props. Firstlight rejects those attributes instead of silently adding interactive or field semantics. ## Count behaviour Firstlight formats the visible count once in PHP so both platforms publish and render the same label: - `0` publishes an empty display label and renders no badge. - `1` through `99` display the complete decimal count. - `100` and above display `99+`. A count badge always requires a contextual `a11y-label`; the formatted visual text alone is not a useful accessible name. A text badge uses its visible label as the accessible name unless `a11y-label` replaces it. ## State timing Badge renders the latest published Element Tree metadata. It keeps no native selection, editing buffer, pending proposal, or animation state. Programmatic updates reconcile by the element's stable node ID and emit nothing. ## Accessibility Both platforms expose static-text semantics without button, selected, disabled, or live-region traits. Hidden zero badges expose no element. Text follows Dynamic Type on iOS and system font scaling on Android. Semantic theme colours are checked at render time. When a customised foreground and background are below 4.5:1 contrast, Firstlight uses whichever of black or white provides the stronger contrast. ## Validation and failure behaviour Missing both display sources, providing both, a negative or non-integer count, an empty label, or an unsupported tone throws an `InvalidArgumentException` before publication. Malformed tone data that unexpectedly reaches a native renderer falls back defensively to `neutral` rather than crashing the host. ## Platform behaviour iOS composes SwiftUI `Text` in a native capsule-style marker. Android composes Material 3 `Badge`. Both inherit NativePHP semantic theme tokens while retaining platform-native typography, scaling, colour-scheme, and layout behaviour. ## Compatibility Badge supports the versions listed in the current [compatibility reference](https://firstlightui.dev/docs/reference/compatibility) and requires both native renderers to be compiled into the host application. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Badge on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/badge/ios-light.png) | ![Badge on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/badge/ios-dark.png) | | Android | ![Badge on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/badge/android-light.png) | ![Badge on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/badge/android-dark.png) | --- # Checkbox Source: https://firstlightui.dev/docs/components/checkbox Checkbox represents one Boolean form or checklist value. Use it for explicit acknowledgements and selections that are normally saved with a form. Use [Switch](https://firstlightui.dev/docs/components/switch) for a setting that takes effect immediately, or [Choice Group](https://firstlightui.dev/docs/components/choice-group) for selection from a visible collection. ## Complete example Declare a Boolean property on the native component, then bind it with `native:model`: ```php ``` ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `value` | `bool` | Accepted value. Defaults to `false` when omitted. | | `native:model` | a Boolean component property | Synchronises the accepted value through NativePHP. `native:model.live` is also accepted. | | `label` | `string` | Visible checkbox label. | | `helper` | `string` | Supporting text below the label. | | `error` | `string` | Validation text and error semantics; shown instead of `helper` when non-empty. | | `required` | `bool` | Marks the field as required visually and for assistive technology. | | `disabled` | `bool` | Prevents proposals and change events while retaining the accepted value. | | `a11y-label` | `string` | Explicit accessible name when a visible label is inappropriate. | | `a11y-hint` | `string` | Additional VoiceOver and TalkBack guidance. | | `class` | `string` | External EDGE layout utilities. | The camel-case authoring aliases `a11yLabel` and `a11yHint` are accepted and publish the same native props. ## Events `@change` receives the proposed Boolean value. Use it when PHP decides whether to accept a change: ```php public bool $acceptedTerms = false; public function updateAcceptedTerms(bool $value): void { $this->acceptedTerms = $value; } ``` ```blade ``` `native:model` normally supplies the same callback for property synchronisation. ## Accepted values Checkbox accepts only strict PHP booleans: `true` and `false`. Bind a literal Boolean with `:`: ```blade ``` `value="false"` is invalid because Blade passes it as a string. `null`, integers, string booleans, arrays, and objects are also rejected. Checkbox has no nullable or indeterminate state. ## State timing Checkbox is [server-authoritative](https://firstlightui.dev/docs/concepts/server-authoritative-state). A tap emits the inverse Boolean as one proposal, but the visible checkmark remains at the last value published by PHP. An accepted response changes the visible state; a rejected response republishes the previous state. Either response clears the pending proposal guard. Programmatic publications update the checkmark without emitting `@change`. Use `native:model` or `native:model.live`. Deferred `blur`, `lazy`, and `debounce` synchronisation modes are rejected. ## Disabled behaviour When `disabled` is `true`, Checkbox stays visible with its accepted checked or unchecked state. It cannot emit a proposal or `@change` event. ## Accessibility Provide a visible `label` or an explicit `a11y-label`; Firstlight warns during development when both are blank. Label, value, required state, hint, helper or error feedback, and the native checkbox role form one accessibility target. The visual checkmark is decorative to avoid a second focus stop. iOS provides a minimum 44-point target and exposes a toggle trait with checked state. Android provides a minimum 48-dp target with `Role.Checkbox` and one TalkBack focus stop. Both platforms support wrapping text and system text scaling. ## Validation and failure behaviour Contract exceptions still reject non-Boolean props and unsupported attributes before publication. User validation is separate: screens that `use ValidatesFields` auto-bind the first MessageBag message for the field's `native:model` or `error-for` name. An authored `error` wins. `required` is display metadata and does not run Laravel rules. See [Validate fields](https://firstlightui.dev/docs/how-to/validate-fields). Error text replaces helper text and is included in accessibility semantics. ## Platform behaviour iOS uses an idiomatic SwiftUI checkmarked row built from a native `Button` and SF Symbol state affordance. Android uses a genuine Material 3 `Checkbox` inside a row-owned checkbox target. The public EDGE API and server-authoritative state contract are shared, while geometry and interaction presentation remain native to each platform. ## Compatibility Checkbox supports the versions listed in the current [compatibility reference](https://firstlightui.dev/docs/reference/compatibility). The host application must compile both Firstlight-owned native renderers declared by the package manifest. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Checkbox on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/checkbox/ios-light.png) | ![Checkbox on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/checkbox/ios-dark.png) | | Android | ![Checkbox on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/checkbox/android-light.png) | ![Checkbox on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/checkbox/android-dark.png) | --- # Choice Group Source: https://firstlightui.dev/docs/components/choice-group Choice Group presents a visible list of labelled options for selecting zero or one value, or zero or more values. ## Complete examples Single choice is the default: ```php public ?string $priority = 'routine'; public array $priorityOptions = [ ['value' => 'routine', 'label' => 'Routine'], ['value' => 'urgent', 'label' => 'Urgent'], ['value' => 'critical', 'label' => 'Critical', 'disabled' => true], ]; ``` ```blade ``` Use `multiple` with a list-valued property: ```php public array $notifications = ['email']; ``` ```blade ``` ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `options` | `array` | Simple strings, a value-to-label map, or rich option arrays. Labels must contain visible text. | | `value` / `native:model` | `string`, `int`, `array`, or `null` | Stable selection. Use a scalar or `null` in single mode and a homogeneous list or `null` in multiple mode. | | `multiple` | `bool` | Enables zero-or-more selection. Defaults to `false`. | | `label` | `string` | Visible field label. | | `helper` | `string` | Supporting text shown when there is no error. | | `error` | `string` | Error text and native error semantics; replaces helper text. | | `required` | `bool` | Communicates required state; PHP still owns validation. | | `disabled` | `bool` | Disables the complete group. | | `a11y-label` | `string` | Explicit accessibility label when a visible label is inappropriate. | | `a11y-hint` | `string` | Additional VoiceOver and TalkBack guidance. | | `class` | `string` | External EDGE layout utilities. | ## Options and values Simple strings use the same value and label: ```php ['Routine', 'Urgent'] ``` A map separates stable values from labels and may use all-string or all-integer keys: ```php ['routine' => 'Routine', 'urgent' => 'Urgent'] ``` Rich options accept only `value`, `label`, and optional `disabled` fields: ```php [ ['value' => 'routine', 'label' => 'Routine'], ['value' => 'urgent', 'label' => 'Urgent', 'disabled' => true], ] ``` All option labels must contain visible text. Values in one group must be unique and must all be strings or all be integers. In single mode, tapping an unselected row proposes its value. Tapping the selected row does nothing, matching radio-choice semantics. PHP can publish `null` to clear the field programmatically. In multiple mode, tapping toggles that stable value while preserving the order of the other values. `null` and `[]` both mean no selection. ## Events and state timing `@change` receives the complete proposed selection: a scalar in single mode and a list in multiple mode. `native:model` normally generates this callback for property synchronisation. Choice Group is [server-authoritative](https://firstlightui.dev/docs/concepts/server-authoritative-state). A tap emits immediately, but selected visuals wait for PHP's next publication. Further taps are ignored while that proposal is in flight so a second proposal cannot be calculated from stale state. Rejected and programmatic publications do not emit. An identical PHP publication must also release the pending-interaction guard. This component therefore requires a NativePHP build containing identical-publication delivery for custom EDGE renderers. Use plain `native:model` or `native:model.live`; deferred `blur` and `debounce` modes are rejected. ## Disabled and required behaviour `disabled` prevents every row from emitting. A rich option's `disabled: true` affects only that row. An empty option set becomes an inert disabled group. `required` is metadata rather than client-side enforcement. Validate the value in PHP and publish `error` when needed. Error text replaces helper text on both platforms. ## Accessibility Provide either a visible `label` or an explicit `a11y-label`. Firstlight warns during development when both are blank. Each complete row is one accessibility target with its label, selected or checked state, and disabled state. Selection is also shown with a native indicator, so it does not depend on colour alone. Rows support wrapping labels and retain at least a 44-point iOS or 48-dp Android interaction target. ## Validation and failure behaviour Contract exceptions still reject malformed options, mixed selections, and unsupported presentation props before publication. User validation is separate: screens that `use ValidatesFields` auto-bind the first MessageBag message for the field's `native:model` or `error-for` name. An authored `error` wins. `required` is display metadata and does not run Laravel rules. See [Validate fields](https://firstlightui.dev/docs/how-to/validate-fields). A same-typed selected value absent from `options` remains unselected rather than choosing a fallback. ## Platform behaviour iOS uses native SwiftUI option rows with trailing checkmarks. Android renders one Material 3 radio-row set for single choice and one homogeneous checkbox-row set for multiple choice. Both inherit NativePHP semantic theme tokens and preserve native focus, selection, disabled, and accessibility behaviour without exposing platform-widget props. ## Compatibility Choice Group supports the versions listed in the current [compatibility reference](https://firstlightui.dev/docs/reference/compatibility), requires both native renderers in the host application, and currently depends on the unreleased NativePHP identical-publication fix described above. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Choice Group on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/choice-group/ios-light.png) | ![Choice Group on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/choice-group/ios-dark.png) | | Android | ![Choice Group on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/choice-group/android-light.png) | ![Choice Group on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/choice-group/android-dark.png) | --- # Media Source: https://firstlightui.dev/docs/components/media Media captures exactly one image or one document into Laravel Storage and binds a `MediaValue` on your native component. Use it when you need a form field with preview, clear, optional crop, and the same `error` slot as other Firstlight fields. ## Complete example ```php */ protected array $rules = [ 'avatar' => 'required|image|max:2048', ]; public function avatarChosen(string $tempPath): void { $previous = $this->avatar; $this->avatar = MediaStorage::commit( $tempPath, 'mobile_public', 'avatars', ); MediaStorage::delete($previous); } public function avatarCleared(): void { MediaStorage::delete($this->avatar); $this->avatar = null; } public function render(): View { return view('native.profile'); } } ``` ```blade ``` ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `mode` | `image` \| `document` | Required. Image allows camera/library and crop; document uses the system file picker only. | | `value` | `MediaValue` \| `null` | Committed value. Empty is `null`. | | `native:model` | a `?MediaValue` component property | Synchronises the accepted value. `native:model.live` is also accepted. | | `label` | `string` | Visible field label. | | `helper` | `string` | Supporting text below the control. | | `error` | `string` | Validation text; shown instead of `helper` when non-empty. | | `required` | `bool` | Marks the field as required visually and for assistive technology. | | `disabled` | `bool` | Prevents pick and clear while retaining the accepted value. | | `disk` | `string` | Laravel Storage disk. Defaults to `mobile_public`. | | `directory` | `string` | Directory prefix under the disk. Defaults to `media`. | | `aspect` | ratio string such as `1:1` | Image only. Implies required crop to that aspect. | | `crop` | `optional` \| `required` | Image only. Freeform crop when `aspect` is omitted. | | `a11y-label` | `string` | Explicit accessible name when needed. | | `a11y-hint` | `string` | Additional VoiceOver and TalkBack guidance. | | `class` | `string` | External EDGE layout utilities. | Document mode rejects `crop` and `aspect`. Unsupported attributes such as `multiple` and `video` are rejected before publication. ### Crop rules - Neither `crop` nor `aspect` → store without a crop sheet. - `aspect` → required crop locked to that aspect. - `crop="optional"` without aspect → freeform crop; Skip is allowed. - `crop="required"` without aspect → freeform crop; Confirm is required. Clear, Skip, Confirm, Cancel, Crop, zoom, and source-chooser copy are package chrome; see [Localize chrome](https://firstlightui.dev/docs/how-to/localize). ## Events `@change` receives the temporary absolute file path after the user confirms a pick (and crop when required). Commit with `MediaStorage::commit` before assigning `MediaValue`. `@clear` fires when the user clears a present value. Delete the previous object with `MediaStorage::delete` and set the model to `null`. ## MediaValue ```php new MediaValue( disk: 'mobile_public', path: 'avatars/a.jpg', mime: 'image/jpeg', size: 1200, width: 100, height: 100, ); ``` Display with `Storage::disk($value->disk)->url($value->path)`. ## Validation Media participates in [Validate fields](https://firstlightui.dev/docs/how-to/validate-fields). Public `MediaValue` properties are adapted to Laravel `UploadedFile` values so stock `image`, `file`, `mimes`, `max`, and `dimensions` rules work. `null` fails `required`. Authored `error` wins over the MessageBag binder. ## Accessibility The field exposes a labelled control with helper or error text. Crop sheets include Confirm, Cancel, optional Skip, and explicit zoom in/out controls meeting 44-point (iOS) and 48-dp (Android) minima. Zoom is not pinch-only. ## Platform behaviour iOS uses PhotosPicker, camera capture, and fileImporter with a Firstlight SwiftUI crop sheet. Android uses Material 3 field chrome and a Firstlight Compose crop dialog. Geometry and system pickers stay platform-native; crop semantics stay shared. ## Compatibility Media supports the versions listed in the current [compatibility reference](https://firstlightui.dev/docs/reference/compatibility). The host application must compile both Firstlight-owned native renderers declared by the package manifest. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Media on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/media/ios-light.png) | ![Media on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/media/ios-dark.png) | | Android | ![Media on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/media/android-light.png) | ![Media on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/media/android-dark.png) | --- # Alert Dialog Source: https://firstlightui.dev/docs/components/alert-dialog Alert Dialog tells the user one fact they must acknowledge. It uses the platform's native one-button alert, not a custom full-screen overlay. Use [Confirmation Dialog](https://firstlightui.dev/docs/components/confirmation-dialog) when the user must choose between confirm and cancel. Use [Transient Feedback](https://firstlightui.dev/docs/components/transient-feedback) for a non-blocking toast. Use [Modal](https://firstlightui.dev/docs/components/modal) only when the surface needs authored child content. ```blade ``` The dismiss handler should set `$showingSaved` to `false`. The same handler runs for the action button, back, and outside dismissal. ## Props and events - `visible`: server-controlled presentation request; defaults to `false`. - `title`: required visible heading and accessible dialog name. - `message`: required explanation. - `action-label`: acknowledgement action text; defaults to the package chrome string `OK`. The `actionLabel` alias is accepted. See [Localize chrome](https://firstlightui.dev/docs/how-to/localize). - `@dismiss`: required callback for the action, back, or outside dismissal. - `class`: external EDGE layout only. The component always presents one action. It does not support `@press`, a cancel action, tones, icons, loading or disabled states, `native:model`, authored children, or an undismissable mode. ## Behavior and accessibility One user acknowledgement produces one `@dismiss` callback. Programmatic closure produces none. After the user dismisses the dialog, copy-only publications do not reopen it; a later server transition from `false` to `true` presents it again. iOS uses SwiftUI `alert`. Android uses Material 3 `AlertDialog` with a single confirm button. Native chrome owns modal focus, action placement, Dynamic Type or font scaling, dark appearance, contrast, reduced motion, and right-to-left behavior. The title is the accessible name. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Alert Dialog on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/alert-dialog/ios-light.png) | ![Alert Dialog on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/alert-dialog/ios-dark.png) | | Android | ![Alert Dialog on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/alert-dialog/android-light.png) | ![Alert Dialog on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/alert-dialog/android-dark.png) | --- # Confirmation Dialog Source: https://firstlightui.dev/docs/components/confirmation-dialog Confirmation Dialog asks the user to confirm or cancel one consequential action using the platform's native presentation and action ordering. ```blade ``` Both handlers should set `$confirmingDeletion` to `false`. The confirmation handler also performs the action; the dismissal handler only cancels the request. ## Props and events - `visible`: server-controlled presentation request; defaults to `false`. - `title`: required visible heading and accessible dialog name. - `message`: required explanation of the decision or consequence. - `confirm-label`: confirmation action text; defaults to the package chrome string `Confirm`. See [Localize chrome](https://firstlightui.dev/docs/how-to/localize). - `cancel-label`: cancellation action text; defaults to the package chrome string `Cancel`. - `tone`: `default` or `destructive`; defaults to `default`. - `@press`: required callback for explicit confirmation. - `@dismiss`: required callback for cancellation, back, or outside dismissal. - `class`: external EDGE layout only. The component always presents one confirm action and one cancel action. It does not support arbitrary action lists, icons, loading or disabled states, `native:model`, per-platform styling, or an undismissable mode. Use [Alert Dialog](https://firstlightui.dev/docs/components/alert-dialog) when the user only needs to acknowledge a message. ## Behavior and accessibility One user outcome produces one callback. Programmatic closure produces none. After a user dismisses the dialog, copy-only publications do not reopen it; a later server transition from `false` to `true` presents a new decision. iOS uses SwiftUI `confirmationDialog`; Android uses Material 3 `AlertDialog`. Their native controls own modal focus, action order, destructive expression, Dynamic Type or font scaling, dark appearance, contrast, reduced motion, and right-to-left behavior. Use specific action labels such as `Delete` rather than generic text when the consequence can be named. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Confirmation Dialog on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/confirmation-dialog/ios-light.png) | ![Confirmation Dialog on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/confirmation-dialog/ios-dark.png) | | Android | ![Confirmation Dialog on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/confirmation-dialog/android-light.png) | ![Confirmation Dialog on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/confirmation-dialog/android-dark.png) | --- # Modal Source: https://firstlightui.dev/docs/components/modal Modal presents authored content in a full-screen native overlay. PHP owns whether the overlay is requested; each platform owns cover or dialog chrome and motion. Use [Alert Dialog](https://firstlightui.dev/docs/components/alert-dialog) for a single acknowledgement. Use [Confirmation Dialog](https://firstlightui.dev/docs/components/confirmation-dialog) for a confirm/cancel decision. Use [Bottom Sheet](https://firstlightui.dev/docs/components/bottom-sheet) for a panel that slides up from the bottom. ## Complete example ```blade ``` The dismiss handler should set `$showingAccount` to `false`. ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `visible` | `bool` | Server-controlled presentation request. Defaults to closed. | | `dismissible` | `bool` | When true, native close, back, and outside dismissal are enabled. Defaults to the upstream native default of `true`. The `dismissable` alias is accepted. | | `a11y-label` | non-empty `string` | Accessible name for the presented surface. | | `a11y-hint` | non-empty `string` | Supplementary screen-reader guidance. | | `class` | `string` | External EDGE layout utilities. | ## Events `@dismiss` is required. It runs when the user closes the overlay. Set the bound visibility property to `false` in that handler. Dismiss handlers must be safe to run more than once because the delegated iOS cover can also notify PHP when a programmatic close is published. There is no overlay `@press`, `native:model`, or detent API. Actions belong on child controls. ## Platform expression Modal is an adapter over Mobile UI `modal`. iOS uses SwiftUI `.fullScreenCover`; Android uses a Material full-screen dialog. Theme tokens own surface colour. Close controls, when shown, use the native Close name. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Modal on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/modal/ios-light.png) | ![Modal on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/modal/ios-dark.png) | | Android | ![Modal on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/modal/android-light.png) | ![Modal on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/modal/android-dark.png) | --- # Bottom Sheet Source: https://firstlightui.dev/docs/components/bottom-sheet Bottom Sheet presents authored content in a panel that slides up from the bottom of the screen. PHP owns whether the sheet is requested; each platform owns sheet chrome, drag to dismiss, and height stops. Use [Modal](https://firstlightui.dev/docs/components/modal) for a full-screen overlay. Use [Alert Dialog](https://firstlightui.dev/docs/components/alert-dialog) for a single acknowledgement. Use [Confirmation Dialog](https://firstlightui.dev/docs/components/confirmation-dialog) for a confirm/cancel decision. ## Complete example ```blade ``` The dismiss handler should set `$showingFilters` to `false`. ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `visible` | `bool` | Server-controlled presentation request. Defaults to closed. | | `a11y-label` | non-empty `string` | Accessible name for the sheet. | | `a11y-hint` | non-empty `string` | Supplementary screen-reader guidance. | | `class` | `string` | External EDGE layout utilities. | ## Events `@dismiss` is required. It runs when the user drags the sheet down, taps outside, or uses system back. Set the bound visibility property to `false` in that handler. There is no overlay `@press`, `native:model`, `dismissible`, or shared `detents` API. Height stops stay native so iOS detents and Material sheet stops are not forced into one geometry. ## Platform expression Bottom Sheet is an adapter over Mobile UI `bottom_sheet`. iOS uses SwiftUI `.sheet` with a drag indicator. Android uses Material 3 `ModalBottomSheet`. Theme tokens own surface and scrim colour. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Bottom Sheet on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/bottom-sheet/ios-light.png) | ![Bottom Sheet on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/bottom-sheet/ios-dark.png) | | Android | ![Bottom Sheet on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/bottom-sheet/android-light.png) | ![Bottom Sheet on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/bottom-sheet/android-dark.png) | --- # Date Picker Source: https://firstlightui.dev/docs/components/date-picker Date Picker chooses one calendar date with the native date picker on each platform. PHP remains authoritative: the closed field always shows the value PHP most recently published. ## Complete example ```blade ``` The bound property must contain a canonical `YYYY-MM-DD` string or `null`: ```php public ?string $appointmentDate = null; ``` ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `value` / `native:model` | `string|null` | Accepted canonical `YYYY-MM-DD` date, or `null` for no date; omission defaults to `null`. | | `min` | `string|null` | Inclusive canonical lower bound. | | `max` | `string|null` | Inclusive canonical upper bound. | | `label` | `string` | Visible field label and accessibility-name fallback. | | `placeholder` | `string` | Field text while the accepted value is `null`. | | `helper` | `string` | Supporting guidance below the field. | | `error` | `string` | Validation feedback that replaces helper text. | | `required` | `bool` | Communicates required metadata; validation remains in PHP. | | `disabled` | `bool` | Prevents opening and confirmation. | | `locale` | BCP-47 string | Formats the field and native calendar without changing the wire date. Omission inherits the application translator locale. | | `timezone` | IANA timezone string | Determines today and Swift date mapping without shifting the wire date. Omission inherits `config('app.timezone')` when it is a valid IANA identifier. | | `a11y-label` | `string` | Explicit accessible name when no visible label is appropriate. | | `a11y-hint` | `string` | Additional VoiceOver and TalkBack guidance. | | `class` | `string` | External EDGE layout for the complete field. | Omitted `value` and explicit `null` publish the same empty state. The component does not accept `DateTimeInterface`, timestamps, whitespace, unpadded dates, or year zero. Years `0001` through `9999` use proleptic Gregorian leap-year rules. `min` and `max` are inclusive, must be ordered, and must contain an accepted non-null value. Null bounds are omitted. Firstlight never silently clamps an authored value. ## Events and synchronisation `@change` receives a canonical date string only after the user confirms the native presentation. In normal use, `native:model` registers this callback and updates the bound property. Plain `native:model` and `native:model.live` are equivalent because a date is a discrete choice. Blur, lazy, and debounce modifiers are rejected. Opening the picker, moving its draft, cancelling, confirming the already accepted date, and programmatic PHP updates emit nothing. The trigger never shows a draft optimistically. PHP may accept, reject, or replace a proposal; the next tree publication remains the source of truth. If accepted value, bounds, locale, timezone, or disabled state changes while the picker is open, Firstlight dismisses it and discards its draft. A null value initially drafts today in `timezone`, clamped to the nearest bound when necessary, but does not publish until confirmation. ## Validation and accessibility Invalid inherited application locale or timezone is omitted so the device calendar is used. Authored malformed locale or timezone still fail before publication. Sheet Confirm and Cancel labels are package chrome, not Blade attributes; see [Localize chrome](https://firstlightui.dev/docs/how-to/localize). User validation is separate: screens that `use ValidatesFields` auto-bind the first MessageBag message for the field's `native:model` or `error-for` name. An authored `error` wins. `required` is display metadata and does not run Laravel rules. See [Validate fields](https://firstlightui.dev/docs/how-to/validate-fields). A visible `label` or explicit `a11y-label` is required during development. Error text replaces helper text without replacing the control's accessible name or value. The field exposes its accepted localized value or placeholder, hint, disabled state, and platform error semantics. The calendar keeps native traversal, focus, selection, today, cancel, and confirm behaviour. The field and actions retain at least a 44-point target on iOS and 48-dp target on Android, and support Dynamic Type or font scaling, dark mode, increased contrast, and right-to-left layout. ## Platform behaviour iOS presents an adaptive popover or sheet containing a genuine graphical SwiftUI `DatePicker` restricted to calendar dates, with explicit Cancel and Confirm actions. Android presents the Material 3 `DatePickerDialog` from a read-only Material field. Android maps Material's selected UTC-midnight cell back to the same canonical date rather than treating it as a local instant. Date Picker deliberately has no time or datetime mode, picker-style override, hour format, clear action, custom presentation labels or icons, range mode, or visual variant props. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Date Picker on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/date-picker/ios-light.png) | ![Date Picker on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/date-picker/ios-dark.png) | | Android | ![Date Picker on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/date-picker/android-light.png) | ![Date Picker on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/date-picker/android-dark.png) | --- # Time Picker Source: https://firstlightui.dev/docs/components/time-picker Time Picker chooses an optional wall-clock time using the platform's native time control. The model value is always exact 24-hour `HH:mm` or `null`, even when the platform displays a localized 12-hour clock. ```blade ``` The user edits a temporary native draft. Cancel discards it and Confirm sends `@change`; the closed trigger continues to show the server-accepted value until PHP republishes the tree. ## Values and props - `value` / `native:model`: exact `HH:mm` or `null`. - `label`, `placeholder`, `helper`, and `error`: field copy. - `required` and `disabled`: field state. - `locale`: BCP-47 tag used only for display. Omission inherits the application translator locale. - `timezone`: IANA identifier used only to seed a null draft with the current local minute. Omission inherits `config('app.timezone')` when it is a valid IANA identifier. - `a11y-label` and `a11y-hint`: explicit accessibility copy. - `class`: external EDGE layout. Use plain `native:model` or `native:model.live`. Time Picker commits only on confirmation, so blur, lazy, and debounce modes are rejected. It deliberately does not expose min/max, seconds, steps, ranges, hour-format overrides, presentation styles, clear affordances, read-only state, icons, or colours. Sheet Confirm and Cancel labels are package chrome, not Blade attributes; see [Localize chrome](https://firstlightui.dev/docs/how-to/localize). ## Validation and accessibility Contract exceptions still reject coerced or non-canonical time strings before publication. User validation is separate: screens that `use ValidatesFields` auto-bind the first MessageBag message for the field's `native:model` or `error-for` name. An authored `error` wins. `required` is display metadata and does not run Laravel rules. See [Validate fields](https://firstlightui.dev/docs/how-to/validate-fields). Always provide either a visible `label` or `a11y-label`. Errors replace helper text visually and are announced by platform semantics. The native presentation retains standard VoiceOver or TalkBack traversal and Cancel/Confirm actions. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Time Picker on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/time-picker/ios-light.png) | ![Time Picker on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/time-picker/ios-dark.png) | | Android | ![Time Picker on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/time-picker/android-light.png) | ![Time Picker on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/time-picker/android-dark.png) | --- # Icon Button Source: https://firstlightui.dev/docs/components/icon-button Icon Button triggers one compact action represented by an icon. It renders as a genuine SwiftUI button on iOS and the Material 3 Icon Button family on Android while keeping one portable Firstlight API. ## Complete example ```blade ``` Icon Button is self-closing and has no visible text slot. Use `` when the action needs a visible label, or `` for a decorative or display-only glyph. ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `icon` | non-empty `string` | Required shared icon name and cross-platform fallback. | | `icon-ios` | `IosSymbol|string` | Optional iOS override. | | `icon-android` | `AndroidSymbol|string` | Optional Android override; typed filled or outlined variants are preserved on the wire. | | `a11y-label` | non-empty `string` | Required accessible action name. Firstlight never derives it from the machine icon name. | | `a11y-hint` | `string` | Optional supplementary VoiceOver or TalkBack guidance. | | `variant` | `primary`, `secondary`, `destructive`, `success`, or `ghost` | Semantic action intent. Defaults to `primary`. | | `size` | `sm`, `md`, or `lg` | Native glyph and container size. Defaults to `md`; the touch target remains at least 44 points or 48 dp. | | `disabled` | `bool` | Prevents interaction and exposes native disabled state. Defaults to `false`. | | `loading` | `bool` | Replaces the glyph with native progress, announces loading, and prevents duplicate presses. Defaults to `false`. | | `class` | `string` | External EDGE layout utilities. | Blade documentation uses `icon-ios` and `icon-android`. The equivalent `iconIos` and `iconAndroid` aliases are accepted for parity with NativePHP. ## Events `@press` is required and invokes the named PHP action once for a native user press: ```php public function addItem(): void { // Persist the action and publish any resulting state. } ``` Icon Button has no `value`, `native:model`, or `@change`. Disabled and loading buttons do not emit. Programmatic publications never emit a press. ## Variants and sizes The variants share the same semantic vocabulary as Firstlight Button. iOS uses Apple-native prominent or plain button expression and theme tints. Android uses `FilledIconButton`, `FilledTonalIconButton`, or `IconButton` according to intent. `sm`, `md`, and `lg` scale the visible glyph and native container without shrinking the accessible interaction target. ## Loading and disabled behaviour `loading` keeps the explicit accessible name, substitutes a native progress indicator, exposes a loading value or state description, and disables the action until PHP publishes a non-loading state. `disabled` retains the icon and name while exposing native disabled appearance and semantics. ## Accessibility Every Icon Button requires `a11y-label`; familiar-looking icon names are not accepted as an accessible-name substitute. The glyph is decorative inside one button accessibility node. Native renderers expose the button role, optional hint, disabled or loading state, focus and press feedback, and interaction targets of at least 44 points on iOS and 48 dp on Android. ## Validation and excluded APIs Firstlight rejects missing or blank icons, missing or blank accessibility labels, missing `@press`, non-boolean state props, invalid variants and sizes, unsupported platform icon values, visible labels or slots, trailing icons, menus, navigation directives, extra gesture callbacks, bindings, field props, and per-control colour or typography escape hatches. ## Compatibility Icon Button supports the versions listed in the current [compatibility reference](https://firstlightui.dev/docs/reference/compatibility) and requires both Firstlight native renderers to be compiled into the host application. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Icon Button on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/icon-button/ios-light.png) | ![Icon Button on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/icon-button/ios-dark.png) | | Android | ![Icon Button on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/icon-button/android-light.png) | ![Icon Button on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/icon-button/android-dark.png) | --- # List Item Source: https://firstlightui.dev/docs/components/list-item List Item presents one tappable application row with a primary headline, optional supporting text, an optional leading identity, and an optional trailing affordance. It renders as a genuine SwiftUI button row on iOS and a Material 3 List Item on Android. ## Complete example ```blade ``` List Item is self-closing. Its content is semantic rather than a general child slot: arbitrary text, controls, and layouts cannot be nested inside the row. ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `headline` | non-empty `string` | Required primary text and default accessible name. | | `supporting` | non-empty `string` | Optional secondary text below the headline. | | `leading-icon` | non-empty `string` | Optional shared leading icon and cross-platform fallback. | | `leading-icon-ios` | `IosSymbol|string` | Optional iOS override; requires `leading-icon`. | | `leading-icon-android` | `AndroidSymbol|string` | Optional Android override; requires `leading-icon`; typed variants are preserved on the wire. | | `leading-avatar` | non-empty `string` | Optional image source representing the row subject. | | `leading-monogram` | one or two characters | Optional short identity marker such as `WJ`. | | `trailing-icon` | non-empty `string` | Optional shared decorative affordance icon. | | `trailing-icon-ios` | `IosSymbol|string` | Optional iOS override; requires `trailing-icon`. | | `trailing-icon-android` | `AndroidSymbol|string` | Optional Android override; requires `trailing-icon`; typed variants are preserved on the wire. | | `trailing-text` | non-empty `string` | Optional short metadata or affordance text. | | `disabled` | `bool` | Prevents presses and exposes native disabled state. Defaults to `false`. | | `a11y-label` | non-empty `string` | Optional replacement for the combined visible accessible name. | | `a11y-hint` | non-empty `string` | Optional supplementary VoiceOver or TalkBack guidance. | | `class` | `string` | External EDGE layout utilities. | Choose at most one leading source: icon, avatar, or monogram. Choose at most one trailing affordance: icon or text. Supporting text and both edges may be omitted for a headline-only row. Blade examples use kebab-case names. The equivalent `leadingIcon`, `leadingIconIos`, `leadingIconAndroid`, `leadingAvatar`, `leadingMonogram`, `trailingIcon`, `trailingIconIos`, `trailingIconAndroid`, and `trailingText` aliases are also accepted for parity with NativePHP authoring. ## Events and state timing `@press` is required and invokes the named PHP action once after a native user press: ```php public function openAccount(): void { // Perform the action and publish any resulting screen state. } ``` List Item has no `value`, `native:model`, selected state, or `@change`. The native row owns only transient press feedback. PHP owns the action outcome and publishes any subsequent headline, supporting text, affordance, accessibility, or disabled state. Programmatic publications never emit a press. ## Disabled behaviour A disabled row retains its visible content, uses native disabled appearance, exposes disabled accessibility semantics, and does not emit `@press`. The `disabled` value must be a real boolean; strings and integers are rejected. ## Leading identity and icons Use `leading-avatar` when an image identifies the row subject, or `leading-monogram` for a one- or two-character textual identity. Use `leading-icon` for a semantic category or destination glyph. All leading content is decorative inside the row's single accessibility node because the headline carries the subject's accessible meaning. Icon resolution happens in PHP. The shared icon is the deterministic fallback; the iOS or Android override wins on its platform. A typed Android symbol keeps its filled or outlined variant in the native wire metadata. Overrides without their shared fallback are invalid. ## Trailing affordances Use `trailing-icon` for a decorative directional or state affordance, or `trailing-text` for short metadata such as `Admin`. The trailing content is not a second action. It remains silent to assistive technology inside the row's single button node. Independent trailing buttons, switches, checkboxes, radio buttons, menus, and swipe actions are excluded. Compose a separate Firstlight control when the interface needs a second target. ## Accessibility By default, assistive technology combines the headline and supporting text as the row's accessible name. `a11y-label` replaces that name and `a11y-hint` adds context. The complete row is one button node with disabled state and a minimum interaction target of 44 points on iOS or 48 dp on Android. Leading avatars, monograms, icons, and trailing content are decorative and do not add duplicate VoiceOver or TalkBack stops. Dynamic Type, Android font scaling, right-to-left layout, native contrast, focus, and press feedback remain platform-owned. ## Validation and excluded APIs Firstlight rejects missing or blank headlines, missing `@press`, blank optional text, non-boolean disabled values, invalid icon override types, icon overrides without shared fallbacks, ambiguous leading or trailing content, and content slots. It also rejects overline text, embedded controls, independent trailing actions, swipe and long-press gestures, menus, navigation directives, model or selection state, loading and validation props, colours, elevation, typography, and per-platform layout escape hatches. ## Platform behaviour iOS uses a SwiftUI `Button` composition with Apple-native headline and supporting typography, optional circular identity or SF Symbol, optional trailing metadata or symbol, native press and disabled behaviour, and a 44-point minimum target. Android uses Material 3 `ListItem` inside one enabled-aware clickable surface, with Material typography, content slots, ripple and disabled semantics, and a 48-dp minimum target. Shared content and action meaning stay the same while spacing, typography, motion, and pressed state remain native. ## Compatibility List Item supports the versions listed in the current [compatibility reference](https://firstlightui.dev/docs/reference/compatibility). The host application must compile both Firstlight-owned native renderers declared by the package manifest. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![List Item on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/list-item/ios-light.png) | ![List Item on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/list-item/ios-dark.png) | | Android | ![List Item on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/list-item/android-light.png) | ![List Item on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/list-item/android-dark.png) | --- # List Source: https://firstlightui.dev/docs/components/list List presents a finite vertical collection of application rows. Compose rows with [List Item](https://firstlightui.dev/docs/components/list-item) and optional grouped sections with [List Section](https://firstlightui.dev/docs/components/list-section). ## Complete example ```blade ``` List is a paired container. Only List Item and List Section children are accepted. ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `separator` | `bool` | Shows native dividers between rows. Defaults to `false`. | | `plain` | `bool` | When sections are present, keeps flat rows with sticky headers instead of grouped inset cards. Defaults to `false`. | | `shows-indicators` | `bool` | Controls native scroll indicator visibility. Defaults to upstream platform behaviour when omitted. | | `class` | `string` | External EDGE layout utilities. | ## Events `@refresh` enables native pull-to-refresh and invokes the named PHP action when the user pulls to refresh. `@end-reached` invokes the named PHP action when the viewport nears the final leaf row. It is intended for pagination rather than precise scroll offsets. See [Paginate lists](https://firstlightui.dev/docs/how-to/paginate-lists) to bind Laravel paginators to these events. To delete a row through Confirmation Dialog authorization, see [Destroy list items](https://firstlightui.dev/docs/how-to/destroy-list-items). ## Composition rules - Every row is a `firstlight:list-item` with its own required `@press` handler. - Group related rows with `firstlight:list-section`. Sections require at least one List Item child. - Horizontal lists, virtualized windows, selectable collections, and arbitrary child components are outside the Firstlight List contract. ## Platform expression List delegates scrolling, separators, grouped section chrome, pull-to-refresh, and end-reached detection to the official Mobile UI list renderer on both platforms while retaining Firstlight List Item rows. ## Adapter note List is an adapter over Mobile UI `list`. List Section publishes the upstream wire type `list_section` so the delegated renderer recognizes grouped sections while the public tag remains ``. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![List on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/list/ios-light.png) | ![List on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/list/ios-dark.png) | | Android | ![List on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/list/android-light.png) | ![List on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/list/android-dark.png) | --- # List Section Source: https://firstlightui.dev/docs/components/list-section List Section groups related [List Item](https://firstlightui.dev/docs/components/list-item) rows under an optional header and footer inside a parent [List](https://firstlightui.dev/docs/components/list). ## Complete example ```blade ``` List Section is a paired container and must live inside List. A section authored on its own does not render application content. ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `header` | non-empty `string` | Optional section title. | | `footer` | non-empty `string` | Optional section footnote. | At least one List Item child is required. ## Composition rules - Child slot accepts only `firstlight:list-item`. - Nested sections, refresh hooks, separators, and field props belong on the parent List instead. ## Platform expression The parent List renderer consumes List Section inline: - iOS renders SwiftUI section headers and footers. - Android renders sticky headers, grouped card rows, and footer text. ## Adapter note The public tag is ``, but the published wire type remains `list_section` so the delegated Mobile UI list renderer can apply grouped section styling. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![List Section on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/list-section/ios-light.png) | ![List Section on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/list-section/ios-dark.png) | | Android | ![List Section on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/list-section/android-light.png) | ![List Section on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/list-section/android-dark.png) | --- # Pill Group Source: https://firstlightui.dev/docs/components/pill-group Pill Group presents compact, individually shaped options for selecting zero or one value, or zero or more values. ## Complete examples Single selection is the default: ```php public ?string $queue = 'mine'; public array $queueOptions = [ ['value' => 'mine', 'label' => 'Mine'], ['value' => 'all', 'label' => 'All'], ['value' => 'archived', 'label' => 'Archived', 'disabled' => true], ]; ``` ```blade ``` Use `multiple` with an array-valued property: ```php public array $queues = ['mine']; ``` ```blade ``` ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `options` | `array` | Simple strings, a value-to-label map, or rich option arrays. | | `value` / `native:model` | `string`, `int`, `array`, or `null` | Stable selection. Use a scalar or `null` in single mode and a homogeneous list or `null` in multiple mode. | | `multiple` | `bool` | Enables zero-or-more selection. Defaults to `false`. | | `label` | `string` | Visible field label. | | `helper` | `string` | Supporting text below the pills. | | `error` | `string` | Error text and native error semantics. | | `required` | `bool` | Communicates required state; PHP still owns validation. | | `disabled` | `bool` | Disables the complete group. | | `a11y-label` | `string` | Explicit accessibility label when a visible label is inappropriate. | | `a11y-hint` | `string` | Additional VoiceOver and TalkBack guidance. | | `class` | `string` | External EDGE layout utilities. | ## Options and values Simple strings use the same value and label: ```php ['Mine', 'All'] ``` A map separates stable values from labels and may use all-string or all-integer keys: ```php ['mine' => 'Mine', 'all' => 'All'] ``` Rich options accept only `value`, `label`, and optional `disabled` fields: ```php [ ['value' => 'mine', 'label' => 'Mine'], ['value' => 'all', 'label' => 'All', 'disabled' => true], ] ``` All values in one group must be unique and must all be strings or all be integers. In single mode, tapping the selected pill proposes `null`. In multiple mode, tapping toggles that stable value while preserving the order of the others. `null` and `[]` both mean no multiple selection. ## Events and state timing `@change` receives the complete proposed selection: a scalar or `null` in single mode and a list in multiple mode. `native:model` normally generates this callback for property synchronisation. Pill Group is [server-authoritative](https://firstlightui.dev/docs/concepts/server-authoritative-state). A tap emits immediately, but selected visuals wait for PHP's next publication. Further taps are ignored while that proposal is in flight so a rapid second tap cannot be calculated from stale state. Programmatic updates and reconciliation do not emit. Use plain `native:model` or `native:model.live`; deferred `blur` and `debounce` modes are rejected. ## Disabled and required behaviour `disabled` prevents every pill from emitting. A rich option's `disabled: true` affects only that pill. An empty option set becomes an inert disabled group. `required` is metadata rather than client-side enforcement. It does not prevent clearing the final pill; validate the resulting value in PHP and publish `error` when needed. ## Accessibility Provide either a visible `label` or an explicit `a11y-label`. Firstlight warns during development when both are blank. Each pill exposes its label, selected state, and disabled state through native platform semantics. Selection also has a visible checkmark, so it does not depend on colour alone. Pills wrap at larger text sizes and retain at least a 44-point iOS or 48-dp Android interaction target. ## Validation and failure behaviour Contract exceptions still reject malformed options, mixed selections, and unsupported presentation props before publication. User validation is separate: screens that `use ValidatesFields` auto-bind the first MessageBag message for the field's `native:model` or `error-for` name. An authored `error` wins. `required` is display metadata and does not run Laravel rules. See [Validate fields](https://firstlightui.dev/docs/how-to/validate-fields). A same-typed selected value absent from `options` remains unselected rather than selecting a fallback. ## Platform behaviour iOS uses native SwiftUI capsule buttons in a wrapping layout. Android uses Material 3 `FilterChip` controls in a wrapping `FlowRow`. Both inherit NativePHP semantic theme tokens and preserve native press, focus, selection, disabled, and accessibility behaviour without exposing platform-widget props. ## Compatibility Pill Group supports the versions listed in the current [compatibility reference](https://firstlightui.dev/docs/reference/compatibility) and requires both native renderers to be compiled into the host application. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Pill Group on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/pill-group/ios-light.png) | ![Pill Group on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/pill-group/ios-dark.png) | | Android | ![Pill Group on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/pill-group/android-light.png) | ![Pill Group on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/pill-group/android-dark.png) | --- # Progress Source: https://firstlightui.dev/docs/components/progress Progress communicates the completion of ongoing work. Publish a fraction when completion is measurable, or use indeterminate progress while the duration is unknown. Firstlight wraps the native progress component from `nativephp/mobile-ui`. The public `` API adds strict values, reliable mode defaults, and actionable diagnostics while retaining the official SwiftUI and Material 3 renderers. ## Complete examples Publish a determinate fraction from `0.0` through `1.0`: ```blade ``` Use indeterminate progress when no meaningful fraction is available: ```blade ``` Omitting `value` also selects indeterminate progress. ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `value` | finite `int`, `float`, or `null` | Published completion fraction from `0.0` through `1.0`. A non-null value selects determinate mode. | | `indeterminate` | `bool` | Explicitly selects or rejects indeterminate mode. Normally inferred from `value`. | | `a11y-label` | non-empty `string` | Required accessible name describing the work. | | `class` | `string` | External EDGE layout utilities. | An integer value is published as a float. `0` means determinate progress that has not advanced; it is different from an omitted or `null` value. `1` means the reported work is complete, but the application decides when to remove the indicator or present its result. Explicit `indeterminate="true"` cannot be combined with a non-null `value`. Explicit `indeterminate="false"` requires a non-null value. ## Events and state timing Progress has no event or model binding. PHP publishes the current value or mode through the ordinary Element Tree, and programmatic updates change the native presentation without producing a callback. Indeterminate animation stays within the native control. Firstlight does not run a timer, publish animation frames, or use a separate bridge. ## Disabled, loading, and failure behaviour Progress already describes loading work, so it has no separate `loading` state. It is display-only and therefore has no disabled, error, helper, required, selected, or pressed state. Values must be actual finite PHP integers or floats within `0.0...1.0`. Numeric strings, booleans, `NaN`, infinities, negative values, and values above `1.0` fail instead of being clamped. A missing, empty, or non-string `a11y-label` also fails. Firstlight does not expose Mobile UI's arbitrary `color` or Android-only `track-color` overrides. It also rejects `label`, `a11y-hint`, `tone`, `variant`, `size`, icons, events, and field props. The installed native Progress renderers do not consume `a11y-hint`, so Firstlight does not pretend that the metadata reaches assistive technology. ## Accessibility Progress has no visible text inside its bounds, so `a11y-label` is required. VoiceOver and TalkBack combine that name with the native progress role and the system-formatted value for determinate progress. Indeterminate progress communicates ongoing activity without inventing a percentage. The native controls retain platform contrast, right-to-left behaviour, Increased Contrast, and Reduced Motion or disabled-animation policy. Progress contains no text that needs a custom scaling treatment. ## Platform behaviour iOS uses SwiftUI `ProgressView()`: the indeterminate initializer for unknown duration and `ProgressView(value:)` for a published fraction. Android uses the corresponding indeterminate or determinate Material 3 `LinearProgressIndicator` overload. Both inherit the host Mobile UI semantic theme. The platforms retain their own geometry and motion; behavioural parity does not require identical pixels. ## Why an adapter? Mobile UI already provides genuine native linear indicators, determinate and indeterminate presentation, semantic theme colours, native animation, and accessible progress values on both platforms. Firstlight therefore keeps its own namespace and strict public contract while delegating rendering rather than duplicating those controls. The public Element Tree type remains `firstlight.progress`. Firstlight can move to package-owned renderers later without changing consumer markup if a durable cross-platform requirement genuinely outgrows the official primitive. ## Compatibility Progress supports the versions in the current [compatibility reference](https://firstlightui.dev/docs/reference/compatibility). The host application must compile the installed `nativephp/mobile-ui` iOS and Android progress renderers declared by the Firstlight adapter. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Progress on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/progress/ios-light.png) | ![Progress on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/progress/ios-dark.png) | | Android | ![Progress on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/progress/android-light.png) | ![Progress on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/progress/android-dark.png) | --- # Segmented Source: https://firstlightui.dev/docs/components/segmented Segmented presents a small set of mutually exclusive choices as a native segmented control. ## Complete example ```php public int $priority = 10; public array $priorityOptions = [ ['value' => 10, 'label' => 'Routine'], ['value' => 20, 'label' => 'Urgent', 'disabled' => true], ]; ``` ```blade ``` ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `options` | `array` | Simple strings, a value-to-label map, or rich option arrays. | | `value` / `native:model` | `string`, `int`, or `null` | Stable selected value. Its type must match the option values. | | `label` | `string` | Visible field label. | | `helper` | `string` | Supporting text below the control. | | `error` | `string` | Error text and error semantics. | | `required` | `bool` | Marks the field as required. | | `disabled` | `bool` | Disables the complete control. | | `a11y-label` | `string` | Explicit accessibility label when a visible label is inappropriate. | | `a11y-hint` | `string` | Additional VoiceOver and TalkBack guidance. | | `class` | `string` | External EDGE layout utilities. | ## Events `@change` receives the selected stable value. `native:model` normally generates this callback for property synchronisation. A user selection emits immediately; server reconciliation and programmatic updates do not emit. ## Options and accepted values Simple string options use the same value and label: ```php ['Mine', 'All'] ``` A map separates stable values from labels and may use all-string or all-integer keys: ```php ['mine' => 'Mine', 'all' => 'All'] ``` Rich options accept only `value`, `label`, and optional `disabled` fields: ```php [ ['value' => 'mine', 'label' => 'Mine'], ['value' => 'all', 'label' => 'All', 'disabled' => true], ] ``` All values in one control must be uniquely typed strings or uniquely typed integers. `null` represents no selection; an empty string remains a distinct string value. ## State timing Segmented is [server-authoritative](https://firstlightui.dev/docs/concepts/server-authoritative-state). A tap emits immediately, while PHP's next published value determines the visible selection. Use plain `native:model` or `native:model.live`; deferred `blur` and `debounce` modes are rejected. ## Disabled behaviour `disabled` prevents every choice from emitting. A rich option's `disabled: true` prevents only that option from emitting. An empty option set becomes an inert disabled control. A disabled or already-selected choice does not emit a new change. ## Accessibility Provide either a visible `label` or an explicit `a11y-label`. Firstlight warns during development when both are blank. The renderers expose the label or explicit accessibility label, optional hint, selected state, disabled state, required indication, helper text, and error text through native platform semantics. ## Validation and failure behaviour Contract exceptions still reject mixed or duplicate option values and unsupported types before publication. User validation is separate: screens that `use ValidatesFields` auto-bind the first MessageBag message for the field's `native:model` or `error-for` name. An authored `error` wins. `required` is display metadata and does not run Laravel rules. See [Validate fields](https://firstlightui.dev/docs/how-to/validate-fields). A same-typed value absent from `options` remains visibly unselected rather than selecting the first option. ## Platform behaviour iOS uses a native SwiftUI field backed by `UISegmentedControl`. Android uses Material 3 `SingleChoiceSegmentedButtonRow` and `SegmentedButton`. The controls share behaviour and authored API while retaining platform-native styling, layout, and interaction. ## Compatibility Segmented supports the versions listed in the current [compatibility reference](https://firstlightui.dev/docs/reference/compatibility) and requires both native renderers to be compiled into the host application. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Segmented on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/segmented/ios-light.png) | ![Segmented on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/segmented/ios-dark.png) | | Android | ![Segmented on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/segmented/android-light.png) | ![Segmented on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/segmented/android-dark.png) | --- # Search Field Source: https://firstlightui.dev/docs/components/search-field Search Field enters and submits one query using each platform's native search, keyboard, selection, clear, focus, and accessibility behaviour. ## Complete example ```blade ``` ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `value` / `native:model` | `string` | Published query; omission defaults to `''`. | | `placeholder` | `string` | Empty-query prompt; never the accessible name. | | `disabled` | `bool` | Prevents focus, editing, clearing, change, and submit. | | `autocapitalize` | enum string | `none`, `sentences`, `words`, or `characters`; omission keeps platform policy. | | `autocorrect` | `bool` | Explicitly enables or disables correction; omission keeps platform policy. | | `a11y-label` | non-empty `string` | Required accessible name. | | `a11y-hint` | `string` | Additional VoiceOver and TalkBack guidance. | | `class` | `string` | External EDGE layout for the complete field. | The search icon and clear button are semantic native affordances. They are not configurable icon slots. Both platforms use the package chrome `clear_search` accessibility label on the clear control. See [Localize chrome](https://firstlightui.dev/docs/how-to/localize). Search Field has no visible label, helper, error, required, read-only, secure, keyboard, content-type, size, tone, or variant API. ## Events and synchronisation `@change` receives the current query string. `@submit` first flushes any pending change and then receives the query. Submitting an empty query still calls the handler. Plain `native:model` and `.live` publish every edit. `.blur` and `.lazy` publish on focus loss or submit. `.debounce.300ms` publishes after the quiet period and flushes on focus loss or submit; durations below 50 ms fail. Native typing, selection, cursor position, and marked-text composition stay in the focused editing buffer. PHP acknowledgements do not reset focus or selection, and programmatic values received while unfocused never emit user events. The native clear action appears for a non-empty enabled query. It retains focus and immediately publishes `''` through `@change`, including under blur or debounce synchronisation. ## Validation and accessibility Values, placeholders, labels, and hints are strict strings. `a11y-label` is mandatory and cannot be blank. Unsupported general-field attributes, invalid capitalization or sync modes, and invalid debounce durations fail with actionable exceptions. The field keeps its native editable-text role, value, focus, and disabled state. The semantic search icon is decorative. The clear action has its own localized accessible name and meets the 44-point iOS or 48-dp Android target baseline. ## Platform behaviour iOS embeds `UISearchTextField`, so UIKit owns search chrome, the search and clear affordances, cursor, selection, marked text, keyboard, and Dynamic Type. Android uses a Material 3 search-field composition with `TextFieldValue`, the search IME action, Material search/clear affordances, Compose semantics, and font scaling. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Search Field on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/search-field/ios-light.png) | ![Search Field on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/search-field/ios-dark.png) | | Android | ![Search Field on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/search-field/android-light.png) | ![Search Field on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/search-field/android-dark.png) | --- # Select Source: https://firstlightui.dev/docs/components/select Select presents a collapsed, single-choice field and publishes one stable string or integer value. ## Complete example ```php public ?string $priority = null; public array $priorityOptions = [ ['value' => 'routine', 'label' => 'Routine'], ['value' => 'urgent', 'label' => 'Urgent'], ['value' => 'critical', 'label' => 'Critical', 'disabled' => true], ]; ``` ```blade ``` ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `options` | `array` | Simple strings, a value-to-label map, or rich option arrays. Labels must contain visible text. | | `value` / `native:model` | `string`, `int`, or `null` | Stable selection. Every non-null value must exactly match one option by type and value. | | `searchable` | `bool` | Forces search for a collection with 12 or fewer options. Collections with 13 or more are always searchable. | | `label` | `string` | Visible field label. | | `placeholder` | `string` | Trigger text when the published value is `null`; it is not the accessible name. | | `helper` | `string` | Supporting text shown when there is no error. | | `error` | `string` | Error text and native error semantics; replaces helper text. | | `required` | `bool` | Communicates required state; PHP still owns validation. | | `disabled` | `bool` | Disables the trigger and all options. | | `a11y-label` | `string` | Explicit accessible label when a visible label is inappropriate. | | `a11y-hint` | `string` | Additional VoiceOver and TalkBack guidance. | | `class` | `string` | External EDGE layout utilities. | ## Options and values Simple strings use the same value and label: ```php ['Routine', 'Urgent'] ``` A map separates stable values from labels and may use all-string or all-integer keys: ```php ['routine' => 'Routine', 'urgent' => 'Urgent'] ``` Rich options accept only `value`, `label`, and optional `disabled` fields: ```php [ ['value' => 10, 'label' => 'Routine'], ['value' => 20, 'label' => 'Urgent', 'disabled' => true], ] ``` Values in one collection must be unique and all strings or all integers. Matching is strict: integer `10` does not match string `'10'`. `null` means no selection and displays the placeholder. Empty options accept only `null` and produce an inert trigger. ## Compact and searchable presentation Select chooses its platform presentation from the option count. Up to 12 options use a compact menu or dropdown. Thirteen or more options use a searchable presentation automatically. Set `searchable` to force search for a smaller collection. Search filters labels locally in authored order. Typing, dismissing search, or selecting the accepted value does not publish an event. The iOS searchable sheet Done action is package chrome; see [Localize chrome](https://firstlightui.dev/docs/how-to/localize). ## Events and state timing `@change` receives the proposed stable scalar with its authored PHP type. `native:model` normally generates this callback for property synchronisation. Select is [server-authoritative](https://firstlightui.dev/docs/concepts/server-authoritative-state). Choosing an enabled option emits immediately, but the trigger continues to show PHP's accepted value until the next publication. Further choices are ignored while that proposal is in flight. Rejected and programmatic publications do not emit. An identical PHP publication must also release the pending-interaction guard. Select therefore requires a NativePHP build containing identical-publication delivery for custom EDGE renderers. Use plain `native:model` or `native:model.live`; deferred `blur` and `debounce` modes are rejected. ## Disabled, required, and error behaviour `disabled` prevents the trigger and every option from emitting. A rich option's `disabled: true` affects only that choice. `required` communicates state but does not add client-side validation. Publish `error` from PHP when validation fails; it replaces helper text on both platforms. ## Accessibility Provide either a visible `label` or an explicit `a11y-label`. Firstlight warns during development when both are blank. The trigger exposes its accessible name, accepted value or placeholder, hint, required state, disabled state, and error. Option rows expose labels, selection, and disabled state. Native search retains editable-text semantics, labels wrap at accessibility sizes, and controls preserve platform minimum interaction targets. ## Validation and failure behaviour Contract exceptions still reject malformed options, type mismatches, and unsupported presentation props before publication. User validation is separate: screens that `use ValidatesFields` auto-bind the first MessageBag message for the field's `native:model` or `error-for` name. An authored `error` wins. `required` is display metadata and does not run Laravel rules. See [Validate fields](https://firstlightui.dev/docs/how-to/validate-fields). ## Platform behaviour iOS uses a SwiftUI `Menu` for compact collections and a native searchable sheet with checkmarked rows for searchable collections. Android uses a Material 3 exposed dropdown for compact collections and a searchable Material dialog with radio rows for searchable collections. Both preserve the same stable-value, disabled-option, threshold, publication, and stale-selection semantics. ## Compatibility Select supports the versions listed in the current [compatibility reference](https://firstlightui.dev/docs/reference/compatibility), requires both native renderers in the host application, and currently depends on the unreleased NativePHP identical-publication fix described above. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Select on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/select/ios-light.png) | ![Select on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/select/ios-dark.png) | | Android | ![Select on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/select/android-light.png) | ![Select on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/select/android-dark.png) | --- # Slider Source: https://firstlightui.dev/docs/components/slider Slider selects one numeric value from a finite, evenly spaced range using the native platform slider. PHP owns the accepted value; the platform owns a draft only while the user is interacting. ## Complete example ```blade ``` Use Blade's `:` binding for numeric literals so PHP receives actual numbers, not numeric strings. The bound property receives a PHP `float`, including when the authored range and step contain only whole numbers: ```php public float $dose = 5.0; ``` ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `value` / `native:model` | finite `int|float` | Required accepted value. Published to native renderers as a float. | | `min` | finite `int|float` | Required inclusive lower bound. | | `max` | finite `int|float` | Required inclusive upper bound. | | `step` | finite positive `int|float` | Grid spacing from `min`; defaults to `1`. | | `label` | `string` | Visible field label and accessibility-name fallback. | | `helper` | `string` | Supporting guidance below the control. | | `error` | `string` | Validation feedback that replaces helper text. | | `disabled` | `bool` | Prevents native editing and publication. | | `a11y-label` | `string` | Explicit accessible name when no visible label is appropriate. | | `a11y-hint` | `string` | Additional VoiceOver and TalkBack guidance. | | `a11y-value` | `string` | Optional spoken value, such as a number with units; it is never shown visibly. | | `class` | `string` | External EDGE layout for the complete field. | `min` must be less than `max`. The accepted value must be inside the inclusive bounds, and both the accepted value and range width must lie on the `step` grid originating at `min`. Firstlight rejects numeric strings, booleans, null, non-finite values, values outside native Float range, off-grid values, and grids larger than Material's signed 32-bit interval limit. It never clamps or coerces authored props. Decimal grid membership uses a `1e-9` epsilon only to tolerate binary floating-point representation noise such as `0.1 + 0.2`. It does not relax the public step grid. ## Events and synchronisation `@change` and `native:model` deliver one standard PHP float proposal. Slider supports three policies: - `native:model` or `native:model.live` publishes changed grid values while the gesture moves. - `native:model.blur` keeps the draft native and publishes once when the gesture ends. - `native:model.debounce.300ms` publishes after the configured quiet period and flushes the final change when the gesture ends. The minimum delay is 50 ms. There is no parallel input, click, press, or submit event. Programmatic PHP publications emit nothing. A publication is authoritative and replaces the native draft, including when PHP rejects a proposal and keeps the prior value. NativePHP Mobile 4.2.0 must expose a publication acknowledgement even when the accepted value is identical for that rejection path to be observable by the renderer. Slider's component-release evidence remains blocked until that upstream runtime behaviour is available and verified ([#365](https://github.com/NativePHP/mobile-air/issues/365)); the renderer already reconciles every publication it receives. ## Accessibility and platform behavior Contract exceptions still reject invalid numeric grids before publication. User validation is separate: screens that `use ValidatesFields` auto-bind the first MessageBag message for the field's `native:model` or `error-for` name. An authored `error` wins. See [Validate fields](https://firstlightui.dev/docs/how-to/validate-fields). A visible `label` or explicit `a11y-label` is required during development. Error text replaces helper text, without replacing the accessible name or current value. `a11y-value` can add units or domain language; otherwise the native numeric value is announced. Both renderers expose disabled and error semantics and retain native adjustable-slider behavior. iOS uses a genuine stepped SwiftUI `Slider`. Android uses a genuine Material 3 `Slider`; the validated interval count is translated to Material's count of interior steps. Both platforms snap native gesture noise back to the authored grid and send the standard Float slider event. Slider deliberately has no range mode, vertical orientation, marks, ticks, visible value label, formatter, min/max captions, required metadata, size, variant, or colour/style escape props. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Slider on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/slider/ios-light.png) | ![Slider on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/slider/ios-dark.png) | | Android | ![Slider on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/slider/android-light.png) | ![Slider on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/slider/android-dark.png) | --- # Stepper Source: https://firstlightui.dev/docs/components/stepper Stepper moves a numeric model by one bounded step at a time. PHP remains the accepted source of truth: a tap proposes the adjacent value and the native control continues showing the accepted value until PHP publishes again. ```blade ``` Use Blade's `:` binding for numbers. Literal HTML attributes such as `min="0"` are strings and are rejected deliberately. ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `value` / `native:model` | finite `int|float` | Required accepted value. | | `min` | finite `int|float` | Required inclusive lower bound. | | `max` | finite `int|float` | Required inclusive upper bound. | | `step` | finite positive `int|float` | Grid spacing from `min`; defaults to integer `1`. | | `label` | `string` | Visible field label and accessible-name fallback. | | `helper` | `string` | Supporting guidance below the control. | | `error` | `string` | Validation feedback that replaces helper text. | | `disabled` | `bool` | Prevents decrement and increment proposals. | | `a11y-label` | `string` | Explicit accessible name when no visible label is appropriate. | | `a11y-hint` | `string` | Additional VoiceOver and TalkBack guidance. | | `class` | `string` | External EDGE layout for the complete field. | `min` must be less than `max`. The accepted value must be within the inclusive bounds, and both it and `max` must lie on the `step` grid originating at `min`. Numeric strings, booleans, null, non-finite values, native Float overflow, off-grid values, and grids larger than the signed 32-bit interval limit are rejected. Values are never clamped or coerced. ## Exact PHP number types Stepper preserves intentional PHP number kinds. If `value`, `min`, `max`, and `step` are all integers, proposals are integers. If any one is a float, all numeric tree props and proposals are floats, even an integral value such as `5.0`. An omitted step defaults to integer `1` for an otherwise integer model. ```php public int $quantity = 5; public float $dose = 5.0; ``` Decimal grid comparisons tolerate only normal binary floating-point noise such as `0.1 + 0.2`; the public value still has to be on the authored grid. ## Events and server authority Use `@change`, plain `native:model`, or `native:model.live`. A decrement or increment sends one adjacent precomputed value through the standard callback. Blur and debounce modes are rejected because a step is already an immediate, discrete action. After a proposal, another tap is ignored until the server publishes the node again. This prevents stale rapid taps from calculating against an old accepted value. The control does not change optimistically and performs no arithmetic on the device. NativePHP Mobile 4.2.0 still needs a content-independent publication acknowledgement from the bundled PHP runtime when PHP rejects a proposal and republishes an identical accepted value. Stepper remains release-blocked on that upstream behaviour ([#365](https://github.com/NativePHP/mobile-air/issues/365)); accepted changes already reconcile normally. ## Accessibility and platform behavior Contract exceptions still reject invalid numeric grids before publication. User validation is separate: screens that `use ValidatesFields` auto-bind the first MessageBag message for the field's `native:model` or `error-for` name. An authored `error` wins. See [Validate fields](https://firstlightui.dev/docs/how-to/validate-fields). Provide a visible `label` or an explicit `a11y-label`. VoiceOver and TalkBack receive the accepted value, hint, helper or error, disabled state, and named decrease/increase actions. At a boundary, only the unavailable direction is disabled. iOS uses the genuine SwiftUI Stepper. Android uses Material 3 icon buttons in the idiomatic minus/value/plus arrangement. Stepper deliberately has no custom icons, formatter, min/max captions, wraparound, orientation, long-press acceleration, required state, size, variant, colour, or style escape props. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Stepper on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/stepper/ios-light.png) | ![Stepper on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/stepper/ios-dark.png) | | Android | ![Stepper on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/stepper/android-light.png) | ![Stepper on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/stepper/android-dark.png) | --- # Status Label Source: https://firstlightui.dev/docs/components/status-label Status Label presents short display-only metadata or status text as a native capsule. Use an appropriate interactive control when the interface must respond to a press. ## Complete example ```blade ``` The same authored tag renders on iOS and Android. ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `label` | non-empty `string` | Visible status text. Required. | | `tone` | `neutral`, `info`, `success`, `warning`, or `danger` | Semantic colour intent. Defaults to `neutral`. | | `a11y-label` | `string` | Replaces the visible text as the accessible name. | | `a11y-hint` | `string` | Adds supplementary VoiceOver or TalkBack context. | | `class` | `string` | External EDGE layout utilities. | ## Events and values Status Label has no value, options, model binding, or events. It is static text from the user's perspective. A later server publication may replace its label, tone, or accessibility metadata without producing a callback. Do not use `native:model`, `@change`, `@press`, `value`, `disabled`, `loading`, `error`, `required`, or `helper`. Firstlight rejects those attributes instead of silently adding interactive or field semantics. ## State timing The component renders the latest published Element Tree metadata. It keeps no native selection, editing buffer, pending proposal, or animation state. Programmatic updates reconcile by the element's stable node ID and do not emit events. ## Disabled, loading, and error behaviour Disabled, loading, and validation states do not apply to display-only status text. Choose the semantic `tone` that describes the current metadata and publish a new label or tone when that status changes. ## Accessibility The visible `label` is the accessible name by default. `a11y-label` replaces that name, and `a11y-hint` adds context. Both renderers expose static text semantics without a button, selected, disabled, or live-region role. Text follows Dynamic Type on iOS and system font scaling on Android. Long labels and accessibility sizes expand or wrap rather than truncating their meaning. Semantic theme colours are checked at render time; when a customised foreground and background are below 4.5:1 contrast, Firstlight uses whichever of black or white has the stronger ratio. ## Validation and failure behaviour A missing, `null`, empty, or whitespace-only `label` throws an `InvalidArgumentException`. An unsupported tone also throws and lists the accepted values. Malformed tone data that reaches a native renderer unexpectedly falls back defensively to `neutral` instead of crashing the host. ## Platform behaviour iOS composes SwiftUI `Text` with a `Capsule` background and native text accessibility. Android composes Material 3 `Text` in a capsule-shaped `Surface` with merged TalkBack semantics. Both use NativePHP semantic theme tokens while retaining platform-native typography, scaling, colour-scheme, and layout behaviour. ## Compatibility Status Label supports the versions listed in the current [compatibility reference](https://firstlightui.dev/docs/reference/compatibility) and requires both native renderers to be compiled into the host application. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Status Label on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/status-label/ios-light.png) | ![Status Label on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/status-label/ios-dark.png) | | Android | ![Status Label on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/status-label/android-light.png) | ![Status Label on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/status-label/android-dark.png) | --- # Text Field Source: https://firstlightui.dev/docs/components/text-field Text Field enters and edits one line of text using the native field, keyboard, autofill, selection, and accessibility behaviour of each platform. ## Complete example ```blade ``` ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `value` / `native:model` | `string` | Published PHP value; omission defaults to `''`. | | `label` | `string` | Visible label and accessibility-name fallback. | | `placeholder` | `string` | Short example; never a replacement for a label. | | `helper` | `string` | Supporting guidance below the field. | | `error` | `string` | Validation feedback that replaces helper text. | | `required` | `bool` | Communicates required metadata without performing validation. | | `disabled` | `bool` | Prevents focus, editing, and every action. | | `read-only` | `bool` | Allows native selection and copy while preventing edits and changes. | | `keyboard` | enum string | `text`, `email`, `phone`, `url`, `number`, or `decimal`. | | `content-type` | enum string | `name`, `username`, `email`, `password`, `new-password`, or `one-time-code`. | | `secure` | `bool` | Masks display without changing the stored string. | | `autocapitalize` | enum string | `none`, `sentences`, `words`, or `characters`; omission keeps platform policy. | | `autocorrect` | `bool` | Explicitly enables or disables platform autocorrection. | | `submit-label` | enum string | `done`, `go`, `next`, `search`, or `send`; omission keeps the platform default. | | `leading-icon` | `string` | Shared decorative leading icon fallback. | | `leading-icon-ios` | `IosSymbol|string` | iOS leading icon override. | | `leading-icon-android` | `AndroidSymbol|string` | Android leading icon and variant override. | | `trailing-icon` | `string` | Shared decorative or authored-action icon fallback. | | `trailing-icon-ios` | `IosSymbol|string` | iOS trailing icon override. | | `trailing-icon-android` | `AndroidSymbol|string` | Android trailing icon and variant override. | | `trailing-a11y-label` | `string` | Required accessible name when `trailing-icon` has `@press`. | | `clearable` | `bool` | Adds a native clear action while editable text is non-empty. The clear accessibility label is package chrome; see [Localize chrome](https://firstlightui.dev/docs/how-to/localize). | | `revealable` | `bool` | Adds native show/hide password state; requires `secure`. Show and hide accessibility labels are package chrome; see [Localize chrome](https://firstlightui.dev/docs/how-to/localize). | | `a11y-label` | `string` | Explicit accessible name when no visible label is appropriate. | | `a11y-hint` | `string` | Additional VoiceOver and TalkBack guidance. | | `class` | `string` | External EDGE layout for the complete field. | ## Events and synchronisation `@change` receives the current string. `@submit` flushes any pending change, then receives the same current string. An authored interactive trailing icon uses standard `@press` and receives no text argument. Plain `native:model` and `.live` publish while editing. `.blur` and `.lazy` publish on focus loss or submit. `.debounce.500ms` publishes after the quiet period and flushes on blur or submit; durations below 50 ms are rejected. Native typing, selection, cursor position, and marked-text composition remain local while focused so PHP acknowledgements do not cause keyboard jumps. ## Icons and trailing actions The shared icon is the fallback; the active platform override wins. Blade uses the exact `-ios` and `-android` suffixes above, and typed Android symbols retain their filled or outlined variant on the wire. Decorative icons are silent. An authored trailing action requires all of `trailing-icon`, `trailing-a11y-label`, and `@press`. `clearable` and `revealable` instead own their platform-native icon and localized accessibility state. These semantic actions are mutually exclusive and cannot share the trailing slot with an authored action. Clearing retains focus and immediately publishes `''` through `@change`, even under blur or debounce sync. Revealing changes presentation only and never publishes the password value. ## Validation and accessibility Contract exceptions still reject invalid types, unsupported enums, incomplete trailing actions, and conflicting affordances before publication. User validation is separate: screens that `use ValidatesFields` auto-bind the first MessageBag message for the field's `native:model` or `error-for` name. An authored `error` wins. `required` is display metadata and does not run Laravel rules. See [Validate fields](https://firstlightui.dev/docs/how-to/validate-fields). Error text replaces helper text without replacing the field's accessible name or value. Decorative icons stay hidden from assistive technology. Icon actions are separate native accessibility nodes with a minimum 44-point target on iOS and 48-dp target on Android. A visible `label` or explicit `a11y-label` is required during development. ## Platform behaviour iOS uses an Apple SwiftUI `TextField` or `SecureField` composition with system field treatment, SF Symbols, Dynamic Type, native keyboard and autofill hints, and Apple spacing. Android uses Material 3 `OutlinedTextField`, Material label and supporting/error slots, `TextFieldValue` selection and composition, keyboard/IME options, and Compose autofill content types. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Text Field on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/text-field/ios-light.png) | ![Text Field error on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/text-field/ios-dark.png) | | Android | ![Text Field on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/text-field/android-light.png) | ![Text Field error on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/text-field/android-dark.png) | --- # Text Area Source: https://firstlightui.dev/docs/components/text-area Text Area enters and edits plain multiline text using each platform's native selection, composition, focus, scrolling, autocapitalization, and autocorrection behaviour. ## Complete example ```blade ``` ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `value` / `native:model` | `string` | Published PHP value; omission defaults to `''`. | | `label` | `string` | Visible label and accessibility-name fallback. | | `placeholder` | `string` | Short empty-state guidance; never replaces a label. | | `helper` | `string` | Supporting guidance below the field. | | `error` | `string` | Validation feedback that replaces helper text. | | `required` | `bool` | Communicates required metadata without performing validation. | | `disabled` | `bool` | Prevents focus and editing. | | `read-only` | `bool` | Keeps native selection, copy, and scrolling while preventing edits. | | `min-lines` | positive `int` | Minimum visible lines; defaults to `3`. | | `max-lines` | positive `int` | Maximum visible lines before native scrolling; defaults to `8`. | | `autocapitalize` | enum string | `none`, `sentences`, `words`, or `characters`; omission keeps platform policy. | | `autocorrect` | `bool` | Explicitly enables or disables platform autocorrection. | | `a11y-label` | `string` | Explicit accessible name when no visible label is appropriate. | | `a11y-hint` | `string` | Additional VoiceOver or TalkBack guidance. | | `class` | `string` | External EDGE layout for the complete field. | `max-lines` must be greater than or equal to `min-lines`. Public Blade examples use kebab-case. The PHP element also accepts `minLines`, `maxLines`, and `readOnly` aliases. ## Events and synchronisation `@change` receives the complete current string, including newline characters. Plain `native:model` and `.live` publish while editing. `.blur` and `.lazy` publish on focus loss. `.debounce.500ms` publishes after the quiet period and flushes on blur. Durations below 50 milliseconds are rejected. Typing, selection, cursor position, marked text or IME composition, focus, keyboard, and scroll position remain native while focused. PHP acknowledgements therefore do not replace the active buffer. Different server publications wait for safe reconciliation, while unfocused programmatic updates replace the displayed string without emitting `@change`. Text Area intentionally has no `@submit`, `@press`, icons, secure mode, single-line keyboard or content hints, clear/reveal affordances, loading state, prefix, suffix, or styling escape props. ## Validation and accessibility Contract exceptions still reject invalid types, unordered line bounds, and unsupported sync modes before publication. User validation is separate: screens that `use ValidatesFields` auto-bind the first MessageBag message for the field's `native:model` or `error-for` name. An authored `error` wins. `required` is display metadata and does not run Laravel rules. See [Validate fields](https://firstlightui.dev/docs/how-to/validate-fields). A visible `label` or explicit `a11y-label` is required during development. Error text replaces helper text while preserving the current accessible name and value. Disabled and read-only states remain distinct. Native type scaling, light and dark appearance, increased contrast, Reduced Motion, and RTL remain platform-owned. ## Platform behaviour iOS uses a genuine SwiftUI `TextEditor` in Apple-native field composition. Android uses Material 3 `OutlinedTextField` with `TextFieldValue`, multiline line bounds, supporting and error slots, and Compose selection and composition. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Text Area on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/text-area/ios-light.png) | ![Text Area on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/text-area/ios-dark.png) | | Android | ![Text Area on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/text-area/android-light.png) | ![Text Area on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/text-area/android-dark.png) | --- # Switch Source: https://firstlightui.dev/docs/components/switch Switch presents one boolean setting as a native on/off control. ## Complete example Declare a boolean property on the native component, then bind it with `native:model`: ```php ``` ## Props | API | Accepted type | Purpose | | --- | --- | --- | | `value` | `bool` | Accepted value. Defaults to `false` when omitted. | | `native:model` | a boolean component property | Synchronises the accepted boolean value through NativePHP. `native:model.live` is also accepted. | | `label` | `string` | Visible setting label. | | `helper` | `string` | Supporting text below the setting. | | `error` | `string` | Error text and error semantics; shown instead of `helper` when non-empty. | | `disabled` | `bool` | Prevents interaction and change events. | | `a11y-label` | `string` | Explicit accessibility label when a visible label is inappropriate. | | `a11y-hint` | `string` | Additional VoiceOver and TalkBack guidance. | | `class` | `string` | External EDGE layout utilities. | ## Events `@change` receives the proposed boolean value. Use it when the server decides whether to accept a change: ```php public bool $notifications = false; public function updateNotifications(bool $value): void { $this->notifications = $value; } ``` ```blade ``` `native:model` normally supplies the same callback for property synchronisation. ## Accepted values Switch accepts only strict PHP booleans: `true` and `false`. Bind a literal Boolean with `:`: ```blade ``` `value="false"` is invalid because Blade passes it as a string. `null`, integers, string booleans, arrays, and objects are also rejected. ## State timing Switch is [server-authoritative](https://firstlightui.dev/docs/concepts/server-authoritative-state). A native interaction immediately emits its proposed Boolean, but the visible on/off state remains the last value published by PHP. A response that accepts the proposal changes the visible state; a response that rejects it leaves the visible state unchanged. Every server publication clears a pending proposal, including one that republishes the same value. Programmatic publications update the accepted state without emitting `@change`. Use `native:model` or `native:model.live`. Deferred `blur`, `lazy`, and `debounce` sync modes are rejected. ## Disabled behaviour When `disabled` is `true`, Switch stays visible with its accepted on/off state and cannot emit a proposal or `@change` event. ## Accessibility Provide a visible `label` or an explicit `a11y-label`; Firstlight warns during development when both are blank. The native controls expose the setting name, on/off value, disabled state, optional hint, and error semantics. SwiftUI provides a minimum 44-point target; the Material 3 row provides a minimum 48-dp target and one TalkBack switch focus stop. ## Validation and failure behaviour Contract exceptions still reject non-Boolean values, unsupported `required` or `placement`, and deferred sync modes before publication. User validation is separate: screens that `use ValidatesFields` auto-bind the first MessageBag message for the field's `native:model` or `error-for` name. An authored `error` wins. See [Validate fields](https://firstlightui.dev/docs/how-to/validate-fields). An error takes precedence over helper text and is included in accessibility semantics. ## Platform behaviour iOS uses a genuine SwiftUI `Toggle` with the native switch style and system motion. Android uses a genuine Material 3 `Switch` inside a row-owned switch semantic target, preserving Material state layers and TalkBack behaviour. The public EDGE API and server-authoritative state contract are shared; native geometry, presentation, and interaction remain platform-specific. ## Compatibility Switch supports the versions listed in the current [compatibility reference](https://firstlightui.dev/docs/reference/compatibility) and requires both native renderers to be compiled into the host application. ## Screenshots | Platform | Light | Dark | | --- | --- | --- | | iOS | ![Switch on iOS in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/switch/ios-light.png) | ![Switch on iOS in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/switch/ios-dark.png) | | Android | ![Switch on Android in light mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/switch/android-light.png) | ![Switch on Android in dark mode](https://raw.githubusercontent.com/firstlightui/nativephp/main/docs/screenshots/switch/android-dark.png) | --- # ValidatesFields Source: https://firstlightui.dev/docs/reference/validates-fields `FirstlightUI\Concerns\ValidatesFields` is a Livewire-shaped Laravel Validator layer for NativePHP EDGE screens. `FirstlightUI\NativeComponent` includes the trait. See [Validate fields](https://firstlightui.dev/docs/how-to/validate-fields) for a complete working screen. Do not place `@nativeError` next to Firstlight fields. That directive emits a sibling text node and skips native field error colour, helper replacement, and accessibility. ## Methods | Method | Contract | | --- | --- | | `validate($rules = null, $messages = [], $attributes = [])` | Validates public properties. Returns the validated array. On failure, stores the exception bag and rethrows `ValidationException`. On success, clears messages for the validated keys. | | `validateOnly($field, $rules = null, $messages = [], $attributes = [])` | Validates one key. On failure, replaces only that key. On success, forgets that key. Other bag keys stay put. | | `addError($field, $message)` | Appends one message without throwing. | | `resetValidation($field = null)` | Forgets one key, an array of keys, or the whole bag. | | `getErrorBag()` | Returns the current `Illuminate\Support\MessageBag`. | | `hasError($field)` | Whether that key currently has messages. | `$rules` may be an array, `null` (use the screen's rules), or a class name. A class name is resolved through the container when possible, then `$source->rules()`, and optionally `messages()` and `attributes()`. This is the Form Request path: Firstlight does not perform an HTTP redirect. When `$messages` or `$attributes` are empty, the trait reads the screen's `messages()` / `$messages` and `validationAttributes()` / `$validationAttributes`. Action dispatch catches `ValidationException` from `@press`, `@change`, `@submit`, and `updated{Property}` handlers, stores the bag, and republishes the screen. Other exceptions still propagate. ## Field binding During the screen's view pass, participating fields read the current bag and set their native `error` slot from the first message for the resolved name: 1. A non-empty authored `error` attribute wins and skips the bag. 2. Otherwise `error-for` (or `errorFor`) selects the bag key. 3. Otherwise the `native:model` / `native:model.*` property name is used. 4. Otherwise a compiled `__syncProperty('name')` change callback is used. An empty authored `error` is treated as unset. Fields without a resolvable name stay empty. Only the first message for a key is published. The view also receives `$errorBag` for explicit `:error="$errorBag->first('email')"` bindings. ## Participating fields These components apply bag messages when the screen uses `ValidatesFields`: [Text Field](https://firstlightui.dev/docs/components/text-field), [Text Area](https://firstlightui.dev/docs/components/text-area), [Checkbox](https://firstlightui.dev/docs/components/checkbox), [Switch](https://firstlightui.dev/docs/components/switch), [Select](https://firstlightui.dev/docs/components/select), [Segmented](https://firstlightui.dev/docs/components/segmented), [Choice Group](https://firstlightui.dev/docs/components/choice-group), [Pill Group](https://firstlightui.dev/docs/components/pill-group), [Date Picker](https://firstlightui.dev/docs/components/date-picker), [Time Picker](https://firstlightui.dev/docs/components/time-picker), [Slider](https://firstlightui.dev/docs/components/slider), and [Stepper](https://firstlightui.dev/docs/components/stepper). `required` on those fields is display metadata and does not run Laravel rules. ## Current limitations - Only public instance properties are validated. Nested and wildcard rule keys are not supported. - Call `validate()` and `validateOnly()` from actions and property hooks, not from `mount()` or `render()`. Those lifecycle methods still sit on NativePHP's generic `Throwable` overlay path, so a `ValidationException` paints the red overlay instead of field messages. - `addError()` does not throw and may run from `render()` when a fixture needs a stable failed frame. --- # Compatibility Source: https://firstlightui.dev/docs/reference/compatibility | Dependency or platform | Current requirement | | --- | --- | | PHP | `^8.4` | | NativePHP Mobile | `^4.2` | | NativePHP Mobile UI | `^0.4` | | iOS application target | iOS 18.0 or later | | Android application target | API 29 or later | | Firstlight Swift package | iOS 18 or later | | Swift tools | Swift 6.2 | These are the package's current declared floors. Firstlight does not promise support for earlier versions. Both the iOS and Android renderers are required for a public component; the same documented EDGE example works on both platforms. After changing NativePHP versions, run `composer update` and `php artisan native:install --force` so bundled PHP binaries, native project files, and plugin copies match the resolved release. ## Pre-alpha runtime limitation The Composer constraints express package and source compatibility; they do not currently mean that NativePHP Mobile 4.2.0 passes Firstlight's public release gate. The bundled PHP Element Runtime can still suppress a byte-identical tree publication before either platform renderer receives it. An identical publication still matters when PHP rejects an event or deliberately keeps the current value. Known consequences include a rejected Segmented selection remaining visible, or a server-authoritative Switch or Pill Group remaining pending and ignoring later input. NativePHP Mobile 4.1.0 shipped Android `treePublicationId` ([#280](https://github.com/NativePHP/mobile-air/pull/280)), which lets plugin renderers observe equal-tree republications **after** Kotlin has received and posted a tree. That fixes Compose equal-assignment suppression only. It does not help when the shared C runtime returns early on an identical encoded frame and never calls either platform reader — the cross-platform gap tracked in the [publication acknowledgement issue](https://github.com/NativePHP/mobile-air/issues/365). Firstlight development may still use unreleased mobile-air branches or guarded binary proofs in the showcase while validating reconciliation behaviour. Those patches must not ship as supported release dependencies. The public-alpha runtime gate remains blocked until an official NativePHP release includes a content-independent publication acknowledgement in its bundled PHP runtime (C `nphp_element_publish()` / `nphp_frame_end()` and both platform readers). See also the closed Android-only report ([#279](https://github.com/NativePHP/mobile-air/issues/279)) and the partial Android fix ([#280](https://github.com/NativePHP/mobile-air/pull/280)) for context. --- ## Key pages - [Firstlight UI](https://firstlightui.dev): a curated form and control layer for NativePHP Mobile UI. - [Components](https://firstlightui.dev/components): the component catalogue, with iOS and Android screenshots. - [Firstlight vs NativePHP Mobile UI](https://firstlightui.dev/firstlight-vs-nativephp-mobile-ui): what each is responsible for, and why they are installed together. - [Pricing](https://firstlightui.dev/pricing.md): Firstlight is free and open source under the MIT licence. There is no paid tier. ## Source - [GitHub repository](https://github.com/firstlightui/nativephp/tree/main/docs)