Styling
Anvil ships its frontend prebuilt in dist/: Bootstrap 5.3, Bootstrap Icons,
htmx, Alpine.js (CSP build) and Chart.js. Applications do not need Node.js,
and no asset is loaded from a CDN.
Publishing the assets
Section titled “Publishing the assets”php spark anvil:publishcopies anvil.css, anvil.js, anvil-theme.js, anvil-charts.js and the
icon fonts to public/assets/anvil (Config\Anvil::$assetsPath) and writes
manifest.json with a content hash per file. The hash is used for cache
busting (?v=) and
Subresource Integrity
(integrity="sha384-…"). The AppStarter runs the command after every
composer install and composer update; run it after updating Anvil.
Bootstrap and icons
Section titled “Bootstrap and icons”Panels are plain Bootstrap 5.3 markup: use Bootstrap’s classes and utilities
in your own views, pages and widgets. Icons are
Bootstrap Icons, referenced by name
without the bi- prefix wherever Anvil takes an icon:
NavigationGroup::make('Settings')->icon('gear');TextColumn::make('email')->icon('envelope');Colours
Section titled “Colours”Each panel can set its accent colour as a hex value #rrggbb; other formats
are ignored:
public function primaryColor(): ?string{ return '#0d9488';}Anvil routes Bootstrap’s primary colour (buttons, links, focus rings,
checkboxes, pagination, active navigation, progress bars) through the CSS
variables --anvil-primary and --anvil-primary-rgb. The override is
written into a <style> element with the CSP nonce.
Further variables, e.g. for a stylesheet of the application:
| Variable | Purpose |
|---|---|
--anvil-primary, --anvil-primary-rgb |
accent colour |
--anvil-body-bg |
page background |
--anvil-surface |
cards |
--anvil-sidebar-bg, --anvil-sidebar-color |
sidebar |
--anvil-border-color |
borders |
--anvil-shadow-sm |
shadow of cards |
--anvil-sidebar-width, --anvil-sidebar-collapsed-width |
width of the sidebar |
--anvil-topbar-height |
height of the top bar |
The light values are set on :root and [data-bs-theme="light"], the dark
ones on [data-bs-theme="dark"].
Light and dark mode
Section titled “Light and dark mode”Users switch between light, dark and the operating system setting in the
top bar. The choice is stored in the cookie anvil_theme and applied before
the first paint by anvil-theme.js, so the page does not flicker. Own
styles should use the variables above or Bootstrap’s (--bs-body-color,
--bs-secondary-color, --bs-*-bg-subtle), so that they work in both
modes.
Rows, cards and values are coloured with tones, the six theme colours of
Bootstrap: primary, secondary, success, info, warning, danger.
Other values throw an InvalidArgumentException (fixed values) or are
ignored (closures).
$collection // the whole row in the table, and the card ->tone(fn (Employee $e): ?string => $e->left_at !== null ? 'warning' : null) ->columns([ // a single value: per value, or by closure TextColumn::make('status')->color(['active' => 'success', 'blocked' => 'danger']), TextColumn::make('balance')->money()->color(fn ($value): ?string => $value < 0 ? 'danger' : null), ]);Tables mark toned rows with a subtle background and a coloured edge, cards
with a coloured left border; toned values get the emphasis colour of the
tone. The classes are anvil-tone, anvil-tone-<tone> and
anvil-tone-text. See Tables for columns and cards.
Content width
Section titled “Content width”The content area uses the whole window by default, so lists and card grids have room. A panel can stop it at 1440px:
public function maxContentWidth(): string{ return Panel::WIDTH_WIDE; // default: Panel::WIDTH_FULL}Forms and detail pages always stop at 1440px, so that their fields stay readable.
Own styles and scripts
Section titled “Own styles and scripts”public function stylesheets(): array{ return [base_url('css/app.css')];}
public function scripts(): array{ return [base_url('js/app.js')];}Both are URLs of the own origin. Scripts are loaded deferred before Anvil
starts Alpine, so they can register Alpine components on alpine:init;
Alpine runs in its CSP build, so templates reference properties and methods
by name, without expressions that need eval.
Do not combine Alpine’s x-show with Bootstrap’s d-* display utilities on
the same element: they are !important, so the element would never hide.
Content Security Policy
Section titled “Content Security Policy”The frontend works with a strict policy, without unsafe-inline and
unsafe-eval:
- no inline scripts or event handlers; configuration is passed as JSON in
the escaped
data-configattribute of#anvil-config, - the colour override uses a
<style>element with the CSP nonce, - htmx runs with
allowEval: false,allowScriptTags: false,selfRequestsOnly: trueand without its indicator styles, - Alpine.js uses its CSP build; components are registered in JavaScript.
Enable CodeIgniter’s CSP (app.CSPEnabled = true in .env) with a policy
like this one in app/Config/ContentSecurityPolicy.php:
default-src 'self'; script-src 'self' 'nonce-…'; style-src 'self' 'nonce-…';img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none';base-uri 'self'; form-action 'self'; frame-ancestors 'none'; script-src-attr 'none'Anvil sets X-Frame-Options, X-Content-Type-Options and
Referrer-Policy on panel pages itself, see
Security principles.
htmx and CSRF
Section titled “htmx and CSRF”All htmx requests send the CSRF token in a header. Because a page sends many
requests, the token must stay valid for the whole session:
Config\Security::$regenerate = false. Shield requires
$csrfProtection = 'session'.
htmx keeps no page snapshots in localStorage (historyCacheSize: 0), so
no sensitive data remains in the browser after logout.