# Coolify UI design system This document defines Coolify's UI design system for its Livewire + Blade + Alpine + Tailwind v4 frontend. The visual system covers the global shell, project and environment pages, application navigation, settings surfaces, tables, modals, toasts, terminals, and metrics. Use this file as the source of truth for frontend design work. Update it in the same change whenever a new shared visual pattern or component is introduced. Onboarding validation and live server validation checkpoints share `` (idle / pending / running / success / error) inside a compact divided list, not legacy green check SVGs or fixed-width status rows. > **Maintainer rules** > > - Keep the work frontend-focused unless existing data must be exposed to the > view. > - Preserve routes, Livewire bindings, permissions, confirmations, and working > interactions while changing layout and presentation. > - Add or update tests when a UI change affects behavior. Follow the testing > requirements in `AGENTS.md`. > - Validate Blade with `./scripts/dev exec php artisan view:cache`, then clear > it with `./scripts/dev exec php artisan view:clear`. > - Build frontend assets with `npm run build`. > - Use existing components before adding another styling abstraction. --- ## 1. Visual direction The interface is compact and product-focused: - near-neutral layered surfaces instead of large bordered boxes; - 13–14px UI typography and 32px controls; - crisp hairline rings plus a restrained card lift (single 1px ring + `0 1px 2px rgb(0 0 0 / 0.05)`) so cards and tables separate from the canvas, never heavy borders or a strong floating shadow; - full-width data tables for dense collections; - outline Reicon glyphs through ``; - the Coolify purple brand accent in light mode; - the readable Coolify yellow accent in dark mode; - solid active-item fills (neutral black/white opacity), not accent gradients; active state is a flat selected surface with no accent rail; - sentence-case labels and headings; - never use the em dash (`—`) in UI copy. Prefer a period, colon, comma, or ASCII hyphen (`-`) for empty cells and separators. Avoid oversized titles, generic dashboard cards, strong shadows, thick dividers, native browser selects, and isolated colored buttons that do not match the current action styles. Standard `.button` controls use a compact 2px bottom depth. Hover raises the button face by 1px and increases the visible depth to 3px. Pressing moves the face down 2px into the edge and removes the depth until release, keeping the overall bottom position stable. Disabled controls stay flat, and focus-visible controls retain the accent ring alongside the depth. Movement and depth-shadow changes transition over 80ms; color transitions keep the shared 120ms duration. Standard button labels use `capitalize`, giving each word an initial capital. Highlighted buttons use `--color-coollabs-300` for their bottom edge; the custom theme derives it from its bright color mixed with black, so custom colors keep a matching edge. Dark mode matches the depth edge of neutral buttons to their regular border color (`rgb(255 255 255 / 0.08)`). --- ## 2. Development and cascade notes PHP runs in this branch's Coolify container (`./scripts/dev exec …`). The main checkout serves the app at `http://localhost:8000` with Vite on `5173`; worktrees use the port block printed by `./scripts/dev urls`. `resources/css/app.css` still contains unlayered global element rules for headings, labels, and tables. Tailwind utilities are layered, so the unlayered rules can win unexpectedly. The settings and dense-surface CSS therefore lives as plain unlayered CSS near the end of `resources/css/app.css`, beginning at: ```css /* Coollabs layer-card settings surfaces */ ``` Important consequences: - scope settings forms with `.application-settings-form` or `.application-settings-workspace`; - add shared surface overrides to the unlayered block instead of stacking `!important` utilities; - listbox panels require ancestors with `overflow: visible`; - anchored cards use `scroll-margin-top: 7rem` to clear the fixed topbar and the page's top padding; - modal shells reuse the layer-card classes but keep content-width sizing on desktop; - Alpine code inside quoted Blade attributes must not introduce conflicting quote characters. --- ## 3. Tokens and color behavior The surface ladder is defined in `resources/css/app.css`. | Token | Light | Dark | Use | |---|---|---|---| | `--coollabs-canvas` | 97% off-white | 14.48% neutral | page canvas (kept below card fills so cards lift) | | `--coollabs-elevated` | 98% neutral | 20.02% neutral | shells and card headers | | `--coollabs-base` | white | 22.64% neutral | nested card bodies | | `--coollabs-recessed` | 96% neutral | 25.2% neutral | inputs and listboxes | | `--coollabs-fill` | 92.2% neutral | 29.31% neutral | dividers and passive fills | | `--coollabs-line` | translucent dark | 32% neutral | control borders | | `--coollabs-hairline` | 85.5% neutral | 32% neutral | shell rings (crisp enough to read as a card edge, ~1.5:1) | | `--coollabs-subtle` | 50% neutral | 70.8% neutral | labels and muted titles (light darkened for WCAG AA 4.5:1) | Accent behavior is intentionally theme-aware: - **Light mode:** Coolify purple (`coollabs`) for active controls, focus, primary actions, and navigation accents. - **Dark mode:** Coolify yellow (`warning`) for the same states because the original purple did not provide sufficient text and ring contrast. Do not hard-code blue focus rings or leave yellow accent utilities active in light mode. Primary action patterns should normally follow: ```html bg-coollabs/10 text-coollabs ring-coollabs/25 dark:bg-warning/15 dark:text-warning dark:ring-warning/25 ``` The filled top-level action/tab treatment uses the same palette at a restrained opacity rather than a fully saturated fill. ### Shell layering The app shell is three distinct surface layers, not one flat color. Chrome lifts, content is the base, cards lift off the content: - **Content canvas** is the base layer: `bg-app` in dark (deepest, `--color-app` `oklch(14.48% 0 0)`, sRGB 10), `bg-neutral-50` in light. The `
` content area and page body use it. - **Sidebar and topbar chrome** use `bg-panel` in dark (`--color-panel` `oklch(19.13% 0 0)`, sRGB 20, a clear step lighter than the content canvas) and `bg-white` in light, so the chrome reads as a separate panel from the content. Dark surface tokens are exact oklch equivalents of chosen sRGB steps. oklch lightness compresses toward pure black below ~15%, so do not pick dark values by round oklch percentages. The dark ladder is `--color-app` 10, `--coollabs-elevated` 22, `--coollabs-base` 28, `--coollabs-recessed` 34 (sRGB), which reads as distinct surfaces. Temperature: every panel is **pure neutral gray** (r=g=b), one consistent temperature across the sidebar, tables, cards, inputs, dividers, borders, and text, in both modes. Do not give one surface a cool (blue) or warm cast while the others stay neutral. The light page canvas uses `bg-neutral-50` (not `bg-gray-50`, which is faintly cool) so it matches the neutral cards and chrome. The only intentional color is the purple/yellow brand accent. - **Cards, tables, and collection tiles** lift off the content canvas with `dark:bg-white/[0.05]` plus the crisp `--coollabs-hairline` ring; in light they are `bg-white` with the ring and the restrained card lift. Do not paint the content area with the same `bg-panel` as the sidebar, and do not drop card fills below `dark:bg-white/[0.05]`; both make surfaces read as one color. Row-hover states keep the lighter `dark:hover:bg-white/[0.025]`. --- ## 4. Page shells and navigation ### Global shell - Main sidebar groups are compact, use outline Reicons, and keep a 32px row height. - Active sidebar rows are rounded pills (`rounded-md`) with a solid neutral selected fill (`bg-black/5` light, `bg-white/6` dark) and no accent rail. Hover rows use the same radius. Do not use accent-tinted gradients on nav rows; yellow washes look muddy on dark UI. - Nested items sit behind a thin 1px guide line (`.nav-children`) and use the same selected pill, not a thick box border. - The update badge sits on the version row and uses a tiny fully rounded primary-action pill. ### Resource navigation Application, service, database, and server pages have no second navigation bar; content starts directly below the 48px global topbar. Every route in a resource family (settings pages, backups, logs, terminal, metrics, danger zone) is an entry in its grouped settings sidebar. Do not add a tab row, a large in-flow resource heading, or legacy `.navbar-main` tabs to one resource type. Keep route-derived active state in Blade/Livewire. Do not rely only on Alpine state because it can disappear after polling or a Livewire morph. The global topbar owns the current resource identity, its compact status badges, configuration warnings (`#configuration-warning-hud-slot`), and the resource actions (`#resource-action-hud-slot`). If a resource is missing from `x-top-breadcrumb`, extend the global topbar instead of repeating its name or status summary in the page. Below `xl` the resource heading repeats the name, status, and links in-flow because the desktop HUD is hidden there. Desktop resource actions dock in `#resource-action-hud-slot` (visible from `xl`) as one ``. The main button is the primary action for the current state (Deploy, Restart, Start, Restart Proxy); the caret opens a menu with the secondary actions: Deploy (without cache) and Restart on applications, Pull latest and restart / Force Restart / Force Deploy / Force Cleanup Containers on services, Refresh Proxy Status on servers. Stop is the last menu item in the error color. Stop, restart, and removal items open the existing confirmation modals. Do not add a separate Advanced dropdown or collapse actions into an overflow menu. Place Links (`x-applications.links`, `x-services.links`) or the server Traefik Dashboard link immediately before the split action; Links stay a separate dropdown because the URL list is unbounded. Below `xl` the same split action renders full width under the in-flow resource name. A resource that cannot deploy yet shows a single Actions dropdown that explains why. The only fixed tab strip left is `x-dashboard.navbar`, and it renders only when a page has at least two real sibling routes or header actions. Never repeat main-sidebar destinations such as Dashboard, Projects, Terminal, Servers, Sources, Destinations, or Storage as a tab row. A single collection page does not need a tab just to fill the bar; keep its primary action in the page header. A tab must be active on the page that renders it. A bar whose only tab points at a different route reads as broken navigation, so project and environment pages (`project.show`, `project.edit`, `project.environment.edit`, `project.clone-me`) carry a plain page header with a 24px title and a 13px muted summary instead of a bar. The environment identity and the way back to its resources already live in `x-top-breadcrumb`; do not restate them in a sub-header. The dashboard is a compact overview, not a metrics wall. It has no page header: live active deployments come first as a compact table, followed by traffic analytics when a server has it enabled, then two full-width sections (Projects, then Servers) that follow the projects-page grid pattern. Each section uses `x-section-heading` linking to its full index. Communicate server health with the shared status badge. ### Top-level dashboard destinations Every page opened directly from the main sidebar uses the same compact content shell: - 24px page title and a 13px muted summary; - the primary action at the top right using the restrained brand fill; - no legacy `coolbox`, `.navbar-main`, or oversized subtitle block; - four-column compact cards for small browsable collections; - a dense table instead of cards when the collection is expected to grow; - `x-empty` anatomy for empty states; - `x-status-badge` for state and `x-reicon` for all interface icons. Collection cards are `min-h-28` or `min-h-32`, use a 32px icon tile, and keep secondary metadata at 11px. They must not grow into dashboard-sized summary cards. Sources, destinations, S3 storage, private keys, and shared-variable scopes use this pattern. Top-level settings families such as Team, Notifications, Keys & Tokens, and instance Settings use their `*settings-layout` component: the same grouped, icon-led settings sidebar as resources, plus a `settings-mobile-header` title below `xl`. Do not nest `