Skip to content

Frontend

Start the CSS compiler beside the application server while you work:

Terminal window
maat tailwind --watch

A new application ships with Tailwind CSS v4 and Alpine.js already wired up, and no Node anywhere in the stack. Tailwind’s utility classes style the starter kit’s pages; Alpine adds the small interactive bits — the mobile sidebar, the account dropdown — without a build step of its own.

No npm. Laravel compiles CSS with Vite and requires Node. Tailwind ships a self-contained binary, so Maat drives that instead: there is no package.json, no node_modules, and no build step other than maat tailwind.

resources/css/app.css

@import "tailwindcss" source(none);
@source "../views";
@plugin "@tailwindcss/forms";
  • @import "tailwindcss" source(none) pulls in Tailwind’s base styles, utilities and variants. source(none) turns off Tailwind v4’s automatic content detection. Without it, Tailwind walks up from this file looking for a project root and scans every file it finds there for class names — which, in this monorepo, means every package under packages/ and examples/. source(none) says “scan nothing automatically”; @source names what to scan instead.
  • @source "../views" is resolved relative to app.css itself, not to the working directory: resources/css/app.css../views is resources/views. Every .khnum.html under there is scanned for class names.
  • @plugin "@tailwindcss/forms" loads the official forms plugin, which gives unstyled <input>, <select> and <textarea> elements a sane baseline instead of the browser’s native chrome.

Add more @source lines if you keep classes outside resources/views — a shared component library, a Dart file that builds class strings dynamically.

maat tailwind

Terminal window
maat tailwind # compile once
maat tailwind --watch # recompile on every save
maat tailwind --minify # compile for production
maat tailwind --input=resources/css/admin.css --output=public/css/admin.css

This is Maat’s answer to npm run dev / npm run build. It reads resources/css/app.css and writes public/css/app.css by default; both are overridable with --input and --output, or by editing config/view.dart’s tailwind block.

Run it alongside maat serve, in a second terminal:

Terminal window
# terminal 1
maat serve
# terminal 2
maat tailwind --watch

They stay separate on purpose. Laravel keeps maat serve and npm run dev as two processes too; folding a CSS watcher into the application’s dev server would tangle two independent restart loops — one for Dart code, one for a completely different toolchain — into one.

A missing CSS entrypoint is a hard failure, not a silent no-op:

No CSS entrypoint at /path/to/app/resources/css/app.css. Create it with
`@import "tailwindcss";`, or pass --input=<path>.

The binary cache

The first run of maat tailwind on a machine downloads the platform-appropriate Tailwind standalone CLI — about 80 MB — into ~/.maat/bin/. It is cached per machine, not per project: every Maat application on the box shares the same download, keyed by version. maat new does not download it; the first maat tailwind in any project does.

Setting Env var Config Default
Cache directory MAAT_HOME $HOME (~/.maat/bin/)
Tailwind version TAILWIND_VERSION view.tailwind.version 4.3.3

Set MAAT_HOME to redirect the cache — useful in CI, where you want the download counted and cached by your pipeline rather than repeated on every run.

Alpine.js

Alpine is loaded from a CDN with a pinned version and a Subresource Integrity hash in resources/views/layouts/app.khnum.html:

<script defer
src="https://cdn.jsdelivr.net/npm/alpinejs@3.17.1/dist/cdn.min.js"
integrity="sha384-x6MR287ZDHWauvRTQIBc3b1F++s/03J/rBHbpRGwBoYbrqIN+3wRx4EXbU1u9NGO"
crossorigin="anonymous"></script>

The browser refuses the script if its bytes do not match the SHA-384 hash. When upgrading Alpine, update both the pinned version and the integrity value. The [x-cloak] rule in resources/css/app.css keeps Alpine-controlled elements hidden until Alpine initializes them.

The starter kit

A new application (not --api) ships:

  • Layoutsresources/views/layouts/app.khnum.html (authenticated shell: sidebar, mobile off-canvas nav, account menu) and layouts/guest.khnum.html (centered card, for the welcome page and anything unauthenticated).
  • Pageswelcome.khnum.html and dashboard.khnum.html, extending those layouts.
  • Componentsresources/views/components/: alert, badge, button, card, input, label, nav-link. Small, unopinionated wrappers around the same Tailwind classes used throughout the pages.

None of it is meant to survive contact with your actual application. Edit or delete any of it — the layouts, the pages, the components — the same way you would gut a fresh Laravel Breeze install. --api skips all of it: no resources/, no public/, no khnum_maat dependency, because a JSON API serves no HTML.

Serving the Compiled CSS

Link to the output with {{ asset('css/app.css') }}. Maat serves the resulting file from public/ and can point the same helper at a CDN. See Static Files and Assets for URL versioning, caching, and traversal protection.

Deployment

Compiled CSS is not committed by default (public/css/ ships only a .gitkeep), so build it as part of your deploy:

Terminal window
maat tailwind --minify

public/ has to ship with the compiled application — the native server reads public/css/app.css off disk at request time, exactly like it reads resources/views/*.khnum.html. See Deployment for the Docker specifics.

See also

  • Views & Templates — Khnum syntax, layouts, components.
  • Static Files & AssetsPublicFiles internals, path-traversal handling, asset().
  • examples/todo — a complete application already on this stack: resources/css/app.css, resources/views/, and a compiled public/css/app.css checked in.