Security and performance

What the kit does for you

Output is escaped. Component props render through Blade’s {{ }}. Client-side code writes text with textContent; server data never reaches innerHTML.

Script URLs are blocked. Every href prop goes through UiKit::safeUrl(), which turns javascript:, data: and vbscript: links into #, including disguised forms such as java\tscript:. This matters when a URL comes from user data.

Secrets stay out of the page. sensitive-input and otp never print a value, even when given one, and neither repeats old() input.

Brand values are validated. Colours, fonts and numbers from config are checked and clamped before they enter the <style> tag.

Forms are protected. form, confirm and confirm-delete add the CSRF token, and only GET, POST, PUT, PATCH and DELETE are accepted as methods.

Generated code starts safe. admin-ui:page refuses names and fields that could reach file paths or source code, uses validated input only and an allow-listed sort, and escapes % and _ in search. It doesn’t add authorization, and says so: that’s yours to add.

The gallery stays out of production. The route is off by default and refuses to load in production even if switched on.

No runtime dependencies. Nothing to audit beyond Laravel itself, and no third-party CDN.

What stays your job

  • Authorization. Hiding a button is not access control. Check permissions in the controller.
  • Sort columns. th puts the column name in the URL. Check it against an allow-list before using it in a query.
  • Uploads. file-input validates nothing. Validate type and size on the server, and don’t trust the browser’s accept.
  • autocomplete endpoints. Authenticate and authorize them like any other route.
  • {!! !!} in your own views. The kit can’t protect output it doesn’t render.

Content Security Policy

Tested with this policy in Chromium, Firefox and WebKit, with no violations:

default-src 'self'; script-src 'self' 'nonce-…'; style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'

Scripts can be strict: the kit uses no inline handlers or eval. Styles need 'unsafe-inline': chart bars, meters, skeletons and grids set sizes in style attributes, and a nonce can’t cover those. Inline styles are a much smaller risk than inline scripts. Under a stricter style-src these components lose their sizing.

The gallery page uses inline scripts and is for development only, so it doesn’t follow this policy.

Rich text and exports

Rich text. Treat everything the editor posts as untrusted: anyone can post anything to a form. Clean it on the server with RichText::clean($html, $buttons) before saving or showing it, or show it with prose, which cleans it for you. Quill 2.0.3, the latest release, has an open low-severity advisory on its HTML export (CVE-2025-15056), with no fixed version yet. The kit only writes that output to a textarea’s value, never into the page, so a React or Vue app must not render the value as HTML without cleaning it.

Exports. Authorize the export route and apply the table’s filters yourself; Export sends whatever it’s given. Text a spreadsheet would run as a formula (=, +, -, @) gets a leading apostrophe in every format. Dompdf runs with remote files and JavaScript off.

Size

AssetMinifiedGzippedCI budgetLoaded
admiral-ui.css75 KB12.6 KB13 KBAlways
admiral-ui.js15 KB4.7 KB5 KBAlways
admiral-ui.fields.js23 KB7.7 KB8 KBOnly on pages with a heavier field, such as a combobox, date picker, repeater or character counter, or a chart
admiral-ui.full.js37 KB11.4 KB11.5 KBInstead of the other two, for bundlers and inline mode
admiral-ui.richtext.js154 KB45.7 KB55 KBOnly on pages with a rich text editor; holds Quill

No web fonts or icon fonts are loaded; icons are inline SVG. For comparison, the bought theme in a real client admin is a 666 KB stylesheet plus 5 icon fonts; its login page went from 1,501 KB in 19 requests to 73 KB in 3 when rebuilt with the kit (uncompressed, excluding images and Google Fonts). CI fails if a file passes its budget.

If the fields file fails to load, those fields stay plain working inputs. A field added later by Livewire, Turbo or htmx pulls the file in when it appears.

Performance

Measured: rendering a table of 1000 rows, each with a badge, two buttons and two icons, takes about 400 ms on a laptop (0.4 ms per row) and produces 551 KB of HTML, or 5 KB gzipped. Run vendor/bin/phpunit laravel/tests/bench/RenderBench.php --group=bench. Paginate long tables; Blade components cost more than plain HTML, and this is the place to notice it.

Built in:

  • A combobox draws at most 100 matching options per keystroke, so large lists don’t freeze the page. Typing narrows the rest. Thousands of options still add page weight, so use autocomplete with a source URL for those.
  • The page watcher rescans at most once per frame, so a large Livewire update stays cheap.
  • Hover colours have no transitions, and the JS loads with defer.

Browser support

Chrome/Edge 123+, Safari 18+ and Firefox 128+. Component checks pass on those exact engines (Chromium 123, Firefox 128, WebKit 18) and on current versions. One difference: accordion name keeps only one item open from Firefox 130; on 128-129 several can stay open. WebKit isn’t the same as Safari on an iPhone. See MANUAL-CHECKS.md.