- Getting Started
- Architecture
- The Basics
- Digging Deeper
-
EDGE Components
- Introduction
- Activity Indicator
- Badge
- Bottom Navigation
- Bottom Sheet
- Button
- Button Group
- Canvas
- Carousel
- Checkbox
- Chip
- Column
- Divider
- Gesture Area
- Icon
- Icons
- Image
- Layout & Styling
- List
- Menus
- Modal
- Pressable
- Progress Bar
- Radio Group
- Row
- Scroll View
- Select
- Shapes
- Side Navigation
- Slider
- Spacer
- Stack
- Tab Row
- Text
- Text Input
- Toggle
- Top Bar
- Virtual List
- Web View
- Plugins
- Testing
- Publishing Your App
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.
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 layoutRoute::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."
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 buildertitle(?string)/subtitle(?string)— title and the small line under ittitleView(Element|View)— render a custom element or Blade view in the bar's centered slot instead of the string title (a logo lockup, wordmark, …)logo(string $src, float $height = 28)— convenience overtitleView()for a bundled logo imageback(bool $show = true)— show the back chevronbackgroundColor(string)/textColor(string)— bar background and title/icon tintelevation(int $px)— hairline thickness at the bottom of the bardisplayMode(string)—large,inline, orautomaticscrollBehavior(string)—collapse,pinned, orenterAlwayssearchBar(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 idicon(?string $name = null, ios:, android:)— a named icon, optionally per-platformlabel(string)— visible/overflow label;a11yLabel(string)— screen-reader label for icon-only actionspress(string $method)— screen method to call when tappedurl(string)— navigate to a URL when tapped;event(string)— dispatch a native event (advanced)destructive(bool = true)— render in the destructive tintitems(array $actions)— nestNavActions to render a pull-down menu;NavAction::divider()adds a separator
Custom title view / logo
Use logo() for the common case — a bundled brand image in place of the string title — or titleView() for any
element tree or Blade view:
public function navBar(NativeComponent $screen): ?NavBar{ return NavBar::make() ->logo('images/logo.png'); // bundled asset, ~28pt tall // ->titleView(Image::make('images/logo.png')->height(24)) // ->titleView(view('native.brand-lockup'))}
TabBar — the bottom tabs#
make()— create a builder;add(Tab $tab)— append a tab (up to 5)activeColor(string)/backgroundColor(string)/textColor(string)— tab colorslabelVisibility(string)—labeled,selected, orunlabeleddark(bool = true)— force dark stylingminimizeOnScroll(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 tabTab::action(string $label, icon:)— a tab that calls a method instead of navigatingTab::search(string $label, icon:, placeholder:)— a tab that presents a search barid(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:
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 }}
Keyboard-aware bottom content#
Beyond the tab bar, a layout or an individual screen can pin its own content to the bottom of the screen — a chat
input, a search field, a contextual action bar. This content stays above the software keyboard automatically:
on iOS via .safeAreaInset(.bottom), on Android via the Scaffold bottom bar plus imePadding(). The main
content region sits above it and keeps its own scroll.
The most ergonomic way is an inline <native:bottom-bar> at the root of a screen's Blade. It's lifted out of the
content flow and pinned, and it overrides the layout's bottomBar() for that screen:
<native:column class="w-full h-full"> <native:scroll-view class="w-full flex-1"> @foreach($messages as $message) <native:text class="p-3">{{ $message->body }}</native:text> @endforeach </native:scroll-view> {{-- Pinned to the bottom, riding above the keyboard while typing --}} <native:bottom-bar class="p-2 glass"> <native:row class="w-full gap-2 items-center"> <native:outlined-text-input native:model="draft" placeholder="Message…" class="flex-1" /> <native:button label="Send" @press="send" /> </native:row> </native:bottom-bar></native:column>
To pin the same content across every screen under a layout, override bottomBar() instead — it returns any
element tree:
public function bottomBar(NativeComponent $screen): ?Element{ // Return an element tree, or null for no bottom bar. // An inline <native:bottom-bar> on a screen overrides this for that screen.}
Style the bar with the glass / glass-thick classes for a Liquid Glass capsule. Bottom-pinned content is only
rendered by layouts using native chrome (usesNativeChrome() is true).
How chrome wraps the screen#
When a screen renders, the framework's wrapWithChrome flow:
- Looks up the layout class declared on the route.
- Calls
$layout->navBar($screen)and$layout->tabBar($screen). - Merges in any
navigationOptions()declared on the screen. - Merges in any imperative state set via
$this->setNavBar([...])/$this->setTabBar([...]). - Wraps the screen content in a
Columnfilling the screen, with the bars stacked above and below the content. - 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.
- TabBar present → wrapper uses
The resulting element tree looks like:
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():
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:
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([...]):
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.
<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>
in no time