# Theme API v1 authoring reference Haven themes are ordinary CSS files served by the Haven server. Theme API v1 defines the CSS variables and semantic layout regions that theme authors can rely on without depending on Haven's internal class names or DOM structure. Theme API v1 is intentionally a CSS-only contract. It does not expose JavaScript helpers, events, component internals, or permission APIs. Structural changes that require moving elements or creating interactive controls belong in a plugin, not a theme. ## Theme file format A theme filename must end in `.theme.css` and should begin with a metadata comment: ```css /** * @name My Theme * @description A short description. * @author YourName * @version 1.0 * @icon M * @haven-theme-api 1 */ ``` The theme list reads `@name`, `@description`, `@author`, `@version`, `@icon`, `@section`, and `@haven-theme-api`. `@section Seasonal` puts the theme in the picker's Seasonal section; any other `@section` name gets a section of its own, and a theme without one is listed under Community. The picker's preview card is drawn from the file's `--bg-primary`, `--bg-secondary`, `--accent` and `--text-primary` values when they are plain colours. Haven checks the API declaration before it injects the stylesheet. Older Haven versions ignore metadata they do not recognise. Declare each field on its own metadata line. Multiple `@haven-theme-api` declarations make the theme invalid. Use a short text character or emoji for `@icon`. Do not put HTML in metadata. ## Compatibility Haven classifies each installed theme before loading it: | Status | Meaning | Loaded | | --- | --- | --- | | Compatible | Declares `@haven-theme-api 1` | Yes | | Legacy | Does not declare a Theme API | Yes | | Unsupported | Declares another positive integer version | No | | Invalid | Declares an empty, fractional, zero, negative, or non-numeric version | No | Legacy themes remain enabled for backward compatibility. Declaring an API is recommended because it lets Haven stop a known-incompatible theme before the theme can hide or break recovery controls. Incompatible themes remain visible in Plugins & Themes and in the admin theme list for diagnosis, but cannot be enabled, newly published, or selected as a new server default. If a selected theme disappears or becomes incompatible, Haven falls back to its built-in theme and updates the user's saved preference. Haven caches the last server compatibility verdict so a previously verified file theme can still load before first paint. A theme with no cached verdict waits for `/api/themes` on its first load. The server verdict is refreshed when the theme list loads, and the server validates the file again whenever it serves the stylesheet. The cache is only a flash-prevention optimisation. ## Installing and publishing 1. Put the file in the server's `themes/` directory. 2. Restart Haven or refresh the Plugins & Themes list. 3. Open the theme picker (the palette button in the sidebar). As an admin you see it under Available to publish. 4. Switch Publish on under its card to add it to every user's theme picker. Settings, Admin, Branding, Custom Themes has the same switches. A published file is a selectable base theme. Haven applies the built-in Haven theme as the stable layout base, then loads the selected file after the core stylesheet. A file that is installed but not published is only for people who manage the server: they can try it, or enable it per browser as an additive CSS tweak from Plugins & Themes. Members do not see it there or in the picker, and the server will not save it as a member's theme. A member who already uses a theme when it is unpublished keeps it until they pick another. Themes are server-managed files. Haven does not download themes from arbitrary URLs or provide a per-user raw CSS editor. `themes/compact.theme.css` is a shipped Theme API v1 example that uses only public tokens, page markers, and layout regions. It deliberately leaves message geometry to the user's Layout Density setting; structural compaction belongs in a plugin. ## Previewing a theme without a server `public/theme-preview.html` is a static copy of the app with sample content: a conversation with every kind of markdown, a picture, a poll, reactions and a reply, the member list, an open profile card, the Settings modal, and a forum channel. It loads Haven's real stylesheet, so a theme file applied to it looks the way it will in the app. 1. Open `public/theme-preview.html` from a checkout in a browser, or in a live editor such as Phoenix Code that previews as you type. 2. In the toolbar at the top right, type the path to your theme relative to the file (`../themes/my.theme.css` for a file in the repository's `themes/` folder) and press Apply, or open the page as `theme-preview.html?theme=../themes/my.theme.css`. 3. Chat and Forum switch the view; Settings modal and Profile card show or hide those. The page is a snapshot, so a change to the app's markup reaches it the next time it is regenerated with `scripts/theme-preview-capture.js`; the file's header comment and the script say how. A running server also serves it at `/theme-preview.html`. ## Scoping a theme Both the login page and the main application expose the API version: ```css html[data-haven-theme-api="1"] { /* Theme API v1 rules */ } ``` Use the page marker when a rule belongs to only one page: ```css [data-haven-page="app"] { /* Main application only */ } [data-haven-page="auth"] { /* Login and registration only */ } ``` ## Public design tokens Override public tokens on `:root`. All tokens are optional; omitted values come from the Haven base theme. ### Backgrounds | Token | Purpose | | --- | --- | | `--bg-primary` | Main content background | | `--bg-secondary` | Sidebars and primary panels | | `--bg-tertiary` | Nested panels and controls | | `--bg-hover` | Hovered controls and rows | | `--bg-active` | Selected controls and rows | | `--bg-input` | Text inputs and editors | | `--bg-card` | Cards, messages, and modal surfaces | ### Accent and text | Token | Purpose | | --- | --- | | `--accent` | Primary accent colour | | `--accent-hover` | Hovered accent controls | | `--accent-glow` | Accent shadow or glow colour | | `--accent-text` | Preferred foreground for core accent controls | | `--text-primary` | Primary text | | `--text-secondary` | Secondary text and timestamps | | `--text-muted` | Hints, placeholders, and quiet labels | | `--text-link` | Links | Always check the contrast between `--accent` and `--accent-text`. A light accent usually needs dark accent text. A few specialised media controls use a fixed foreground for compatibility with existing themes, so verify those too. ### Borders and semantic states | Token | Purpose | | --- | --- | | `--border` | Default border | | `--border-light` | Stronger or raised border | | `--success` | Positive and connected states | | `--success-text` | Foreground on success-filled controls | | `--danger` | Destructive and error states | | `--danger-text` | Foreground on danger-filled controls | | `--warning` | Warning states | | `--warning-text` | Foreground on warning-filled controls | | `--led-on` | Connected presence indicator | | `--led-off` | Disconnected presence indicator | | `--led-glow` | Connected indicator glow | Semantic colours can also appear as text or icons on Haven surfaces. Check each state colour against your backgrounds as well as against its paired foreground. ### Typography and geometry | Token | Purpose | | --- | --- | | `--font-main` | Main interface font stack | | `--font-mono` | Codes and technical values | | `--font-heading` | Headings and display labels | | `--radius` | Default radius | | `--radius-sm` | Compact control radius | | `--transition` | Standard transition timing | | `--sidebar-width` | Navigation sidebar width | | `--right-width` | Context sidebar width | Use `rem` for dimensions. Haven's interface zoom changes the root font size, so pixel-based shell dimensions do not scale with the rest of the interface. ### Decorative tokens | Token | Purpose | | --- | --- | | `--msg-glow` | Optional message hover glow | | `--scanline` | Optional scanline overlay value | ### Internal values Message geometry properties such as `--msg-pad-x`, `--msg-pad-y`, `--msg-avatar`, `--msg-gap`, and `--msg-gutter` are not part of Theme API v1. Haven changes them for density preferences and responsive breakpoints, and file themes load after those core rules. Treating them as unconditional theme tokens would override the user's density and mobile geometry. They may be formalised in a later API after the related state has a stable pre-paint contract. ## Public layout regions Select major areas through `data-haven-region`: ```css [data-haven-region="navigation-sidebar"] { background: var(--bg-secondary); } ``` ### Application regions | Region | Purpose | | --- | --- | | `app-shell` | Primary application shell, including the status bar | | `workspace` | Main multi-column workspace | | `server-rail` | Server navigation rail | | `server-list` | Dynamic server list inside the server rail | | `navigation-sidebar` | Channels and direct-message sidebar | | `account` | Current account identity and account actions | | `sidebar-content` | Reorderable sidebar content | | `join-channel` | Join-channel section | | `create-channel` | Create-channel section | | `channels` | Channel list section | | `direct-messages` | Direct-message list section | | `sidebar-footer` | Pinned sidebar footer and controls | | `sidebar-actions` | Persistent sidebar action buttons | | `theme-picker` | Theme selector | | `main` | Main content column | | `channel-header` | Active channel header | | `welcome` | No-channel welcome state | | `message-area` | Active chat area | | `webcams` | Webcam viewer | | `screen-shares` | Screen-share viewer | | `music-player` | Listen Together player | | `pinned-messages` | Docked pinned-message panel | | `message-list` | Scrollable message list | | `composer` | Message composer | | `soundboard` | Docked soundboard panel | | `context-sidebar` | Voice and member context sidebar | | `search-results` | Docked search-results panel | | `voice-roster` | Current voice participant list | | `member-list` | Current channel member list | | `voice-settings` | Voice device and quality settings | | `voice-controls` | Active call controls | | `status-bar` | Debug and connection status bar | | `thread-panel` | Thread conversation panel | | `settings` | Settings surface | ### Authentication regions | Region | Purpose | | --- | --- | | `auth-shell` | Authentication page layout | | `auth-card` | Login and registration card | | `auth-header` | Authentication branding header | | `theme-picker` | Authentication theme selector | `theme-picker` exists on both pages, but only one page is loaded in a document. ## Public layout state Haven exposes three state attributes for themes and layout plugins: | Hook | Values | Purpose | | --- | --- | --- | | `data-haven-density` on `html` | `compact`, `cozy`, `spacious` | The user's message-density choice | | `data-haven-layout-editing` on `html` | `1` while active, otherwise absent | Mod Mode is editing the native layout | | `data-haven-layout-owner` on `html` | Plugin-defined owner ID, otherwise absent | The structural layout plugin currently moving regions | The density attribute is applied before first paint. Message geometry properties remain internal; use the state only to adapt surrounding layout without overriding the user's density choice. Interactive plugins can also listen for synchronous state changes: ```js document.addEventListener('haven:density-change', (event) => { console.log(event.detail.density); }); document.addEventListener('haven:layout-editing', (event) => { if (event.detail.active) restoreNativeLayout(); else reapplyLayout(); }); document.addEventListener('haven:layout-owner-change', (event) => { console.log(event.detail.owner); }); ``` `haven:layout-editing` fires before Mod Mode starts moving panels and after it has restored the saved native arrangement. Structural plugins should suspend on the first event and reapply only after the second. `event.detail.owner` identifies the owner that was active before editing. Haven reserves ownership for that plugin until the second event completes, so other layout plugins cannot take its place based on listener order. Structural layout plugins must also acquire the shared owner before moving regions and release it after restoring them: ```js if (HavenApi.Layout.acquire('MyLayout')) { movePublicRegions(); } // During stop(), below the desktop breakpoint, or before Mod Mode edits: restorePublicRegions(); HavenApi.Layout.release('MyLayout'); ``` `acquire()` returns `false` while another structural plugin owns the layout. Listen for `haven:layout-owner-change` to retry after that owner releases it. This prevents one plugin from recording another plugin's temporary DOM as the native restore position. `HavenApi.Layout.owner` exposes the current owner. ### Appearing in the Layout picker The **Sidebar Layout** picker in Settings, Layout lists **Original** (Haven's own layout) and every layout plugin on the server, one at a time. A plugin shows up there when: - its file is named `Layout.plugin.js` (the picker shows its `@name`, without a trailing "Layout"); - its class has `_engage()` and `_disengage()`, which move and restore the regions; - it acquires the layout under its file's base name, for example `HavenApi.Layout.acquire('MyLayout')` for `MyLayout.plugin.js`; - it keeps its on/off as `HavenApi.Data.save('MyLayout', 'layoutOn', '1' | '0')` and reads it back on `start()`, as Braid and Compact do. Picking another layout disengages the current one and saves `layoutOn = '0'` for it, so a plugin never has to know about the others. ## Stability policy For Theme API v1, Haven intends to keep these stable: - `data-haven-theme-api="1"` - `data-haven-page` values - documented `data-haven-region` values - documented `data-haven-density`, `data-haven-layout-editing`, and `data-haven-layout-owner` states - documented `haven:density-change`, `haven:layout-editing`, and `haven:layout-owner-change` events - `HavenApi.Layout.acquire()`, `release()`, and `owner` - documented public design tokens The following are not part of Theme API v1: - element IDs and class names - exact DOM nesting or sibling order - inline styles used to represent runtime state - dynamically generated message, channel, member, and modal internals - undocumented custom properties - undocumented plugin APIs or JavaScript objects A region keeps its semantic responsibility, but Haven may change its element type, class, ID, children, or location. Write selectors against the region and avoid depending on its internal descendants when possible. ## Responsive layout Haven's core breakpoints are: - above `900px`: full desktop shell - `769px` through `900px`: tablet layout with an overlay context sidebar - `768px` and below: mobile overlay navigation and context sidebars - `480px` and below: phone sizing Restrict deep desktop layout changes to the full desktop shell unless the theme also implements and tests the tablet and mobile states: ```css @media (min-width: 901px) { [data-haven-region="navigation-sidebar"] { width: 15rem; } [data-haven-region="context-sidebar"] { width: 15rem; } } ``` Do not remove the mobile overlay controls solely because they are hidden on a desktop screenshot. ## Assets and backgrounds Assets placed beside a theme are served from `/themes/`. Use a relative path: ```css [data-haven-region="main"] { background-image: linear-gradient(rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.55)), url("wallpaper.jpg"); background-position: center; background-size: cover; } ``` Keep assets small and provide enough contrast for text. Remote resources expose members' IP addresses to their hosts and may be blocked by Haven's security policy. Prefer files installed with the theme. ## Motion and accessibility Respect reduced-motion preferences: ```css @media (prefers-reduced-motion: reduce) { * { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; } } ``` Keep visible focus states, readable contrast, keyboard access, and touch target sizes. Hiding a duplicate-looking control is unsafe unless every function it provides remains reachable elsewhere. ## Theme or plugin? Use a theme for: - colour, typography, spacing, radii, and backgrounds - sizing major regions - styling or hiding optional decoration - reordering direct flex or grid children when behavior remains intact Use a plugin for: - moving controls between different DOM parents - creating menus, buttons, or interactive state - preserving access to actions removed from persistent chrome - reacting to voice, channel, media, or permission changes - restoring changed DOM when the customization is disabled CSS can visually position an element outside its parent, but that does not move its semantics, clipping context, keyboard order, or event assumptions. Prefer a reversible plugin when a layout requires a real structural move. `plugins/CompactLayout.plugin.js` is the minimal bundled example. It moves only complete public regions, records their original parents and sibling positions, restores the native layout below the desktop breakpoint and during Mod Mode, and leaves message density under the user's control. Its `Ctrl+Alt+C` shortcut and sidebar button provide an immediate way back to the classic layout. ## Security Themes are trusted server-managed CSS. CSS cannot use Haven's JavaScript API, but it can hide or imitate interface elements and can request external assets. Only install themes from sources you trust. Plugins have a different security model: they execute JavaScript in the Haven page and are fully trusted code. Theme API v1 does not make plugins safe or sandbox them. ## Extension Safe Mode If a theme hides controls or a plugin breaks startup, open Haven with: ```text /app.html?haven-safe-mode=1 ``` The login page also accepts `?haven-safe-mode=1`. Safe mode is stored only for the current browser tab, so it survives the login-to-app navigation without changing saved preferences. While safe mode is active: - Haven uses its built-in base theme. - File themes and additive CSS tweaks are not injected. - Plugin files are not fetched, evaluated, or started. - Installed extensions are still listed from server-provided metadata. - Enabled extensions can be identified and disabled. Open Settings, Plugins & Themes to either reset all theme/plugin choices or exit safe mode without changing them. Resetting keeps unrelated account and display preferences, updates the server-synchronised theme preference to Haven, and then reloads without the safe-mode query parameter. If the server cannot confirm the reset, extensions remain suppressed and the recovery notice offers a retry after the connection returns. ## Minimal complete example ```css /** * @name Calm Slate * @description A small Theme API v1 example. * @author Example * @version 1.0 * @icon S * @haven-theme-api 1 */ :root { --bg-primary: #202225; --bg-secondary: #17191c; --bg-tertiary: #292c30; --bg-hover: #33373d; --bg-active: #3c4249; --bg-input: #111315; --bg-card: #24272b; --accent: #7aa2f7; --accent-hover: #8fb1fa; --accent-glow: rgba(122, 162, 247, 0.25); --accent-text: #101216; --text-primary: #f0f1f3; --text-secondary: #b3b7bd; --text-muted: #7f858e; --text-link: #8ab4f8; --border: #34383e; --border-light: #454a52; } @media (min-width: 901px) { [data-haven-region="navigation-sidebar"] { width: 15rem; } } ``` For a commented template containing every public token, copy `themes/custom.css.example`.