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 | ![]() |
![]() |
| Android | ![]() |
![]() |
Open context menu:
| Platform | Light | Dark |
|---|---|---|
| iOS | ![]() |
![]() |
| Android | ![]() |
![]() |







