July 30, 2026 — The unofficial Laracon US Day 3. Get your ticket to The Vibes

You're viewing pre-release documentation — version 4.x is in beta

Features, APIs and behaviour may change before the stable release. View the stable version (3.x)

Layouts


Overview#

Layouts wrap the screens routed beneath them with shared chrome — a top nav bar, a bottom tab bar, or both — so individual screens stay focused on their content.

A NativeLayout class declares which chrome to render, and the framework automatically wraps every screen registered under that layout with the result. Push a detail screen onto a tabs section and the chrome swaps from "tabs" to "stack" automatically; pop back and it swaps back.

Attaching a layout to a route#

Use Route::native(...)->layout(...) for a single screen, or Route::nativeGroup(...) for a set of screens that share the same chrome.

Copied!
use App\NativeComponents\Browse;
use App\NativeComponents\Home;
use App\NativeComponents\ItemDetail;
use App\NativeComponents\Layouts\StackLayout;
use App\NativeComponents\Layouts\TabsLayout;
use App\NativeComponents\Profile;
 
// Three screens that share a tab-bar layout
Route::nativeGroup(TabsLayout::class, function () {
Route::native('/tabs', Home::class);
Route::native('/tabs/browse', Browse::class);
Route::native('/tabs/profile', Profile::class);
});
 
// One screen with a stack-style top bar (back chevron + title)
Route::native('/item/{id}', ItemDetail::class)
->layout(StackLayout::class);

A screen with no layout renders without chrome — useful for splash, onboarding, or full-bleed views.

Built-in layouts#

NativePHP doesn't ship layouts in the framework — you write your own (they're tiny, see below). The sample app includes two reference layouts you can copy as a starting point:

  • App\NativeComponents\Layouts\StackLayout - Back chevron + screen title. No bottom tabs.
  • App\NativeComponents\Layouts\TabsLayout - Title bar plus a 3-tab bottom nav.

Writing a custom layout#

Extend Native\Mobile\Edge\Layouts\NativeLayout and override navBar() and/or tabBar(). Returning null from a method means "don't render that chrome."

Copied!
namespace App\NativeComponents\Layouts;
 
use Native\Mobile\Edge\Layouts\Builders\NavAction;
use Native\Mobile\Edge\Layouts\Builders\NavBar;
use Native\Mobile\Edge\Layouts\Builders\Tab;
use Native\Mobile\Edge\Layouts\Builders\TabBar;
use Native\Mobile\Edge\Layouts\NativeLayout;
use Native\Mobile\Edge\NativeComponent;
 
class SyncUpTabsLayout extends NativeLayout
{
public function navBar(NativeComponent $screen): ?NavBar
{
return NavBar::make()
->title($screen->navTitle())
->subtitle('All caught up')
->back()
->backgroundColor('#0891b2')
->textColor('#FFFFFF')
->elevation(8)
->action(NavAction::make('search')->icon('search')->press('openSearch'));
}
 
public function tabBar(NativeComponent $screen): ?TabBar
{
return TabBar::make()
->dark()
->activeColor('#0891b2')
->labelVisibility('labeled')
->add(Tab::link('Chats', '/syncup', icon: 'chat_bubble')->badge('2'))
->add(Tab::link('Friends', '/syncup/friends', icon: 'person.3.fill')->news())
->add(Tab::link('Profile', '/syncup/profile', icon: 'person'));
}
}

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.

Builder reference#

The chrome is described entirely with these fluent builders — you never place a top bar or tab bar as an element in a screen's Blade.

NavBar — the top bar#

  • make() — create a builder
  • title(?string) / subtitle(?string) — title and the small line under it
  • back(bool $show = true) — show the back chevron
  • backgroundColor(string) / textColor(string) — bar background and title/icon tint
  • elevation(int $px) — hairline thickness at the bottom of the bar
  • displayMode(string)large, inline, or automatic
  • scrollBehavior(string)collapse, pinned, or enterAlways
  • searchBar(string $placeholder = '', ?string $onQuery = null, int $debounceMs = 300) — attach a native search bar (see Search)
  • action(NavAction $action) — append a trailing action

NavAction — a top-bar button or menu#

  • make(string $id) — create an action with a unique id
  • icon(?string $name = null, ios:, android:) — a named icon, optionally per-platform
  • label(string) — visible/overflow label; a11yLabel(string) — screen-reader label for icon-only actions
  • press(string $method) — screen method to call when tapped
  • url(string) — navigate to a URL when tapped; event(string) — dispatch a native event (advanced)
  • destructive(bool = true) — render in the destructive tint
  • items(array $actions) — nest NavActions to render a pull-down menu; NavAction::divider() adds a separator

TabBar — the bottom tabs#

  • make() — create a builder; add(Tab $tab) — append a tab (up to 5)
  • activeColor(string) / backgroundColor(string) / textColor(string) — tab colors
  • labelVisibility(string)labeled, selected, or unlabeled
  • dark(bool = true) — force dark styling
  • minimizeOnScroll(bool = true) — shrink the bar as content scrolls (iOS 26)
  • highlight(string $currentUrl) — mark the active tab by longest-prefix URL match

Tab — a tab item#

  • Tab::link(string $label, string $url, icon:, ios:, android:) — a navigating tab
  • Tab::action(string $label, icon:) — a tab that calls a method instead of navigating
  • Tab::search(string $label, icon:, placeholder:) — a tab that presents a search bar
  • id(string), press(string $method), badge(string $text, ?string $color = null), news(bool = true) (red dot), active(bool = true)

Drawer navigation#

For a slide-out side drawer, mix the native-ui HasLayoutDrawer trait into your layout and return a Drawer from drawer(). The content is any Blade view, so you build the drawer's UI with normal EDGE components:

Copied!
use Nativephp\NativeUi\Builders\Drawer;
use Nativephp\NativeUi\Concerns\HasLayoutDrawer;
 
class AppLayout extends NativeLayout
{
use HasLayoutDrawer;
 
public function drawer(NativeComponent $screen): ?Drawer
{
return Drawer::make(view('native.sidebar'))
->width(320)
->reveal(); // ->modal() (dim + slide over) is the default
}
}

How chrome wraps the screen#

When a screen renders, the framework's wrapWithChrome flow:

  1. Looks up the layout class declared on the route.
  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([...]).
  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.
    • NavBar without TabBar → wrapper uses safeAreaBottom(); the NavBar handles its own top inset.
    • No bars → wrapper uses safeArea() for both edges.

The resulting element tree looks like:

Copied!
Column.fill().safeAreaTop() ← (or whichever variant applies)
├─ TopBar ← navBar
├─ <screen content> (flex-grow: 1)
└─ BottomNav ← tabBar

Don't apply safe-area to the root of a screen wrapped by a layout — the layout already handles it.

Per-screen NavBar contributions#

Screens can add actions or override the title without writing their own layout, by implementing navigationOptions():

Copied!
use Native\Mobile\Edge\Layouts\Builders\NavAction;
use Native\Mobile\Edge\Layouts\Builders\NavBarOptions;
use Native\Mobile\Edge\NativeComponent;
 
class ItemDetail extends NativeComponent
{
public function navigationOptions(): ?NavBarOptions
{
return NavBarOptions::make()
->title("Item #{$this->param('id')}")
->action(NavAction::make('save')->icon('save')->press('save'));
}
 
public function save(): void
{
// ...
}
}

Non-null fields on the returned NavBarOptions override the layout's defaults; null fields fall through. Actions are appended to whatever the layout already declared.

Per-screen titles#

Override navTitle() to give a screen its own title that the layout's NavBar can read:

Copied!
class Profile extends NativeComponent
{
public function navTitle(): string
{
return 'My Profile';
}
}

A StackLayout that calls ->title($screen->navTitle()) will then show "My Profile" automatically when this screen is on top.

Imperative state changes#

If you need to mutate the chrome at runtime — for example, to flip the title between "Edit" and "Done" — call $this->setNavBar([...]) or $this->setTabBar([...]):

Copied!
class Notes extends NativeComponent
{
public bool $editing = false;
 
public function toggleEdit(): void
{
$this->editing = ! $this->editing;
 
$this->setNavBar([
'title' => $this->editing ? 'Editing' : 'Notes',
]);
}
}

Imperative state is merged onto the layout's NavBar at the next render. Supported keys mirror the NavBar builder methods: title, subtitle, back, backgroundColor, textColor, elevation.

Inline overrides#

A screen can put its own <native:top-bar> or <native:bottom-nav> at the root of its blade, and the framework will skip the layout-supplied chrome for that slot only. This is useful for one-off screens (e.g. a chat detail with a custom titled top bar) without dropping the layout entirely.

Copied!
<native:column class="w-full h-full">
{{-- Override only the top bar — the layout's tab bar still renders --}}
<native:top-bar :title="$thread->name" />
 
<native:scroll-view class="w-full flex-1">
{{-- ... --}}
</native:scroll-view>
</native:column>