# 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 `docker exec coolify php artisan view:cache`, then clear > it with `docker exec coolify php artisan view:clear`. > - Build frontend assets in the Vite container with > `docker exec coolify-vite 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 the left accent rail plus a flat selected surface; - 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 mix the accent equally with black for a pronounced bottom edge, so custom theme colors produce a matching edge instead of a generic one. Dark mode matches the depth edge of neutral buttons to their regular border color. Highlighted buttons keep their dark, color-matched accent edge. --- ## 2. Development and cascade notes PHP runs in the `coolify` container. The development app is normally available at `http://localhost:8000`, with Vite on port `5173`. `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 both fixed navigation layers; - 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 | 10% neutral | page canvas (kept below card fills so cards lift) | | `--coollabs-elevated` | 98% neutral | 15% neutral | shells and card headers | | `--coollabs-base` | white | 17% neutral | nested card bodies | | `--coollabs-recessed` | 96% neutral | 20% neutral | inputs and listboxes | | `--coollabs-fill` | 92.2% neutral | 26.9% 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` `#0a0a0b`), `bg-gray-50` in light. The `
` content area and page body use it. - **Sidebar and topbar chrome** use `bg-panel` in dark (`--color-panel` `#141418`, 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 hex, not oklch. oklch lightness compresses toward pure black below ~15% (oklch(10%) renders as sRGB 3, oklch(15%) as sRGB 11), so oklch values there give no visible step between layers. 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 an accent rail on the left plus a solid neutral selected fill (`bg-black/5` light, `bg-white/6` dark). Hover rows use the same radius. Do not use accent-tinted gradients on nav rows; yellow washes look muddy on dark UI. - Nested items use a thin guide line with a visible active segment, not a thick box border. - The update badge sits on the version row and uses a tiny fully rounded primary-action pill. ### Layer-2 navigation Application and server pages use the same fixed second navigation layer directly below the global topbar. Do not keep a large in-flow resource heading or legacy `.navbar-main` tabs on one resource type while using the compact layer-2 bar on another. Active tabs are a light brand fill: - purple tint in light mode; - yellow tint in dark mode; - no fully saturated tab background. 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 and its compact status badges. Layer 2 owns route tabs, resource links, and contextual action buttons only. If a resource is missing from `x-top-breadcrumb`, extend the global topbar instead of repeating its name or status summary in layer 2. Mobile resource navigation may repeat this context because the desktop global topbar is hidden there. Desktop resource lifecycle actions dock in `#resource-action-hud-slot` and use ``. Show primary actions (Deploy, Redeploy, Restart, Stop) as sibling header buttons. Collapse that group into an Actions dropdown only when the remaining top-bar width cannot fit them (breadcrumb keeps a 200px floor). Infrequent operations live in a separate Advanced dropdown with the grid icon: force restart / force deploy / force cleanup on services, and Traefik dashboard / refresh proxy status on servers. Place Advanced immediately after Links, or first in the action cluster when there is no Links control. Application Deploy is a dropdown with Deploy and Deploy (without cache). A running service Restart control is a dropdown with Restart current version and Pull latest and restart. Mobile headings keep a full-width Actions dropdown because the desktop HUD is hidden below `xl`. Do not hide primary actions behind a menu on a wide desktop. Links stay a separate dropdown because the URL list is unbounded. Only add layer-2 tabs when they represent real sibling routes inside one context. Never repeat main-sidebar destinations such as Dashboard, Projects, Terminal, Servers, Sources, Destinations, or Storage as a second tab row. A single collection page does not need a tab just to fill the bar; keep its primary action in the page header instead. When tabs are useful, their left edge uses the same compact `pl-2` alignment as application navigation rather than the content container's wide horizontal padding. A layer-2 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. Use two full-width sections that follow the projects-page grid pattern: projects first, then servers. Keep one `New` action in the page header and let its modal choose the resource type. Place active deployments above the resource grids as a compact, live-updating table rather than a metric card. 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 a compact header followed by a small route-derived tab strip. The active tab uses the same purple-light/yellow-dark tint as resource tabs. Do not nest `