Skip to content

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.

Terminal window
php spark anvil:publish

copies 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.

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');

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"].

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.

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.

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.

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-config attribute of #anvil-config,
  • the colour override uses a <style> element with the CSP nonce,
  • htmx runs with allowEval: false, allowScriptTags: false, selfRequestsOnly: true and 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.

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.