Set up Bridge UI in a Phoenix app with Inertia.js (Vue or React).
Bridge UI works with Phoenix through Inertia.js (inertia_phoenix).
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 frontend. Packages are published on npm as 0.0.3.
bun add @bridge-ui/[email protected]bun add @bridge-ui/[email protected]pnpm add @bridge-ui/[email protected]pnpm add @bridge-ui/[email protected]yarn add @bridge-ui/[email protected]yarn add @bridge-ui/[email protected]npm install @bridge-ui/[email protected]npm install @bridge-ui/[email protected]App entry
Wire Bridge UI into your Inertia client entry:
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);
},
});
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 Phoenix apps, these typically call JSON API endpoints via axios.
Below is a complete pattern you can copy: a router entry, a controller, and a small client helper.
Phoenix route
# router.ex
scope "/api/examples", MyAppWeb do
get "/users", UserSelectController, :index
end
Controller
The controller returns { label, value, description } objects compatible with Select options:
# lib/my_app_web/controllers/user_select_controller.ex
defmodule MyAppWeb.UserSelectController do
use MyAppWeb, :controller
alias MyApp.Accounts
alias MyApp.Accounts.User
def index(conn, params) do
ids = List.wrap(params["ids"]) |> Enum.reject(&(&1 in [nil, ""]))
search = params["search"] |> to_string() |> String.trim()
users =
cond do
ids != [] -> Accounts.list_users_by_ids(ids)
search != "" -> Accounts.search_users(search, limit: 20)
true -> Accounts.list_users(limit: 20)
end
json(
conn,
Enum.map(users, fn %User{} = user ->
%{
value: user.id,
label: user.name,
description: user.email
}
end)
)
end
end
Query parameters:
| Param | Purpose |
|---|---|
| search | Filter options while the user types |
| ids | Resolve labels for already-selected values |
Client helper
Create assets/js/lib/asyncSelect.ts (path may vary with your asset pipeline):
import type { SelectAsyncData, SelectValue } from "@bridge-ui/core";
import axios from "axios";
export function asyncSelect(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 { asyncSelect } from "@/examples/core/integrations/asyncSelect";
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="asyncSelect({ url: '/api/examples/users' })"
/>
</template>
import { useState } from "react";
import { asyncSelect } from "@/examples/core/integrations/asyncSelect";
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={asyncSelect({ url: "/api/examples/users" })}
/>
);
}
asyncData implies searchable. Selected values are resolved through the same endpoint with ids when the page loads or when chips need labels.
Next steps
- useModalAction — imperative modals from Inertia pages
- useDialogAction — confirm-style dialogs
- useSnackbarAction — toast notifications