Help Page House Style - edmozley/freeitsm GitHub Wiki
Every module in FreeITSM has an in-app guide โ 26 module pages, from tickets/help.php down to workflow/help-ssl.php, plus the 19 admin-area guides and the landing page under system/help/. They all look the same because they all use one stylesheet: assets/css/help.css.
This page is the rule they follow. Read it before writing a new guide, and before adding anything to an existing one.
โ ๏ธ The System help pages were missed by the original sweep, and only came onto the house style in 1.8.0 โ five weeks later. They were built on 26 June 2026, six weeks beforehelp.cssexisted, so the August sweep of "every guide" never saw them, and they quietly carried the 27th private copy of the layout. Everything the next section describes as a solved problem was still live in them: a 118-line block under asyshelp-prefix, hardcoded indigo instead of theme tokens, a decorative.highlightband withmargin: 0 -48pxbleeding 48px wider than its neighbours โ the identical CMDB bug โ a contents raildisplay:noned below 900px, and no indent model at all, so headings and body copy sat at the same level on every page.๐ The lesson is about the sweep, not the pages. "Every help page" was resolved in practice as
*/help.php, which structurally cannot matchsystem/help/<topic>.php. A pattern that answers a slightly different question than the one you asked reports complete, and looks complete. When you sweep a category, enumerate the category and count it โ do not assume one glob is the same shape as the idea.
Until August 2026 there was no house style. There were 26 copies of one.
Each guide carried its own <style> block โ the same ~280 lines of layout under its own prefix: cal-help-*, ct-help-*, kb-help-*, tk-help-*, wt-help-*, and a dozen more. Every copy was individually reasonable. The problem was that changing one never changed the others, so they drifted:
- The CMDB guide tinted sections 2, 4 and 8 with a grey band, for no rule anyone could state โ and those sections bled 48px wider than their neighbours, so "level 1" appeared to be a different width depending where you were on the page.
-
.cmdb-help-section > phad no indent at all, so three paragraphs sat at level 1 while the steps and cards beside them were indented. Three others had been patched by hand with an inlinestyle="margin-left:46px". - Twelve pages hand-coloured their feature icons blue / green / orange / purple down a single grid.
- Seven different class names โ
tip,callout,warn,gotcha,danger,consequence,exampleโ did the job of one. - Most pages simply
display: noned their contents list below 900px.
7,657 lines of duplicated styling became 600. The rest of this page is what replaced it.
This is the rule. Everything else is detail.
level 1 .help-section full width of the pane, NEVER indented
.help-section-header (number) + <h3> + lead <p>
level 2 everything else in it indented to --help-indent
level 3 .help-subsection its <h4> sits at level 2,
its content one step further in
A guide reads as a numbered list of sections. Each section spans the full width; everything belonging to that section lines up underneath its heading. That holds at every screen size โ on a phone the indent shrinks from 46px to 32px along with the badge that sets it, rather than collapsing to the left edge.
The old pages put margin-left: 46px on each individual component, which is exactly why the occasional paragraph fell out of line โ somebody added a <p> and there was nothing to remind them. The house style does it the other way round:
.help-section {
padding-left: var(--help-indent); /* everything inside starts here */
}
.help-section-header {
margin-left: calc(var(--help-indent) * -1); /* except the heading, which backs out */
}Anything you drop into a section is aligned correctly by default, and there is no way to forget. Never add margin-left to a help page to line something up by hand โ if it isn't aligned, the markup is in the wrong place.
Every section looks identical: number badge, heading, indented body, a hairline rule to the next one. There is no "highlighted section" variant, deliberately. A variant that means nothing in particular gets applied inconsistently, which is how this whole mess started. If a section needs to stand out, that is a job for its words.
body {
--accent: var(--cmdb-accent);
--accent-hover: var(--cmdb-accent-hover);
--accent-soft: var(--cmdb-accent-soft);
--on-accent: var(--cmdb-on-accent);
}Four lines. assets/css/help.css draws everything โ the hero gradient, the badges, the note callouts, the card icons, the flow steps โ from those four variables. Every module already defines its own set in assets/css/theme.css, in both light and dark palettes, so nothing else needs writing. See Theming & Dark Mode for where those tokens come from.
Tickets is the one exception: it has no accent of its own and uses the application's brand colour, which assets/css/help.css already reads from the global --accent. Its style block says exactly that and nothing else.
The sweep had to decide, a dozen times, whether a splash of colour was information or decoration. The rule that settled it:
Colour that carries the meaning stays. Colour that only repeats the label sitting next to it goes.
| Kept โ the colour is the information | Removed โ the label already said it |
|---|---|
| Service-status dots (operational / degraded / maintenance / outage) | Feature-card icons in four unrelated colours down one grid |
| The morning-checks red/amber/green trend bars and its three RAG cards | Flow-chain steps in a per-module rainbow |
| Change types (standard / normal / emergency) and the change lifecycle badges | Knowledge's pending / approved / rejected card borders |
| The change risk scale, green through to deep red | Contracts' four coloured dots beside settings headings |
| Watchtower's status dots; the LMS progress pills; the mailbox-auth badges |
Everything in the "kept" column was moved onto the theme's semantic tokens (--success-*, --warning-*, --danger-*) on the way through, because the originals were hardcoded pastels that only worked in light mode.
Don't build the hero banner from accent โ accent-hover. On dark palettes the module accents are deliberately lifted so they read on dark surfaces, so a full-bleed banner made from them glares. assets/css/help.css handles this already:
.help-hero { background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%); }
[data-theme-mode="dark"] .help-hero {
background: linear-gradient(135deg, var(--accent-soft) 0%, var(--surface-2) 100%);
color: var(--text);
}Colour key: ๐งฑ page skeleton ยท ๐ body copy ยท ๐๏ธ grouped content ยท ๐ท๏ธ inline ยท ๐ป code & tables
| ๐จ | Class | What it is |
|---|---|---|
| ๐งฑ | .help-container |
the flex shell; also where the --help-indent / --help-gutter variables live |
| ๐งฑ |
.help-sidebar + h3
|
the fixed contents rail; h3 is a group heading |
| ๐งฑ |
.help-nav-link + .help-nav-num
|
one entry; add .active for the current section |
| ๐งฑ | .help-main |
the scrolling column (the scroll-spy listens on this, not window) |
| ๐งฑ | .help-hero |
the banner; takes <h1> or <h2>
|
| ๐งฑ | .help-content |
the padded column the sections sit in |
| ๐งฑ |
.help-section + -header + -num
|
level 1 โ see the heading model above |
| ๐ | .help-subsection |
level 3 โ a titled block within a section |
| ๐ | plain <p>, <ul>, <ol>, <h4>, <h5>
|
styled as direct children of .help-section; no class needed |
| ๐ | .help-lede |
the one sentence that sets up a whole guide |
| ๐ | .help-note |
a callout. Modifiers: .warn, .bad, .ok. Plain = the module accent |
| ๐ | .help-back |
"โ Back to โฆ" on a deep-dive page hanging off another guide |
| ๐ | .help-muted |
an aside within a paragraph |
| ๐๏ธ |
.help-cards + .help-card
|
the card grid. Modifiers .cols-1 / .cols-3 / .cols-4; 2 is the default |
| ๐๏ธ | .help-card-icon |
the tinted icon tile. .plain for an emoji (no tile) |
| ๐๏ธ | .help-card.row |
the one sanctioned card variant: icon left rather than above |
| ๐๏ธ | .help-card-eg |
the italic "e.g. โฆ" line at the foot of a card |
| ๐๏ธ |
.help-steps + .help-step + .help-step-num
|
a numbered procedure |
| ๐๏ธ |
.help-defs + .help-def + -term + -desc
|
term-and-definition rows |
| ๐๏ธ | .help-list |
a plain stack of tinted rows |
| ๐๏ธ |
.help-flow + .help-flow-step + .help-flow-arrow
|
A โ B โ C. Steps are neutral; .accent marks one |
| ๐๏ธ |
.help-diagram + -node + -line
|
a monospace tree drawing |
| ๐๏ธ | .help-timeline |
a worked example told in order |
| ๐ท๏ธ | .help-pill |
a status tag. Modifiers .ok .warn .bad .info; plain = neutral |
| ๐ท๏ธ | .help-dot |
a small coloured status dot |
| ๐ป |
.help-code, <pre>
|
a code block (same dark scheme in both themes โ a terminal should look like a terminal) |
| ๐ป |
<code>, .help-kbd
|
inline identifier; a keyboard key |
| ๐ป | .help-table |
a wrapper <div> around the <table>, so a wide table scrolls inside its section instead of stretching the page |
| ๐๏ธ |
.help-tabs + .help-tab-panel
|
3.5.0. One guide per product inside a section: buttons with data-tab, panels with the same data-tab and hidden on all but one. The page's own script switches them. Without JavaScript all panels show stacked, so nothing is lost |
| ๐๏ธ |
.help-checklist + .help-check + .help-check-title
|
3.5.0. A tick-off step: <label class="help-check"><input type="checkbox" data-check="id"> + .help-check-body holding .help-check-title and the description. .done strikes the title. Ticks are kept in localStorage by the page (never anything that matters) |
| ๐๏ธ |
.help-progress + .help-progress-bar > span
|
3.5.0. "3 of 5 done" and a bar, for a checklist |
| ๐ท๏ธ | .help-ext |
3.5.0. A link that opens in a new tab (target="_blank" rel="noopener"); adds โ. Use it for System โ X links in a step-by-step guide, so the guide stays open beside the screen being set up |
The first page to use the tabs and checklist is system/help/sso-groups.php, a step-by-step guide. Its sgCheck() / sgLink() helpers are the pattern to copy. .help-check-title is a block: a <strong> in a step's description stays inline (an early version styled every strong as a block and broke the sentences).
Note the pill and note modifiers are named for what they mean, not for a colour โ .help-note.bad, not .help-note.red. That is what lets them follow the theme.
<link rel="stylesheet" href="../assets/css/theme.css?v=22">
<link rel="stylesheet" href="../assets/css/inbox.css">
<link rel="stylesheet" href="../assets/css/help.css?v=1">
<style>
/* The only thing a help page should need to say for itself: its colour. */
body {
--accent: var(--mymod-accent);
--accent-hover: var(--mymod-accent-hover);
--accent-soft: var(--mymod-accent-soft);
--on-accent: var(--mymod-on-accent);
}
</style><div class="help-container">
<div class="help-sidebar">
<h3>Guide</h3>
<a href="#overview" class="help-nav-link active" data-section="overview">
<span class="help-nav-num">1</span> Overview
</a>
</div>
<div class="help-main" id="helpMain">
<div class="help-hero">
<h2>My module guide</h2>
<p>One sentence on what the module is for.</p>
</div>
<div class="help-content">
<div class="help-section" id="overview">
<div class="help-section-header">
<span class="help-section-num">1</span>
<div>
<h3>Overview</h3>
<p>The lead paragraph.</p>
</div>
</div>
<!-- everything from here is level 2 and indents itself -->
<p>Body copy.</p>
<div class="help-cards">โฆ</div>
<div class="help-note">A tip.</div>
</div>
</div>
</div>
</div>Copy the scroll-spy script from any existing guide โ it listens on #helpMain and filters on [data-section].
The guides under system/help/ use the same classes, but they are assembled differently: one registry drives everything, so a page cannot disagree with its own navigation.
<?php require __DIR__ . '/_init.php';
$helpSlug = 'carddav'; // the key in system/help/_registry.php
require __DIR__ . '/_top.php'; ?>
<div class="help-section" id="setup">
<div class="help-section-header"><?php echo helpSectionNum('setup'); ?>
<div><h3>Setting one up</h3><p>The standfirst.</p></div>
</div>
<p>Body copy.</p>
</div>
<?php require __DIR__ . '/_bottom.php'; ?>_registry.php owns the hero, the standfirst, the ordered section list and a terms synonym string. From that one entry you get the <h1>, the sidebar rail, the section badges, and the page's deep-linkable rows in the landing-page search. The card's icon, title and description are not repeated there โ they come from getSystemAreas(), so a card and its help page cannot drift apart, and an area with no registry entry simply gets no card instead of a link to a page that does not exist.
Two things to know:
- ๐ A section on the page but not in the registry gets no badge, no nav entry and no search row. That is the design, not a bug โ but it means adding a section means editing two files. Count them against each other before you ship.
โ ๏ธ termsis a SECOND search haystack, separate from the System landing card's*_keywordsinlang/en/system.php. A new feature needs its vocabulary in both, or it is half-findable. CardDAV shipped complete, tested and documented and still could not be found by searching for "address book", because only one of the two had been updated.
System help is English-only, consistent with the System module, so its content is written inline rather than through lang/ โ which is also why the class-names-in-translation-strings trap below does not apply to it.
If you find yourself adding layout CSS to a help page, it belongs in assets/css/help.css so every module gets it. What legitimately stays in a page is a diagram of that module and nothing else: the knowledge guide's mock AI conversation, morning checks' trend chart, watchtower's drawing of a real module card, contracts' tab strip, the change lifecycle, the RFP phase strip. Everything else has a 5-to-9-line style block.
Worth knowing before you rename anything.
lang/en/tickets.php contained <span class="tk-help-code">. lang/*/process-mapper.php had pm-help-kbd. lang/*/lms.php had lh-pill โ across all 21 locales. A grep of the .php pages reports "clean" while the rendered page is broken.
After any class rename in a help page, grep
lang/as well.
Two failures, both from the same cause:
-
self-service numbered two of its nine sections with an empty badge. The numbers were hardcoded in the headings, two sections had been inserted later, and JavaScript patched the numbers at runtime to paper over it. It now derives the number from the
$helpNavarray in PHP, so the heading and the sidebar cannot disagree โ and it no longer needs JS to show a number at all. -
workflow rendered "1." twice, once in the badge and once in the heading text, because the number was baked into the translation string. Stripped from the ten headings in
en,pt-BRandukโ and note that only 3 of 21 locales had ever carried them, which is its own argument for keeping numbers out of prose. -
System help went further and numbered its sections entirely in JavaScript:
_bottom.phpcounted.syshelp-sectionnodes and inserted a badge into each one. That numbers whatever is in the DOM rather than what the sidebar lists, so a section present on the page but absent from_registry.phpsilently shifted every number after it โ and no number appeared at all without JS. Fixed in 1.8.0 byhelpSectionNum()insystem/help/_init.php:
/** The badge for a section, resolved from the same array that builds the sidebar. */
function helpSectionNum(string $id): string
{
global $helpNav;
foreach (($helpNav ?? []) as $i => $s) {
if (($s['id'] ?? '') === $id) {
return '<span class="help-section-num">' . ($i + 1) . '</span>';
}
}
return ''; // unregistered id: no badge, rather than a wrong number
}<div class="help-section" id="passwords">
<div class="help-section-header"><?php echo helpSectionNum('passwords'); ?>
<div>
<h3>Passwords</h3>
<p>The section's standfirst.</p>
</div>
</div>๐ Returning nothing for an unregistered id is the point, not a shortcoming. A missing badge is visible and honest; a number that is confidently wrong is neither. It also gives you a check worth running โ count sections against badges against sidebar links, and prove the check can fail by injecting an unregistered section before you trust a pass.
Two pages had <table class="help-table">. It needs to be <div class="help-table"><table>โฆ</table></div> or a wide table stretches the whole page instead of scrolling inside its section.
Two throwaway scripts earned their keep and are worth rebuilding if you do this again:
-
An orphan-class check โ extract every
class="โฆ"from the markup, and confirm each name has a rule in some stylesheet the page actually links (parse the<link>tags rather than assuming). This is what catches a rename that dropped a rule on the floor: a modifier like.tag.greenwhose CSS went with the deleted style block, so the badge silently renders plain. -
A fetch-and-read check โ pull every help page over HTTP with a forged session and read the body, not the status code. A PHP fatal is served as HTTP 200, so a status-only check reports a broken page as a pass. While you have the HTML, grep it for unresolved
*.help.*keys โ a missing translation falls back to printing the key.
Then a console-error pass in headless Chrome across every page โ with a negative control, so you know the check can actually fail. Injecting a call to an undefined function into one copy and confirming the check catches it takes ten seconds and is the difference between "no errors" and "no detection".
Two things to know when doing this locally:
- The portal page needs an
ss_user_idsession, notanalyst_id. -
system/integrations/help.phpredirects without a?provider=parameter.
-
Theming & Dark Mode โ where the
--accent/--surface/ semantic tokens come from - Mobile: Techniques & Tricks โ the wider rules for narrow screens
-
Internationalisation (i18n) โ why help text lives in
lang/, and the parity rules - Architecture โ where help pages sit in the file layout