Bridge UI

Laravel + Inertia

Set up Bridge UI in a Laravel app with Inertia.js (Vue or React).

Bridge UI integrates cleanly with Laravel and Inertia.js.

Use the framework selector in the site header to switch between React and Vue on framework-specific steps below.

Install packages

Add Bridge UI and icons to your Laravel project. Packages are published on npm as 0.0.3.

bun add @bridge-ui/[email protected]
bun add @bridge-ui/[email protected]

Vite entry points

Point the Laravel Vite plugin at your Inertia entry:

laravel({
  input: ["resources/css/app.css", "resources/js/vue/app.ts"],
}),

Use a Blade root (for example app-vue.blade.php) that loads this Vite entry.

laravel({
  input: ["resources/css/app.css", "resources/js/react/app.tsx"],
}),

Use a Blade root (for example app-react.blade.php) that loads this Vite entry.

App entry

Register the Bridge UI plugin in resources/js/vue/app.ts:

import { createBridgeUI } from "@bridge-ui/vue";
import { createInertiaApp } from "@inertiajs/vue3";
import { createApp, h } from "vue";

createInertiaApp({
  setup({ el, App, props, plugin }) {
    createApp({ render: () => h(App, props) })
      .use(plugin)
      .use(createBridgeUI())
      .mount(el);
  },
});

resources/js/react/app.tsx follows the standard Inertia React setup. Bridge UI is provided from the layout—no extra plugin in the entry file:

import { createInertiaApp } from "@inertiajs/react";
import { createRoot } from "react-dom/client";

createInertiaApp({
  setup({ el, App, props }) {
    createRoot(el).render(<App {...props} />);
  },
});

Layout

Wrap your Inertia layout with BridgeUIProvider and BridgeUIHosts so imperative actions (useModalAction, useDialogAction, useSnackbarAction) work across pages.

<script setup lang="ts">
import { BridgeUIProvider } from "@bridge-ui/vue";
import { BridgeUIHosts } from "@bridge-ui/vue/Actions";
</script>

<template>
  <BridgeUIProvider :global="{}" :components="{}">
    <BridgeUIHosts
      :modal="{ modal: { transition: 'fade' } }"
      :dialog="{ modal: { transition: 'fade' } }"
      :snackbar="{ max: 3, timeout: 5000, position: 'bottom-center' }"
    >
      <slot />
    </BridgeUIHosts>
  </BridgeUIProvider>
</template>
import { BridgeUIProvider } from "@bridge-ui/react";
import { BridgeUIHosts } from "@bridge-ui/react/Actions";
import type { PropsWithChildren } from "react";

const Layout = ({ children }: PropsWithChildren) => (
  <BridgeUIProvider global={{}} components={{}}>
    <BridgeUIHosts
      modal={{ modal: { transition: "fade" } }}
      dialog={{ modal: { transition: "fade" } }}
      snackbar={{ max: 3, timeout: 5000, position: "bottom-center" }}
    >
      {children}
    </BridgeUIHosts>
  </BridgeUIProvider>
);

export default Layout;

Select async data

The Select component accepts an asyncData object with search and resolve callbacks. In Laravel apps, these typically call JSON API routes via axios.

Below is a complete pattern you can copy: a JSON route, a controller, and a small client helper.

Laravel route

// routes/web.php
use App\Http\Controllers\Examples\UserSelectController;

Route::get('/api/examples/users', UserSelectController::class);

Controller

The controller returns { label, value, description } objects compatible with Select options:

<?php

namespace App\Http\Controllers\Examples;

use App\Models\User;
use Illuminate\Http\Request;

class UserSelectController extends Controller
{
    public function __invoke(Request $request)
    {
        $ids = $request->collect('ids')->filter()->values();

        return User::query()
            ->select('id', 'name', 'email')
            ->when($ids->isNotEmpty(), function ($query) use ($ids) {
              $query->whereIn('id', $ids);
            })
            ->when($ids->isEmpty() && $request->filled('search'), function ($query) use ($request) {
                $search = $request->string('search');

                $query->where('name', 'like', "%{$search}%")
                  ->orWhere('email', 'like', "%{$search}%");
            })
            ->orderBy('name')
            ->limit(20)
            ->get()
            ->map(fn (User $user) => [
                'value' => $user->id,
                'label' => $user->name,
                'description' => $user->email,
            ]);
    }
}

Query parameters:

Param Purpose
search Filter options while the user types
ids Resolve labels for already-selected values

Client helper

Create resources/js/lib/laravelSelect.ts:

import type { SelectAsyncData, SelectValue } from "@bridge-ui/core";
import axios from "axios";

export function laravelSelect(options: { url: string }): SelectAsyncData {
  const { url } = options;

  return {
    limit: 20,
    resolve: async (values: SelectValue[]) => {
      const { data } = await axios.get(url, {
        params: { ids: values },
        headers: { Accept: "application/json" },
      });

      return data;
    },
    search: async (query) => {
      const params: Record<string, string> = {};
      if (query) params.search = query;

      const { data } = await axios.get(url, {
        params,
        headers: { Accept: "application/json" },
      });

      return data;
    },
  };
}

Usage in a page

<script setup lang="ts">
import { laravelSelect } from "@/examples/core/integrations/laravel/laravelSelect";
import { Select } from "@bridge-ui/vue/Components/Select";
import { ref } from "vue";

const userId = ref(null);
</script>

<template>
  <Select
    label="User"
    v-model="userId"
    placeholder="Type to search users..."
    :async-data="laravelSelect({ url: '/api/examples/users' })"
  />
</template>
import { useState } from "react";

import { laravelSelect } from "@/examples/core/integrations/laravel/laravelSelect";
import { Select } from "@bridge-ui/react/Components/Select";

export default function UserSelect() {
  const [userId, setUserId] = useState<null | number | string>(null);

  return (
    <Select
      label="User"
      value={userId}
      onChange={setUserId}
      placeholder="Type to search users..."
      asyncData={laravelSelect({ url: "/api/examples/users" })}
    />
  );
}

asyncData implies searchable. Selected values are resolved through the same route with ids when the page loads or when chips need labels.

For SPAs without a backend, pass inline callbacks instead—see the Async data playground on the Select page.

Next steps