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.jsx included.
  • Next.js App Router: pass next/link’s Link from a client component ("use client"), the way you’d read usePathname() 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 as on 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.js as 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 row as a function in React, or a #row slot in Vue; each gets { item, index, remove }.

  • Rows per page: perPageOptions and onPerPageChange on Pagination; there’s no form to submit.

  • Validation errors, old() values and CSRF are server ideas, so pass error yourself 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.

  • Layout with a nav adds the Cmd/Ctrl+K command palette and its topbar button, as in Blade; commandPalette={false} turns it off. Palette links route through linkAs and close the palette.

  • AuthLayout is the card part of the Blade page; your app renders the document.

  • Layout starts with a “Skip to main content” link, and its main area has id="main".

  • Unsaved changes: put data-adm-warn-unsaved on 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. BulkActions already announces its count.

  • Toasts: toast("Saved") or toast("Couldn't save", "destructive"). With a button: toast("Perk deleted", "success", { action: { label: "Undo", onClick: restore } }). There is no Toasts component; 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.