Skip to content
fossil-skin Skin Architecture
Login

Skin Architecture

How the dark theme works and how to modify it.


Files

File Purpose
skin/css.txt All CSS — design tokens, layout, timeline fixes, dark-mode overrides
skin/header.txt TH1 template — HTML injected before page content on every Fossil page
skin/footer.txt TH1 template — HTML injected after page content
skin/apply.py Writes skin parts to every repo's SQLite config table
www/index.html Custom landing page served by Caddy at /
www/a11y.js Accessibility + dark-mode JS served by Caddy at /a11y.js

Design Tokens

The skin is a dark rendering of Fossil's default skin: same layout (title bar, menu band, horizontal submenu row), same sizes (16px system sans-serif body, 2.2em project title), with colors picked for WCAG AA contrast on a near-black page. Defined in :root in css.txt; www/index.html repeats them.

Token Value Usage
--bg #1c1e22 Page background
--surface #272a30 Menu band, cards, code blocks
--surface2 #31353d Hover states, selected rows
--border #41454e All borders
--text #e8e9eb Body text (13:1 on --bg)
--muted #b3b8c0 Secondary text (7.3:1 on --bg)
--link #7fb6ef Links (7.2:1 on --bg); the default skin's #3779BF, lightened
--title #8cbdf0 Project title
--active #f08a3c Active menu underline; the default skin's #D26911, lightened
--green / --red / --yellow #6fd48f / #f08a7e / #f2c94c Diff and status colors
--font system-ui stack Body text (no web fonts)
--mono ui-monospace stack Code

Deploying Skin Changes

After editing any skin file, push to repos:

python3 /var/fossil/skin/apply.py

That's it. Fossil serves the updated skin immediately — no restart needed.


Key Dark-Mode Overrides

Timeline graph lines

Fossil's built-in CSS hardcodes all graph elements black (.tl-node, .tl-line, .tl-arrow). On a dark background these are completely invisible. Every graph selector is overridden with !important using --muted for lines/arrows and --accent for nodes.

Branch row colors

Fossil JS sets style.backgroundColor on timeline cells with light pastel branch colors designed for white backgrounds. Two layers:

  1. Inline styles (JS-injected) — caught with td[style*="background"]. We overlay a semi-transparent dark layer using background-image: linear-gradient(rgba(15,17,23,0.75), ...). CSS renders background-image on top of background-color, so the pastel bleeds through as a subtle hue tint while keeping text contrast above 6:1.

  2. Stylesheet rule — Fossil's built-in CSS also applies background-color: #efefef to .timelineModernCell[id]. Overridden with background-color: transparent.

Chat message bubbles

Same issue as branch colors: Fossil JS sets inline background-color from the user's assigned color (m.uclr). Same background-image overlay technique with 72% dark opacity.

isDark flag

Fossil's C code hardcodes isDark: false for all custom skins, ignoring the skin-detail config key. This affects the timeline graph renderer (it picks white vs. black node colors). Fixed in a11y.js via window.fossil.config.skin.isDark = true — runs after Fossil's own inline scripts because of defer, and again on window.load because Fossil's load handlers may reset it.


TH1 Template Notes

The header.txt and footer.txt files use Fossil's TH1 scripting language (a Tcl subset).

Available variables in header/footer:

The capexpr function checks user capabilities — used in the mainmenu loop to hide links the current user can't access.


CSP and JavaScript

Fossil's Content Security Policy:

script-src 'self' 'nonce-<per-request-nonce>'

Inline scripts without the matching nonce are blocked. a11y.js is served as a static asset by Caddy at /a11y.js, which satisfies 'self'. It's loaded in header.txt with:

<script defer src="/a11y.js"></script>

The defer attribute ensures:

  1. HTML is fully parsed before the script runs
  2. Fossil's inline config scripts (which set window.fossil) have already executed
  3. The script runs before DOMContentLoaded

window.load re-runs the timeline and isDark fixes because Fossil's own load handlers insert SVG graph elements and may reset config values.