Skip to main content
Firstlight

Firstlight / Docs

Recipes for the screens a forms app needs, setup for your coding agent, Laravel on native screens, and accessibility.

View source
List Item

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

<firstlight:list-item
    headline="Account"
    supporting="Manage your profile and security"
    leading-icon="person"
    leading-icon-ios="person.crop.circle"
    leading-icon-android="account_circle"
    trailing-icon="chevron-right"
    trailing-icon-ios="chevron.right"
    trailing-icon-android="chevron_right"
    a11y-hint="Opens account settings"
    :disabled="! $canManageAccount"
    @press="openAccount"
/>

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 nullable string Optional secondary text; null, empty, and whitespace-only strings omit it.
leading-icon non-empty string Optional shared leading icon and cross-platform fallback.
leading-icon-ios `IosSymbol string`
leading-icon-android `AndroidSymbol string`
leading-icon-tone, trailing-icon-tone neutral, info, success, warning, danger Semantic icon colour from the active theme.
leading-icon-color, trailing-icon-color hex string Explicit icon colour overrides tone on both platforms; accepts #RGB, #RRGGBB, #AARRGGBB.
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.
leading-label non-empty string Text identity such as 4B-12, without the monogram length limit.
trailing-icon non-empty string Optional shared decorative affordance icon.
trailing-icon-ios `IosSymbol string`
trailing-icon-android `AndroidSymbol string`
trailing-text nullable string Optional short metadata; null, empty, and whitespace-only strings omit it.
trailing-status non-empty string Display-only Status Label inside the row's single target.
trailing-status-tone neutral, info, success, warning, danger Status meaning; defaults to neutral and requires trailing-status.
state normal, provisional, pending-removal Normal (default), subdued provisional text, or struck-through headline/supporting text. Does not disable actions.
state-label non-empty string Required spoken meaning for a nonnormal state.
leading-actions array of action descriptors Optional actions revealed from the logical leading edge inside List.
trailing-actions array of action descriptors Optional actions revealed from the logical trailing edge inside List.
context-menu array of menu item descriptors Optional native context menu opened by long press, with matching screen-reader actions.
disabled bool Prevents row and swipe action 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, monogram, or label. Choose at most one trailing affordance: icon, text, or status. 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.

The new attributes also accept matching camelCase aliases and fluent setters: leadingLabel(), trailingStatus(), trailingStatusTone(), state() and stateLabel().

Status and pending state

<firstlight:list-item
    headline="Alex Morgan"
    supporting="Discharge"
    leading-label="4B-12"
    trailing-status="Waiting to sync"
    trailing-status-tone="warning"
    state="pending-removal"
    state-label="Discharge waiting to sync"
    @press="openPatient(42)"
/>

provisional subdues text with a readable secondary foreground rather than making the row disabled. pending-removal strikes through the headline and supporting text but keeps the row until PHP publishes its removal. Neither state starts a job, retries sync, or changes action availability. Use disabled separately when actions must be unavailable.

The leading label and trailing status join the default spoken row name. An explicit a11y-label replaces that name; state-label is still announced as state. The status is not a nested action or separate accessibility stop. At accessibility text sizes it moves below the headline/supporting text so the headline remains readable.

Read-only summaries and comparisons

Use Mobile UI text/columns for read-only key-value content, not a List Item with a fake press handler. For a comparison, PHP decides match, mismatch or unknown; Status Label communicates the result with words as well as a mark:

<native:column class="w-full gap-2">
    <native:text class="text-theme-on-surface">Patient: {{ $patientName }}</native:text>
    <native:text class="text-theme-on-surface">Date of birth: {{ $formattedDob }}</native:text>
    <native:text class="text-theme-on-surface">Label identifier: {{ $labelIdentifier }}</native:text>
    <native:text class="text-theme-on-surface">On file: {{ $fileIdentifier }}</native:text>
    @if ($comparison === 'match')
        <firstlight:status-label label="Matches" tone="success" icon="check" icon-ios="checkmark" />
    @elseif ($comparison === 'mismatch')
        <firstlight:status-label label="Does not match" tone="warning" />
    @else
        <firstlight:status-label label="Not compared" tone="neutral" />
    @endif
</native:column>

The app supplies the display values and comparison result. This is ordinary native composition, not a new catalogue layout or validation engine.

Swipe actions

Put swipe-enabled rows inside firstlight:list, directly or in a firstlight:list-section. The same example works on both platforms:

<firstlight:list>
    <firstlight:list-item
        headline="Message"
        @press="openMessage(42)"
        :leading-actions="[
            ['method' => 'archive(42)', 'label' => 'Archive', 'icon' => 'archive'],
        ]"
        :trailing-actions="[
            ['method' => 'requestDestroy(42)', 'label' => 'Delete', 'role' => 'destructive'],
            ['method' => 'flag(42)', 'label' => 'Flag'],
        ]"
    />
</firstlight:list>

Each descriptor requires a non-empty method callback expression and visible label. Optional fields are icon (shared fallback), ios (IosSymbol|string), android (AndroidSymbol|string), and role (empty or destructive). Icon overrides require a shared icon. Labels also name the accessibility actions. Empty arrays mean no actions on that edge. leadingActions / trailingActions attribute aliases and fluent ->leadingActions([...]) / ->trailingActions([...]) are supported.

The first action is nearest its originating edge. Leading and trailing follow the reading direction, including RTL. Swipe to reveal, then tap a button: full swipes never execute actions. The ordinary @press remains independent. On Android, tapping an exposed foreground row closes its drawer; tap the closed row to invoke @press.

Swiping never removes a row. Even role="destructive" only selects semantic destructive styling; PHP decides whether to confirm, authorize, and delete. Connect a Delete action to the destructive-list helper using a stable domain key rather than a list position. Publishing updated actions or disabling the row replaces native action availability without emitting an event. Android also closes the drawer when row configuration changes.

Action colours come from the native theme's primary/destructive token pairs. Per-action tint, enabled flags, full-swipe options, and the legacy on-swipe-delete shortcut are not supported. Omit unavailable actions or disable the entire row, and always authorize sensitive operations in PHP.

Context menu

Give a row a context-menu to offer secondary commands without adding a second target. The same example works on both platforms, inside or outside List:

<firstlight:list-item
    headline="Message"
    supporting="From Alex"
    @press="openMessage(42)"
    :context-menu="[
        ['method' => 'reply(42)', 'label' => 'Reply', 'icon' => 'reply', 'ios' => 'arrowshape.turn.up.left'],
        ['method' => 'forward(42)', 'label' => 'Forward', 'icon' => 'forward', 'ios' => 'arrowshape.turn.up.right', 'disabled' => ! $canForward],
        ['method' => 'requestDestroy(42)', 'label' => 'Delete', 'icon' => 'delete', 'ios' => 'trash', 'role' => 'destructive'],
    ]"
/>

Each item requires a non-empty method callback expression and visible label. Optional fields are icon (shared fallback), ios (IosSymbol|string), android (AndroidSymbol|string), role (empty or destructive), and disabled (a real boolean, default false). Icon overrides require a shared icon. Items appear in the authored order. An empty array means no menu. The contextMenu alias and fluent ->contextMenu([...]) are supported.

Long-pressing the row opens the menu: a SwiftUI context menu with the row preview on iOS, and a Material 3 dropdown menu anchored at the touch point on Android. Choosing an enabled item closes the menu and invokes its PHP method once. Tapping the row still invokes @press; a long press never does.

Disabled items stay visible with native disabled styling and cannot be chosen. Firstlight registers no callback for them, so publishing disabled => false later is what makes an item actionable. A disabled row has no menu at all.

role="destructive" only applies native destructive styling (red text and icon). Choosing Delete does not remove the row; connect it to the destructive-list helper so PHP confirms, authorizes, and deletes.

Publishing a different menu, or disabling the row, replaces what the next menu shows without emitting an event. Android closes an open menu when that happens. An open iOS system menu can stay on screen until dismissed, but its items then emit nothing.

Context menus and swipe actions can be combined. Submenus, section titles, checkmark or selected state, keyboard shortcuts, custom previews, and per-item colours are not supported.

Events and state timing

@press is required and invokes the named PHP action once after a native user press:

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 transient press feedback and swipe reveal state. 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 emits neither @press nor swipe action callbacks. New swipes and screen-reader actions are unavailable. On iOS, an already-open system drawer can remain visible until dismissed, but tapping its retained buttons emits no callback. Android closes the drawer immediately. 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.

Set leading-icon-tone="danger" for an error category, or the corresponding trailing-icon-tone for a status glyph. Both accept the Status Label vocabulary: neutral uses on-surface-variant, info uses primary, success uses success, warning uses warning, and danger uses destructive. Warning defaults to accessible amber, not brand accent; see Theming. Without a tone, leading icons retain primary and trailing icons retain on-surface-variant. An explicit *-icon-color wins over tone on iOS and Android. CamelCase aliases and fluent leadingIconTone(), trailingIconTone(), leadingIconColor(), and trailingIconColor() are supported. These props only colour icons, not avatars, monograms, text, or swipe actions.

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, overflow menus, and other embedded targets are excluded. Swipe actions and the long-press context menu are separate from these decorative trailing affordances.

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.

Inside List, swipe actions are available through native VoiceOver actions and TalkBack custom actions without performing a swipe gesture. Revealed Android buttons also expose their visible labels and button roles.

Every enabled context menu item is also a VoiceOver action and a TalkBack custom action named by its label, so screen-reader users can run it from the Actions rotor or TalkBack actions menu without a long press. Disabled items are not offered as actions. TalkBack's own long-press action still opens the visible menu. Menu items follow the reading direction, including RTL, and their text scales with Dynamic Type and Android font scaling.

Validation and excluded APIs

Firstlight rejects missing or blank headlines, missing @press, blank accessible labels/hints, 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, malformed or unlabelled swipe or context menu descriptors, raw @longPress handlers, trailing-menu, navigation directives, model or selection state, loading and validation props, non-icon colours, elevation, typography, and per-platform layout escape hatches.

Optional supporting and trailing-text values may be null or empty, including literal empty strings. They render no slot and an empty trailing-text does not conflict with a trailing icon. Other non-string values remain invalid. Overline is not part of Firstlight's row contract.

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.

The context menu is SwiftUI .contextMenu with Button(role: .destructive) items on iOS, and Material 3 DropdownMenu / DropdownMenuItem on Android, opened by the row's long-click and anchored where the finger went down. 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. 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 List Item on iOS in dark mode
Android List Item on Android in light mode List Item on Android in dark mode

Open context menu:

Platform Light Dark
iOS List Item context menu on iOS in light mode List Item context menu on iOS in dark mode
Android List Item context menu on Android in light mode List Item context menu on Android in dark mode

Served from firstlightui/nativephp on GitHub.