React and Vue
Thin components over the same CSS and behaviour as the Blade ones. For React 18+ and Vue 3.5+.
npm install github:yeppymp/admiral-ui-kit#v0.5.1
import "@admiral/ui-kit/admiral-ui.css";
import "@admiral/ui-kit/admiral-ui.js"; // dialogs, dropdowns, combobox, date picker
Add class="adm-root" to <body>, then a brand file or the CSS variables in README.md.
React
Every value has value + onValueChange; on/off controls have checked + onCheckedChange; overlays have open + onOpenChange. Named areas are props: actions, footer, toolbar, head, empty, topbarEnd.
import { Button, Combobox, Dialog, Input, PageHeader, Table } from "@admiral/ui-kit/react";
function Segments() {
const [name, setName] = useState("");
const [country, setCountry] = useState("nz");
const [open, setOpen] = useState(false);
return (
<>
<PageHeader title="Segments" actions={<Button variant="primary" icon="plus" onClick={() => setOpen(true)}>New segment</Button>} />
<Dialog title="New segment" open={open} onOpenChange={setOpen} footer={<Button variant="primary">Create</Button>}>
<Input label="Name" value={name} onValueChange={setName} required />
<Combobox label="Country" options={{ nz: "New Zealand", sg: "Singapore" }} value={country} onValueChange={setCountry} />
</Dialog>
</>
);
}
Same-page tabs take their panels as an object: <TabGroup tabs={{ a: "A", b: "B" }} panels={{ a: <A />, b: <B /> }} />.
Vue
v-model everywhere; v-model:checked for on/off controls and v-model:open for overlays. Named areas are slots.
<script setup>
import { ref } from "vue";
import { Button, Combobox, Dialog, Input, PageHeader } from "@admiral/ui-kit/vue";
const name = ref(""), country = ref("nz"), open = ref(false);
</script>
<template>
<PageHeader title="Segments">
<template #actions><Button variant="primary" icon="plus" @click="open = true">New segment</Button></template>
</PageHeader>
<Dialog title="New segment" v-model:open="open">
<Input label="Name" v-model="name" required />
<Combobox label="Country" :options="{ nz: 'New Zealand', sg: 'Singapore' }" v-model="country" />
<template #footer><Button variant="primary">Create</Button></template>
</Dialog>
</template>
Routers
Nav, Layout and Tabs take linkAs: your router’s link component. Their links then route without a page load. Give each item href or to (either works), and set active from the current route yourself.
// React Router
import { Link, useLocation } from "react-router";
<Layout name="Admin" linkAs={Link} nav={[{ items: [{ label: "Users", to: "/users", active: pathname.startsWith("/users") }] }]} />
<!-- Vue Router. linkAs="RouterLink" also works, because app.use(router) registers it by name. -->
<script setup>
import { RouterLink, useRoute } from "vue-router";
</script>
<template><Layout name="Admin" :link-as="RouterLink" :nav="nav" /></template>
- Next.js Pages Router: pass
linkAs={Link}anywhere,_app.jsxincluded. - Next.js App Router: pass
next/link’sLinkfrom a client component ("use client"), the way you’d readusePathname()for the active item anyway. A server component can’t pass a component as a prop; Next stops with “Functions cannot be passed directly to Client Components”. Every other kit component renders in server components. - Nuxt: pass the component,
import { NuxtLink } from "#components", then:link-as="NuxtLink". The name"NuxtLink"doesn’t work, because Nuxt doesn’t register it globally. - A single tab can use its own link with
ason the tab.
Server rendering (Next.js, Nuxt)
Works as is. Import admiral-ui.js in a client component (Next App Router), _app.jsx (Pages Router) or app.vue (Nuxt); it does nothing on the server. Combobox, autocomplete and date pickers wait until React or Vue has hydrated, then the kit enhances them, so the server HTML and the first client render match and ids from useId stay the same.
Nuxt’s PostCSS warns Lexical error ... l - .05 while building the kit’s CSS. Ignore it: it can’t parse the relative colours (oklch(from … calc(l - .05) c h)), and the output is unchanged.
How it differs from Blade
-
Rich text: import
@admiral/ui-kit/admiral-ui.richtext.jsas well; a bundler can’t find it on its own. The value is HTML from the editor: clean it on your server, and never render it unclean. -
Repeater: rows come from the value array. Pass
rowas a function in React, or a#rowslot in Vue; each gets{ item, index, remove }. -
Rows per page:
perPageOptionsandonPerPageChangeonPagination; there’s no form to submit. -
Validation errors,
old()values and CSRF are server ideas, so passerroryourself and post however your app does. -
Charts draw the same as Blade’s. Numbers print in English by default, the same on the server and in the browser;
setStrings({ locale: "de" })changes it. Pass[label, value]pairs when labels are numbers such as years, because an object sorts those keys. -
Layoutwith anavadds the Cmd/Ctrl+K command palette and its topbar button, as in Blade;commandPalette={false}turns it off. Palette links route throughlinkAsand close the palette. -
AuthLayoutis the card part of the Blade page; your app renders the document. -
Layoutstarts with a “Skip to main content” link, and its main area hasid="main". -
Unsaved changes: put
data-adm-warn-unsavedon your own<form>to warn when the page is closed or reloaded. Leaving through your router isn’t covered; use its own navigation guard. -
Screen-reader announcements:
window.AdmiralUI.announce("Saved")reads a message without moving focus.BulkActionsalready announces its count. -
Toasts:
toast("Saved")ortoast("Couldn't save", "destructive"). With a button:toast("Perk deleted", "success", { action: { label: "Undo", onClick: restore } }). There is noToastscomponent; the area appears on the first toast. -
Tables sort and page through
onSort/onPageChange(@sort,@page-change), not URLs. -
Text:
setStrings({ cancel: "Abbrechen", … })once at startup, or the same<meta name="adm-i18n">tag the Blade head prints. Missing keys fall back to English.
Tested
Every component renders on the server in both frameworks, using only classes the CSS defines. A React app and a Vue app drive every stateful component (42 checks each) in Chromium, Firefox and WebKit, including a combobox unmounted and remounted, options added later, and state written back into fields. Types are checked for valid use and for rejecting wrong variants, icons and missing required props.
Real apps for React Router 7, Vue Router 4, Next.js 16 (App Router and Pages Router), Nuxt 3 and Nuxt 4 (tests/apps), built from the packed kit, in Chromium, Firefox and WebKit: nav and tab links route without a page load, active state follows the route and the back button, the server HTML hydrates with no warnings and the same ids, and the combobox, date picker and calendar work on the first page and after routing. The same apps check charts, toasts, the context menu, and the command palette routing and closing. The charts are checked against Blade’s: same numbers and the same markup, for 12 cases.
Not tested: React Native, and Vue 3 versions before 3.5.