diff --git a/resources/views/docs/mobile/4/digging-deeper/lifecycle-hooks.md b/resources/views/docs/mobile/4/digging-deeper/lifecycle-hooks.md
index 0b00bc41..7ba00ae6 100644
--- a/resources/views/docs/mobile/4/digging-deeper/lifecycle-hooks.md
+++ b/resources/views/docs/mobile/4/digging-deeper/lifecycle-hooks.md
@@ -166,6 +166,51 @@ class Dashboard extends NativeComponent
+## The app going to the background
+
+
+
+The hooks above follow screens. To react when the whole app leaves the foreground and comes back, listen for the
+`AppBackgrounded` and `AppForegrounded` events:
+
+```php
+use Native\Mobile\Attributes\On;
+use Native\Mobile\Events\App\AppBackgrounded;
+use Native\Mobile\Events\App\AppForegrounded;
+
+#[On(AppBackgrounded::class)]
+public function backgrounded(): void
+{
+ // Save a draft. The OS may suspend PHP right after this.
+}
+
+#[On(AppForegrounded::class)]
+public function foregrounded(int $secondsAway): void
+{
+ // Away for more than five minutes? Reload anything that may be stale.
+}
+```
+
+- `AppBackgrounded` fires when the user goes home, switches apps, or locks the device. The OS may suspend PHP right
+ after it, so save what must survive and don't start slow work.
+- `AppForegrounded` fires when the app comes back. `$secondsAway` is measured natively on a clock that keeps counting
+ while the device sleeps, which makes it a good fit for timeouts such as locking the app or refreshing stale data.
+ A cold launch does not fire it.
+
+Returning to the app doesn't call `onResume()` — that only runs when a screen is shown again after the one above it
+is popped. `AppForegrounded` is the hook for "the user is back".
+
+
+
+Both events [broadcast globally](../the-basics/events#broadcasting-globally), so you can also listen anywhere in the
+app with `Event::listen(AppBackgrounded::class, ...)`.
+
## Observing the lifecycle from outside
diff --git a/resources/views/docs/mobile/4/edge-components/bottom-sheet.md b/resources/views/docs/mobile/4/edge-components/bottom-sheet.md
index 2a192834..f0750daf 100644
--- a/resources/views/docs/mobile/4/edge-components/bottom-sheet.md
+++ b/resources/views/docs/mobile/4/edge-components/bottom-sheet.md
@@ -30,6 +30,10 @@ Per Material 3, the container color resolves from `theme.surface`. For a custom
```
@endverbatim
+Handlers like `@press="$show = true"` and `@dismiss="$show = false"` set the property directly — no component method
+needed . Keep the property in sync on `@dismiss` so the sheet opens again the
+next time you set it to `true`. See [Handlers](introduction#handlers) for the full syntax.
+
## Props
- `visible` - Whether the sheet is shown (required, boolean)
diff --git a/resources/views/docs/mobile/4/edge-components/introduction.md b/resources/views/docs/mobile/4/edge-components/introduction.md
index a87e9029..d9e0a7d6 100644
--- a/resources/views/docs/mobile/4/edge-components/introduction.md
+++ b/resources/views/docs/mobile/4/edge-components/introduction.md
@@ -73,6 +73,40 @@ Boolean attributes follow PHP truthiness. A bare attribute (`disabled`) or a bou
as you'd expect, but a literal string such as `disabled="false"` is a non-empty string and counts as `true`. Bind
booleans with `:` whenever the value can be false.
+## Handlers
+
+Event attributes such as `@press`, `@tap`, `@change`, and `@dismiss` take a handler. A handler is one of:
+
+- a method name: `@press="save"`
+- a method call with literal arguments: `@press="selectItem(@{{ $item->id }})"`
+- an assignment to a public property : `@press="$showSheet = true"`
+- a bool toggle : `@press="$expanded = !$expanded"`
+
+@verbatim
+```blade
+
+
+
+ {{-- ... --}}
+
+
+
+ Settings
+
+```
+@endverbatim
+
+The assigned value must be a literal: `true`, `false`, `null`, a number, or a quoted string. Nothing is evaluated, so
+`$open = foo()`, `$open = $other`, and `$open = !$other` don't run. A toggle only works on a property that holds a
+bool. Assignments go through the same state pipeline as [data binding](../digging-deeper/data-binding), so
+`#[Locked]` properties are refused and the `updating` / `updated` hooks fire. The same syntax works in a
+[nested component's](../the-basics/nested-components#events-up) event binding.
+
+A handler that can't run is never dropped silently. With `app.debug` on, it throws
+`Native\Mobile\Edge\Exceptions\InvalidHandlerException`, and the error screen names the handler and the component.
+With debug off, it's logged and nothing runs. That covers a method the component doesn't have, an expression handlers
+don't support, a property that isn't public, and a toggle on a property that isn't a bool.
+
## The `native:` prefix is optional
`` and `` compile to exactly the same thing — the prefix is optional. We use it throughout
diff --git a/resources/views/docs/mobile/4/edge-components/modal.md b/resources/views/docs/mobile/4/edge-components/modal.md
index 7aec6c4a..e80213c6 100644
--- a/resources/views/docs/mobile/4/edge-components/modal.md
+++ b/resources/views/docs/mobile/4/edge-components/modal.md
@@ -31,6 +31,10 @@ Per Material 3, backdrop and surface colors come from `theme.background`. The cl
```
@endverbatim
+Handlers like `@press="$show = true"` and `@dismiss="$show = false"` set the property directly — no component method
+needed . Keep the property in sync on `@dismiss` so the modal opens again the
+next time you set it to `true`. See [Handlers](introduction#handlers) for the full syntax.
+
## Props
- `visible` - Whether the modal is shown (required, boolean)
diff --git a/resources/views/docs/mobile/4/edge-components/top-bar.md b/resources/views/docs/mobile/4/edge-components/top-bar.md
index 493d3cf8..35378f28 100644
--- a/resources/views/docs/mobile/4/edge-components/top-bar.md
+++ b/resources/views/docs/mobile/4/edge-components/top-bar.md
@@ -102,6 +102,9 @@ On iOS, if more than 5 actions are provided, they collapse into an overflow menu
- `material-variant` - Material style hint for Android, e.g. `outlined` (optional). Set automatically when you use an `AndroidOutlined` enum case
- `label` - Text label. Used for accessibility and displayed in overflow menus (optional but recommended)
- `url` - A URL to navigate to when tapped (optional)
+- `transition` - The transition used when tapping navigates to `url`, as a
+ [transition](../the-basics/routing#custom-transitions) value such as `slide_from_bottom` (optional, default: `none`).
+ Only applies to an action with a `url` and no `@tap`
- `destructive` - Render in the destructive (red) tint (optional, boolean)
+To present the destination like a sheet, give the action a `transition`:
+
+@verbatim
+```blade static
+
+```
+@endverbatim
+
+The settings screen can then dismiss itself with `$this->back(Transition::SlideToBottom)`. See
+[Custom transitions](../the-basics/routing#custom-transitions).
+
### Platform icons
`` resolves icons through the same contract as [``](icon#typed-icon-enums): a
@@ -205,9 +225,12 @@ class PhotoViewer extends NativeComponent
This is equivalent to returning `NavBarOptions::make()->hidden()` from `navigationOptions()`; if both are set, the
explicit builder wins. It's the top-bar parallel to the tab bar's [`hidden()`](bottom-nav#per-screen-tab-bar).
-Navigation keeps working while the bar is hidden — pushes, pops, and `@navigate` all behave normally. With the bar
-hidden the screen is full-bleed: on Android the content extends up to the very top edge, under the transparent
-status bar; use the safe-area utilities on elements that should stay clear of the clock and status icons. On iOS
+With native chrome, the bar isn't removed: its config stays on the screen and is marked hidden, so the
+`NavigationStack` stays in place. Navigation keeps working while the bar is hidden — pushes, pops, and `@navigate` all
+behave normally.
+
+With the bar hidden the screen is full-bleed: on Android the content extends up to the very top edge, under the
+transparent status bar; use the safe-area utilities on elements that should stay clear of the clock and status icons. On iOS
the edge-swipe-back gesture is tied to the visible bar, so a pushed screen that hides it should render its own back
control (for example a floating button that uses `@navigate` to return to the parent screen).
diff --git a/resources/views/docs/mobile/4/getting-started/upgrade-guide.md b/resources/views/docs/mobile/4/getting-started/upgrade-guide.md
index bcb3c791..51829932 100644
--- a/resources/views/docs/mobile/4/getting-started/upgrade-guide.md
+++ b/resources/views/docs/mobile/4/getting-started/upgrade-guide.md
@@ -3,6 +3,41 @@ title: Upgrade Guide
order: 3
---
+## Upgrading To 4.8 From 4.7
+
+After `composer update nativephp/mobile`, run `php artisan native:install --force`. `composer update` doesn't touch
+the native iOS and Android projects, so the navigation and transition changes in 4.8 only reach your app after a
+reinstall.
+
+### Layouts use native chrome by default
+
+`NativeLayout::usesNativeChrome()` now returns `true`. If your layouts never overrode it, their nav and tab bars switch
+from the custom-drawn `Column` chrome to the platform's native renderers (`NavigationStack` / `TabView` on iOS,
+`NavHost` / `Scaffold` on Android). Inline `` and `` were already native, so this
+brings layout-supplied bars in line with them.
+
+Open a few existing screens on a device or simulator and check the chrome, especially screens that set
+`$hidesNavBar` or `$hidesTabBar`. With native chrome those bars are marked hidden instead of being removed from the
+tree.
+
+To keep the old chrome for now, override the method and return `false`:
+
+```php
+public function usesNativeChrome(): bool
+{
+ return false;
+}
+```
+
+This opt-out is temporary and will be removed. See [Native chrome](../the-basics/layouts#native-chrome).
+
+### Handlers that can't run now throw in debug
+
+Handlers such as `@press="$showSheet = true"` now work (see [Handlers](../edge-components/introduction#handlers)).
+Handlers that can't run are no longer dropped silently: with `app.debug` on they throw `InvalidHandlerException`, and
+with it off they're logged. If an error screen names a handler after you upgrade, it's usually a typo in a method name
+or an assignment whose value isn't a literal.
+
## Upgrading To 4.6 From 4.5
### Translated iOS permission strings need `supported_locales`
diff --git a/resources/views/docs/mobile/4/the-basics/events.md b/resources/views/docs/mobile/4/the-basics/events.md
index 5690b998..f4d11fe2 100644
--- a/resources/views/docs/mobile/4/the-basics/events.md
+++ b/resources/views/docs/mobile/4/the-basics/events.md
@@ -409,6 +409,8 @@ These events of the package carry the marker:
| Event | Fires when | Arguments | More |
| --- | --- | --- | --- |
+| `Native\Mobile\Events\App\AppBackgrounded` | The app goes to the background | None | [Lifecycle Hooks](../digging-deeper/lifecycle-hooks#the-app-going-to-the-background) |
+| `Native\Mobile\Events\App\AppForegrounded` | The app comes back from the background | `secondsAway` | [Lifecycle Hooks](../digging-deeper/lifecycle-hooks#the-app-going-to-the-background) |
| `Native\Mobile\Events\System\AppearanceChanged` | The system switches between light and dark | `mode` | [Theming](../digging-deeper/theming#reacting-to-changes) |
| `Native\Mobile\Events\System\OrientationChanged` | The app window turns to portrait or landscape | `orientation` | [System](system#orientation) |
| `Native\Mobile\Events\Device\ThermalStateChanged` | The thermal state of the device changes | `state`, `previous` | [Device](device#events) |
diff --git a/resources/views/docs/mobile/4/the-basics/layouts.md b/resources/views/docs/mobile/4/the-basics/layouts.md
index d755d9a2..f05b68cc 100644
--- a/resources/views/docs/mobile/4/the-basics/layouts.md
+++ b/resources/views/docs/mobile/4/the-basics/layouts.md
@@ -115,6 +115,42 @@ class SyncUpTabsLayout extends NativeLayout
The `$screen` parameter is the live `NativeComponent` instance for the current screen, so the layout can read
properties or methods on it (such as `$screen->navTitle()`) to customize the chrome per screen.
+## Native chrome
+
+
+
+Layouts render their bars with the platform's own navigation chrome: a `NavigationStack` and `TabView` on iOS, a
+`NavHost` and `Scaffold` on Android. You get edge-swipe back, predictive back, large titles, and Liquid Glass on
+iOS 26+ without any extra setup. This is controlled by `usesNativeChrome()`, which returns `true` by default.
+
+Before 4.8, the default was `false`, and a layout had to override `usesNativeChrome()` to get native chrome. A layout
+that never overrode it now renders native bars.
+
+To fall back to the custom-drawn `Column` chrome, override it and return `false`:
+
+```php
+use Native\Mobile\Edge\Layouts\NativeLayout;
+
+class LegacyLayout extends NativeLayout
+{
+ public function usesNativeChrome(): bool
+ {
+ return false;
+ }
+}
+```
+
+> [!WARNING]
+> The `Column` fallback is temporary. The switch will be removed, and native chrome will be the only option. Treat
+> `return false` as a stopgap while you move a layout over.
+
+### Hiding bars with native chrome
+
+`$hidesNavBar` / `NavBarOptions::make()->hidden()` and `$hidesTabBar` / `TabBarOptions::make()->hidden()` still hide
+a bar on one screen, but native chrome hides it differently. The `Column` path drops the bar from the tree. With
+native chrome, the bar's config is kept and marked hidden, so the `NavigationStack` and `TabView` stay in place and
+pushes, pops, and tab switches keep working across screens that hide a bar.
+
## Builder reference
The chrome is described entirely with these fluent builders — you never place a top bar or tab bar as an element
@@ -421,6 +457,13 @@ When a screen renders, the framework's `wrapWithChrome` flow:
2. Calls `$layout->navBar($screen)` and `$layout->tabBar($screen)`.
3. Merges in any `navigationOptions()` declared on the screen.
4. Merges in any imperative state set via `$this->setNavBar([...])` / `$this->setTabBar([...])`.
+
+With [native chrome](#native-chrome) (the default), the screen content is then wrapped in a native root element that
+carries the bar config as props, and the platform's navigation renderer draws the bars. You don't manage safe areas
+for the bars yourself.
+
+A layout that returns `false` from `usesNativeChrome()` takes the `Column` path instead:
+
5. Wraps the screen content in a `Column` filling the screen, with the bars stacked above and below the content.
6. Picks the right safe-area variant for the wrapper:
- **TabBar present** → wrapper uses `safeAreaTop()`; the bar handles its own bottom inset.
diff --git a/resources/views/docs/mobile/4/the-basics/nested-components.md b/resources/views/docs/mobile/4/the-basics/nested-components.md
index 7ef214e3..970ff8fc 100644
--- a/resources/views/docs/mobile/4/the-basics/nested-components.md
+++ b/resources/views/docs/mobile/4/the-basics/nested-components.md
@@ -139,6 +139,16 @@ public function onTaskToggled(string $source, string $title, bool $done): void
}
```
+A tag binding can also set one of the parent's properties instead of calling a method
+, using the same `$property = ` syntax as any other
+[handler](../edge-components/introduction#handlers). The emit's arguments are ignored:
+
+@verbatim
+```blade static
+
+```
+@endverbatim
+
**`#[On]` listeners** — a string-form `#[On('event-name')]` method fires on *any* ancestor, however deep the
emitter is. A grandchild's emit reaches the screen without the intermediate child forwarding anything:
@@ -244,6 +254,48 @@ parameters other names.
+## Skipping unchanged children with #[Memo]
+
+
+
+A screen re-runs its Blade view on every frame: every tap, keystroke, async result, or native event. Its children
+re-render with it, so a long list of extracted rows rebuilds every row even when only one changed. Add the `#[Memo]`
+attribute to a child whose output depends only on its own props and state:
+
+```php
+namespace App\NativeComponents;
+
+use Illuminate\View\View;
+use Native\Mobile\Attributes\Memo;
+use Native\Mobile\Edge\NativeComponent;
+
+#[Memo]
+class TaskRow extends NativeComponent
+{
+ public array $task = [];
+
+ public function render(): View
+ {
+ return view('native.task-row');
+ }
+}
+```
+
+When nothing the child renders from has changed, it reuses the elements it built last time and skips its Blade view.
+It re-renders when any of these change:
+
+- its public properties, both props and its own state
+- the `@event` bindings on its mounting tag
+- the public properties of any child nested inside it
+
+A child's first render and streaming renders are never memoized.
+
+> [!IMPORTANT]
+> `#[Memo]` is opt-in because it only compares props, state, and bindings. If the child's `render()` or view also
+> reads the time, the session, config, or the database, a memoized child keeps showing what it showed last. The same
+> goes for a `#[Computed]` or `#[Poll]`-driven value that changes without a public property changing. Leave
+> `#[Memo]` off those children.
+
## Lifecycle
- `mount()` runs when a child's key first appears in the parent's tree.
diff --git a/resources/views/docs/mobile/4/the-basics/routing.md b/resources/views/docs/mobile/4/the-basics/routing.md
index 67ff9e32..a743639a 100644
--- a/resources/views/docs/mobile/4/the-basics/routing.md
+++ b/resources/views/docs/mobile/4/the-basics/routing.md
@@ -53,6 +53,14 @@ $this->back();
Pops the current screen off the stack and returns to whatever was beneath it. The default transition is a slide-out
to the right.
+To dismiss a screen you presented from the bottom, pass a transition :
+
+```php
+use Native\Mobile\Edge\Transition;
+
+$this->back(Transition::SlideToBottom);
+```
+
## Replacing the current screen
```php
@@ -80,11 +88,23 @@ native experience.
## Custom transitions
-Chain `transition()` after a navigation method to override the default animation:
+`navigate()`, `back()`, and `replace()` take an optional transition that overrides the default animation
+. It's the last argument, after the URI and data:
```php
use Native\Mobile\Edge\Transition;
+$this->navigate('/settings', [], Transition::SlideFromBottom);
+$this->replace('/login', [], Transition::None);
+$this->back(Transition::SlideToBottom);
+```
+
+You can pass a `Transition` case or its string value, such as `'slide_from_bottom'`. Calls without a transition
+behave as they did before.
+
+You can also chain `transition()` after a navigation method:
+
+```php
$this->navigate('/item/42')->transition(Transition::SlideFromBottom);
```
@@ -93,6 +113,7 @@ Available cases:
- `Transition::SlideFromRight` - default for `navigate()`
- `Transition::SlideFromLeft` - default for `back()`
- `Transition::SlideFromBottom` - modal-style presentation
+- `Transition::SlideToBottom` - modal-style dismissal, the inverse of `SlideFromBottom`: the outgoing screen slides down off the bottom edge, revealing the screen beneath it. Pair the two for sheet-style present and dismiss
- `Transition::Fade` - default for `replace()`
- `Transition::FadeFromBottom` - subtle vertical fade
- `Transition::ScaleFromCenter` - zoom-in effect
@@ -100,6 +121,12 @@ Available cases:
- `Transition::Morph` - cross-fade while elements that share a `ref` move between the two screens. See [Morphing elements between screens](#morphing-elements-between-screens)
- `Transition::None` - swap with no animation
+
+
## Navigating from Blade
The `@navigate` directive triggers navigation directly from your view, with no component method required. It works on