NativePHP UI
shadcn/ui - styled native components for NativePHP Mobile SuperNative screens.
NativePHP UI
shadcn/ui for NativePHP Mobile SuperNative screens.
Write <ui:button>, <ui:card>, <ui:tabs> in your NativeComponent views and get shadcn's look — its colors, spacing, radii and dark mode — rendered as real SwiftUI and Jetpack Compose views, not HTML in a web view.
<ui:page>
<ui:card>
<ui:card-header>
<ui:card-title>Create project</ui:card-title>
<ui:card-description>Deploy your new project in one click.</ui:card-description>
</ui:card-header>
<ui:card-content>
<ui:field label="Name">
<ui:input native:model="name" placeholder="Name of your project" />
</ui:field>
<ui:switch native:model="private">Private repository</ui:switch>
</ui:card-content>
<ui:card-footer>
<ui:button variant="outline" @press="cancel">Cancel</ui:button>
<ui:button class="flex-1" :loading="$deploying" @press="deploy">Deploy</ui:button>
</ui:card-footer>
</ui:card>
</ui:page>
- 30+ components, 50+ tags
- shadcn base colors (neutral, zinc, slate, stone, gray) and accent themes (blue, green, orange, red, rose, violet, yellow)
- Automatic light / dark mode
- Configurable radius, like shadcn's
--radius - Two-way binding with
native:model - No native code, no rebuild for updates — it's pure PHP on top of NativePHP's EDGE elements
Requirements
- NativePHP Mobile v4.5+ with SuperNative (
NativeComponent) screens nativephp/mobile-ui0.8+ (installed automatically)- Android 8.0 (API 26)+, iOS 18.2+, the same minimums as
nativephp/mobile-ui
Installation
Purchase a license on the CodingwithRK Marketplace, add the marketplace repository it gives you to your composer.json, then:
composer require codingwithrk/nativephp-ui
php artisan native:plugin:register codingwithrk/nativephp-ui
Optionally publish the config:
php artisan vendor:publish --tag=nativephp-ui-config
AI skill (Claude Code & Codex)
The package ships an agent skill that teaches AI coding agents how to build screens with these components: every tag and attribute, binding, theming, testing, ready-made screen recipes and common mistakes.
-
With Laravel Boost: run
php artisan boost:installand enable Agent Skills. Boost picks the skill up from this package and installs it for every agent you use. -
Without Boost:
php artisan nativephp-ui:skill # Claude Code (.claude/skills) and Codex (.agents/skills) php artisan nativephp-ui:skill --claude # only Claude Code php artisan nativephp-ui:skill --codex # only Codex php artisan nativephp-ui:skill --force # overwrite after upgrading the package
How it works
<ui:*> tags are expanded at view-compile time into ordinary <native:*> EDGE markup (<native:row>, <native:text>, <native:icon>, …) styled with shadcn's tokens. So everything you know about EDGE still applies:
- Your classes win. A
classon a<ui:*>tag is applied after the component's own, so<ui:button class="h-12 w-full">just works. - Unknown attributes are forwarded to the native element:
ref,a11y-label,@navigate.fade='/next',@longPress,native:key, and so on. - Props can be dynamic:
:variant="$danger ? 'destructive' : 'default'". Use single-quoted strings inside:propexpressions. <ui:*>tags only compile inside native views. Web/Livewire views are left untouched.
Theming
config/nativephp-ui.php:
'base_color' => 'zinc', // neutral | zinc | slate | stone | gray
'accent' => 'violet', // null | blue | green | orange | red | rose | violet | yellow
'radius' => 10, // sm = r-4, md = r-2, lg = r, xl = r+4
'tokens' => [
'light' => ['primary' => '#4F46E5'],
'dark' => ['primary' => '#818CF8'],
],
Every token is available to your own markup as a theme class: bg-theme-ui-card, text-theme-ui-muted-foreground, border-theme-ui-border, …
Tokens: background foreground card card-foreground popover popover-foreground primary primary-foreground secondary secondary-foreground muted muted-foreground accent accent-foreground destructive destructive-foreground border input ring.
They are prefixed ui- and merged into native-ui's theme, so your app's own Material-style tokens (primary, surface, …) are untouched.
Binding state
Stateful components bind the same way native inputs do:
{{-- Two-way: reads and writes $notifications --}}
<ui:switch native:model="notifications" />
{{-- Controlled: reads $wifi, calls setWifi(bool $on) --}}
<ui:switch :checked="$wifi" @change="setWifi" />
Writes go through the same __syncProperty path as native:model, so updatedNotifications() hooks and #[Locked] behave as you'd expect.
| Component | Controlled prop |
|---|---|
switch, checkbox |
:checked |
toggle |
:pressed |
radio-group, tabs, toggle-group, accordion |
:value |
dialog, sheet, drawer |
:open (handler: @close) |
Components
Layout & typography
<ui:page>…</ui:page> {{-- scrolling screen root; :scroll="false" for a fixed one --}}
<ui:h1>Heading</ui:h1> {{-- also ui:h2, ui:h3, ui:h4 --}}
<ui:p>Body text</ui:p> {{-- also ui:lead, ui:large, ui:small, ui:muted, ui:code --}}
<ui:separator /> <ui:separator orientation="vertical" />
Button
<ui:button @press="save">Save</ui:button>
<ui:button variant="outline" size="sm" icon="plus">Add</ui:button>
<ui:button variant="ghost" size="icon" icon="more_horiz" a11y-label="More" />
<ui:button variant="destructive" :disabled="$locked" :loading="$deleting" @press="delete">Delete</ui:button>
<ui:button variant="link" @navigate='/terms'>Terms</ui:button>
Variants: default secondary destructive outline ghost link. Sizes: default sm lg icon icon-sm icon-lg. While disabled or loading, the press handler is removed.
Badge, Kbd, Spinner, Avatar, Skeleton, Progress
<ui:badge>New</ui:badge> <ui:badge variant="secondary|destructive|outline" icon="check">Verified</ui:badge>
<ui:kbd>⌘K</ui:kbd>
<ui:spinner />
<ui:avatar :src="$user->photo" :fallback="$user->initials" size="sm|default|lg|xl" />
<ui:skeleton class="h-4 w-[220]" />
<ui:progress :value="$percent" />
Card
<ui:card>
<ui:card-header>
<ui:card-title>Notifications</ui:card-title>
<ui:card-description>You have 3 unread messages.</ui:card-description>
<ui:card-action><ui:button size="sm" variant="outline">View</ui:button></ui:card-action>
</ui:card-header>
<ui:card-content>…</ui:card-content>
<ui:card-footer>…</ui:card-footer>
</ui:card>
Alert
<ui:alert icon="info">
<ui:alert-title>Heads up!</ui:alert-title>
<ui:alert-description>You can add components to your app.</ui:alert-description>
</ui:alert>
<ui:alert variant="destructive" icon="warning">…</ui:alert>
Forms
<ui:field label="Email" description="We'll never share it." :error="$emailError">
<ui:input native:model="email" keyboard="email" icon="mail" :invalid="filled($emailError)" />
</ui:field>
<ui:label>Bio</ui:label>
<ui:textarea native:model="bio" placeholder="Tell us about yourself" />
<ui:switch native:model="marketing">Marketing emails</ui:switch>
<ui:checkbox native:model="terms">Accept terms and conditions</ui:checkbox>
<ui:radio-group native:model="plan">
<ui:radio-group-item value="free">Free</ui:radio-group-item>
<ui:radio-group-item value="pro">Pro</ui:radio-group-item>
</ui:radio-group>
<ui:input> and <ui:textarea> forward everything to NativePHP's native text input: placeholder, secure, revealable, keyboard, max-length, @submit, native:model.debounce.300ms, …
Tabs
<ui:tabs native:model="tab">
<ui:tabs-list>
<ui:tabs-trigger value="account">Account</ui:tabs-trigger>
<ui:tabs-trigger value="password">Password</ui:tabs-trigger>
</ui:tabs-list>
<ui:tabs-content value="account">…</ui:tabs-content>
<ui:tabs-content value="password">…</ui:tabs-content>
</ui:tabs>
Only the active panel is rendered.
Toggle & toggle group
<ui:toggle native:model="bold" icon="format_bold" a11y-label="Bold" />
<ui:toggle-group type="single" native:model="align" variant="outline">
<ui:toggle-group-item value="left" icon="format_align_left" a11y-label="Left" />
<ui:toggle-group-item value="center" icon="format_align_center" a11y-label="Center" />
</ui:toggle-group>
type="multiple" binds an array.
Accordion
<ui:accordion native:model="faq">
<ui:accordion-item value="shipping" title="Is shipping free?">Yes, on orders over $50.</ui:accordion-item>
<ui:accordion-item value="returns" title="Can I return it?">Within 30 days.</ui:accordion-item>
</ui:accordion>
type="multiple" lets several items stay open at once.
Dialog, Sheet, Drawer
These are presented as a native bottom sheet, which is how shadcn itself shows dialogs on mobile.
<ui:dialog native:model="confirming">
<ui:dialog-header>
<ui:dialog-title>Delete project?</ui:dialog-title>
<ui:dialog-description>This action cannot be undone.</ui:dialog-description>
</ui:dialog-header>
<ui:dialog-footer>
<ui:button variant="destructive" @press="delete">Delete</ui:button>
<ui:button variant="outline" @press="cancel">Cancel</ui:button>
</ui:dialog-footer>
</ui:dialog>
<ui:sheet :open="$filtersOpen" @close="closeFilters" detents="medium,large">…</ui:sheet>
Swiping the sheet away sets the bound property back to false (or calls @close). Other attributes, such as detents, go to the native bottom sheet.
Item & Empty
<ui:item-group>
<ui:item icon="person" title="Profile" description="Name, photo, bio" chevron @navigate='/profile' />
<ui:item variant="outline" title="Two-factor authentication">
<ui:switch native:model="twoFactor" />
</ui:item>
</ui:item-group>
<ui:empty icon="folder" title="No projects yet" description="Create your first project to get started.">
<ui:button @press="create">Create project</ui:button>
</ui:empty>
Custom components
Register your own <ui:*> tag from a service provider. Extend Codingwithrk\NativephpUi\Components\Component, return native markup from render(Tag $tag) and classes from classes():
use Codingwithrk\NativephpUi\Ui;
Ui::register('stat', \App\Ui\Stat::class);
Testing
Components render through NativePHP's own test harness:
Native::test(Settings::class)
->tap('dark-mode') // ref="dark-mode" on <ui:switch>
->assertSet('darkMode', true);
License
Commercial. See LICENSE. © CodingwithRK.