Repository navigation
UI Style Guide
Status: first draft, from discussion #185. The action row is live in Tickets, Assets and Software, so it can be tried before every other module follows. Comments on the discussion are welcome. Once it has settled, the rest of the modules move over one family at a time (section 7).
The rules every analyst-facing screen in FreeITSM follows, so a screen behaves the same whichever module you are in. They sit alongside the Help page house style, Theming and dark mode and the mobile techniques.
The principle behind all of them: predictable beats clever. Somebody moving between twenty-odd modules should find the same thing in the same place, called the same word.
- One word, unless a second word is strictly necessary. Save, Delete, Cancel, New, Export, Scan - not Save changes, Add application or Export CSV.
- Sentence case when there is more than one word: Delete forever, never Delete Forever.
- No "+" and no icon on a text button. New, not + New. Icon-only buttons (search, filter, refresh in a list header) are fine, and they carry a tooltip.
- The tooltip says what the one word does to what - New ticket, New asset. That is what a screen reader reads, so the short label loses nothing.
When two words are allowed:
| Case | Example | Why |
|---|---|---|
| Two buttons with the same verb side by side | New channel / New message | One word would make them identical |
| A second, stronger version of an action | Delete beside Delete forever | The difference is the point |
| A label that names a place or a thing that changes | Go to TICKET-123, Move to Junk | The second word is the information |
| Right-click menu items | Copy asset tag, Print label | A menu is read, not scanned, and has room |
| An empty page's invitation | Create your first article | It is a sentence, not a button row |
Classes: the shared .btn with .btn-primary or .btn-secondary (assets/css/inbox.css), or .action-row-btn in the action row. .btn-primary on its own sets only colour - always pair it with .btn. A module defining its own button family is a bug waiting to be found, and several still do (section 7).
Where: a strip directly under the module header, the full width of the page. What this page is on the left; what you can do here on the right, with New at the far right.
┌──────────────────────────────────────────────────────────────────────┐
│ ⋮⋮ Assets [Assets][Table][Users][Dashboard]… [Default▾] 🔔 A │ module header
├──────────────────────────────────────────────────────────────────────┤
│ Assets - 633 found [New] │ action row
├──────────────┬───────────────────────────────────────────────────────┤
│ [search....] │ │
-
New is one word, in the module's own colour (
--accent), and its tooltip names the thing. -
Every view of the same records gets the same row - list, table, board, calendar - so New is never missing just because you switched views. Where the form lives on another page, New links there (Assets' table view opens
./?new=1). - Outside every pane and panel. On a phone it stays on screen, never inside a Filters or Tags sheet.
- A page with nothing to create has no New button, never a dead one. The row can still carry the title.
- Shortcuts may stay where they are useful. A + at the top of a board column, which creates a task already in that status, is a shortcut beside New, not instead of it.
- Things that are not "creating a record" keep their own words and places - Upload, Scan, Plan, Assign.
Building it: never by hand. Straight after the header include:
<?php include 'includes/header.php'; ?>
<?php require_once '../includes/action_row.php';
renderActionRow(t('tickets.nav.inbox'), [
actionRowNew('openNewTicketModal()', t('tickets.list.new_ticket_btn')),
]); ?>| Option | Use |
|---|---|
title_id |
The page rewrites the title - Assets' Assets - 633 found |
count_id |
A count after the title, which the page fills; the dash appears only once it is filled |
extra |
Trusted HTML before the buttons - Software's search box |
actionRowNew($js, $tooltip, true) |
New is a link, not a script call |
The row brings its own stylesheet (assets/css/action-row.css, through BASE_URL), so no page needs a new <link>. It also makes the page's .main-container (or .dt-page) take whatever height is left, so a page needs no calc(100vh - N) of its own.
-
Pages use the whole width of the screen. No
max-widthon a page's container, no centred column. ⚠️ max-width: noneis not enough inside a flex parent: an inheritedmargin: autostill shrinks and centres it. Usewidth: 100%; max-width: none; margin: 0.- Caps on single things are fine - a text input, a modal, a reading column inside a document viewer - because a form field stretched across 3000px helps nobody.
-
Check by measuring, never by reading the CSS: the container's
getBoundingClientRect().widthagainst the window.
-
Theme tokens only -
var(--surface),var(--text),var(--border),var(--accent)- never a hard-coded colour, so dark mode works. See Theming and dark mode. -
Each module's accent comes from
includes/module-colors.phpthrough--accent. The primary button takes it.
- Sentence case for every heading, tab, label and button: Software inventory, Recycle bin.
- Plain hyphens in text, and one name for one thing across modules.
-
Every string goes through
t(). A new shared word belongs inlang/en/common.php.
Everything phone-specific lives inside @media (max-width: 768px), so the desktop never changes because of a phone fix. See Mobile-friendly techniques and the module's own mobile page before changing anything. A shared component may carry its own phone rules in its own stylesheet, gated the same way (action-row.css does).
First: Tickets, Assets and Software (the three named in #185). Then, once the discussion is happy with them, one family at a time:
| Family | Modules | Where New is today |
|---|---|---|
| ✅ Done | Tickets, Assets (incl. Table), Software | The action row |
| Registers | CMDB, Domains (incl. Table), Contracts | Toolbar; toolbar; sidebar bottom (phone: footer bar) |
| Service work | Changes (incl. Calendar, Table), Problems, Tasks (Board, List, Calendar, Timeline, Table) | Sidebar below filters; sidebar top; per-column + only |
| Content | Knowledge, Checklists, Process mapper, Forms, LMS, Workflow, Network mapper | Sidebar below tags (phone: Tags sheet); sidebar top (phone: Filters sheet); sidebar; toolbars |
| Other | Calendar (incl. Table), Projects, Service status, War room | Sidebar top; toolbar; section titles; sidebar |
| No create | People, Morning checks, Reporting, Watchtower, System | Row for the title only, if at all |
Also found by the audit, to fix as each module moves:
- About 70 buttons with more than one word on main pages - Create Ticket, Export CSV, Paste list, Back to list, Full screen - and eight in Title Case.
-
About 15 different button classes for the one create action, and module-local
.btnfamilies in Assets, CMDB, Service status, Calendar and LMS. - "+" written three ways - in the translation, added in PHP, or as an icon - and none at all elsewhere.
- Reporting's landing page is the one main page still capped (1160px).
FreeITSM — an open-source IT Service Management platform · github.com/edmozley/freeitsm · MIT licence
- Installation
- ⏰ Scheduled tasks (cron jobs)
-
Architecture
- ↳ 🧹 Root folder tidy (URL routing)
- ↳ 🔗 Database Integrity (FKs & constraints)
- ↳ 🛠️ Database Verification — Developer Guide
- ↳ 🧩 Service Layer (UI + API share code)
- ↳ 🪤 Code traps (the TRAP: comments)
- ↳ 📝 Commenting for the Engineering wiki
- ↳ 🛠️ Engineering wiki scanner - Developer Guide
- ↳ 📊 Service-Layer progress
- 🧪 Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- 📅 Date & Time Formats
- 💱 Currencies
- 🧭 The sidebar (optional module rail)
- 🔀 Display order (modules, Watchtower cards) — Developer Guide
- Theming & Dark Mode
- 🗂️ Recent — getting back to what you were doing
- ⌨️ Command palette (⌘K)
- 🔍 Searching inside tickets
- 📄 Attached documents
-
Mobile‑Friendly
- ↳ 🎫 Mobile: Tickets
- ↳ 💻 Mobile: Assets
- ↳ 📅 Mobile: Calendar
- ↳ 📚 Mobile: Knowledge
- ↳ 🚦 Mobile: Service Status
- ↳ 🗼 Mobile: Watchtower
- ↳ 🧩 Mobile: Problem Management
- ↳ 🔁 Mobile: Change Management
- ↳ 💿 Mobile: Software
- ↳ ✅ Mobile: Tasks
- ↳ 📝 Mobile: Forms
- ↳ 📄 Mobile: Contracts
- ↳ 📄 Mobile: Domains
- ↳ 📄 Mobile: People
- ↳ 🚀 Mobile: Projects
- ↳ 🗂️ Mobile: Files
- ↳ 🎓 Mobile: LMS
- ↳ 🗺️ Mobile: CMDB
- ↳ 🗺️ Mobile: Network Mapper
- ↳ 🧭 Mobile: Process Mapper
- ↳ ⚙️ Mobile: Workflow
- ↳ 🖥️ Mobile: System
- ↳ 📊 Mobile: Reporting
- ↳ 📖 Mobile: System Wiki
- ↳ 🙋 Mobile: Self-Service Portal
- ↳ 🧰 Mobile: Techniques & Tricks
-
Security
- Layer 1 — which modules you can enter
- ↳ 🧩 Module Access Control
- ↳ 🛠️ Module Access — Developer Guide
- Layer 2 — what you can administer
- ↳ 🎭 Roles & Permissions
- ↳ 🛠️ Roles — Developer Guide
- ↳ 🔤 Why capabilities are constants
- Layer 3 — the System module
- ↳ 🔑 Admin Access Control
- Hardening
- ↳ 📄 Security review response 2026-08
- ↳ 🛡️ Security hardening 2026-08
- ↳ 🛠️ Security hardening 2026-08 — Developer Guide
- ↳ 🛡️ Round three — plain English
- ↳ 🛠️ Round three — Developer Guide
- ↳ 🛡️ CSRF protection (S4) — Developer Guide
- Single Sign-On (SSO)
- 🗂️ LDAP & Active Directory
- 📇 CardDAV contact sync
- Browser Extension
- API Reference
-
🔌 REST API — how it works
- ↳ 🎫 REST API: Tickets
- ↳ 💻 REST API: Assets
- ↳ 🔴 REST API: Problems
- ↳ 🟠 REST API: Changes
- ↳ 📚 REST API: Knowledge
- ↳ ✅ REST API: Tasks
- ↳ 🗄️ REST API: CMDB
- ↳ 📜 REST API: Contracts
- ↳ 🗓️ REST API: Calendar
- ↳ 💿 REST API: Software
- ↳ 🌐 REST API: Domains
- ↳ 🚦 REST API: Service Status
- ↳ ☀️ REST API: Morning Checks
- ↳ 📝 REST API: Forms
- ↳ ⚙️ REST API: Workflow
- ↳ 🏷️ REST API: Cost centres
- ↳ 🧑💼 REST API: Analysts
- ↳ 🗺️ REST API: Network Mapper
- ↳ 🧭 Using the API docs page
- ↳ 📐 OpenAPI specification
- ↳ ✅ OpenAPI: kept correct
- ↳ 🛠️ Maintaining the catalogue
- Watchtower
-
Tickets
- ↳ 📋 Rota copy and paste — Developer Deep Dive
- ↳ ✅ Checklists & SOPs
- ↳ ☑️ Mandatory fields
- ↳ 🧾 Custom ticket fields
- ↳ 🏷️ Ticket categories
- ↳ 👥 Assigning tickets to a team, and escalation
- ↳ 🏢 One board across every company
- ↳ Mailbox Authentication
- ↳ 📤 Email send log
- ↳ 📎 Attachment size limits and large emails
- ↳ Basic IMAP mailboxes
- ↳ Email rendering & images
- ↳ SLA Management
- ↳ WhatsApp channel
-
↳
✈️ Telegram channel - ↳ ⭐ CSAT company scope and filters — Developer Guide
- ↳ 👥 Microsoft Teams channel
- ↳ 🗨️ Mattermost channel
- ↳ 💬 Web chat channel
- ↳ 🟣 Slack channel
- ↳ 🔗 Linking tickets
- ↳ ⓘ Record previews
- ↳ 📝 Ticket notes: internal or shared
- ↳ 🗒️ Canned responses
- ↳ ✉️ Limiting replies to particular senders
- ↳ 📨 Telling the analyst a ticket is theirs
- ↳ ✍️ Email signatures
- ↳ 🌐 The public web address
- ↳ 🔢 Ticket numbering
- ↳ 🙋 Raising a ticket for someone else
- ↳ 🔀 Merging tickets
- ↳ 🔒 Confidential tickets
- ↳ 👥 Portal managers
- ↳ 👁 Who has seen a ticket
- ↳ 📜 Reading long tickets
- ↳ ⑂ Splitting tickets
- ↳ ✅ Selecting several tickets
- ↳ 🗂️ The folder pane
- ↳ 🔽 Just my tickets, or no closed ones
- ↳ 🛠️ Snoozing tickets — Developer Guide
- ↳ 👥 Collision detection
- ↳ ⏱️ Time tracking
- ↳ 📅 Scheduled work in your own calendar
- Problem Management
- Tasks
- 🚀 Projects
-
Assets
- ↳ 🏢 Moving an asset between companies
- ↳ 📍 Shared asset locations
- ↳ 🧑💼 Assigning assets to analysts
- ↳ 📆 Warranty and lease alerts
- ↳ 📄 Leases
- ↳ 🔭 Saved table views
- ↳ 🖨️ Recording anything, and importing it
- ↳ 🏷️ QR asset labels
- ↳ 📋 Who holds what, and handover documents
- ↳ 🖥️ The inventory agent (PowerShell)
- ↳ 🗄️ Proxmox VE servers
- ↳ ☁️ VMware Cloud Director servers
- ↳ 🔗 Linking equipment to tickets
- ↳ ☑️ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- ↳ 🎨 The form designer — Developer Guide
- ↳ 📐 Layout & the grid — Developer Guide
- ↳ 🗂️ Collections — grouping submissions
- ↳ 📄 Submissions as PDFs
- ↳ ⚡ What happens next — a form's own actions
- ↳ 🛠️ Sections & conditional logic — Developer Guide
- ↳ 🛠️ Lookup fields — Developer Guide
- ↳ 🛡️ Catalogue request approvals
- People
- Domains
- 🗂️ Files
- Contracts
- Service Status
- 🔔 Notifications
- 🚨 War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- ↳ 📊 Progress tracker
- ↳ Concepts & vocabulary
- ↳ Email routing & mailboxes
- ↳ Settings: global vs per-company
- ↳ Users & self-service
- ↳ Staff cross-company access
- ↳ 🏢 One board across every company
- ↳ Worked examples
- ↳ Pitfalls & gotchas
- ↳ Scope: what it's for
- ↳ 🛠️ Developer Guide (make a module multi-company)
- ↳ 🗄️ Case study: CMDB (a linked graph)
- ↳ 🧪 Test harness (prove it's isolated)
-
🐞 Bugs resolved
- ↳ 🔒 Repeating tasks ignored company scope
- ↳ 📬 #183 Mailbox checks overlapped
- ↳ 🌐 #188 Transfer lock shown off when unknown
- ↳ ✅ #184 Subtask names and Overdue
- ↳ 🧩 #186 Teams: the buttons did nothing
- ↳ 🔒 Text in a change could run script
- ↳ 🔒 AI settings changed without the AI permission
- ↳ 🚪 #178 Deleting an analyst failed
- ↳ 🎫 A team with no departments showed 0 tickets
- ↳ 🚀 Projects: adding a phase did nothing on Save
- ↳ 🔒 Leavers could still sign in
- ↳ 🖼️ Logo and courses broke on Apache with PHP-FPM
- ↳ 🔢 Chat tickets ignored your ticket numbering
- ↳ 📅 Dates shown as a dash, or in server time
- ↳ 🔒 Assets → Users showed people from other companies
- ↳ 🔒 Restricted analysts could read other modules' data
- ↳ 🖼️ Replies with a picture in the thread failed to send
- ↳ 📎 Reply attachments never reached the customer
- ↳ 🛠️ Outbound email attachments — Developer Guide
- ↳ 🔑 A global SSO provider was missing from the portal
- ↳ 🔀 Behind a proxy, the SSO redirect said http
- ↳ ✏️ The portal tagline moved when you saved it
- ↳ 🎨 The portal settings screen forgot what you saved
- ↳ 🛡️ The approvals inbox said "Error" and nothing else
- ↳ 📄 A table's answers were missing from the PDF
- ↳ ◉ A single-select column let you tick every option
- ↳ 📐 The portal ignored a form's field widths
- ↳ 📋 The tasks board stopped taking clicks
- ↳ 🗂️ #121 The index list is out of date after upgrading
- ↳ 📅 #133 The calendar subscription was empty
- ↳ 📋 #131 Tasks always reopened on the board
- ↳ 💥 #129 Every page returned HTTP 500 after upgrading
- ↳ 🐳 #127 A PHP warning above the System page
- ↳ 🕐 #126 Notes stamped with the server's clock
- ↳ 🌍 Storing every date in UTC
- ↳ 🚪 The portal was down for everyone signed in
- ↳ ⚙️ #120 Workflow notes could never be written
- ↳ ⚙️ #123 Three errors when running Database Verification
- ↳ 📝 #122 The description box was a stub in the corner
- ↳ 💣 Demo data deleted real accounts
- ↳ 🔐 #117 Sign-in redirected to the wrong address
- ↳ 🎨 #108 The priority dot was invisible
- ↳ ⏱️ #116 Time logged from the right-click menu
- ↳ 🔑 #114 API keys refused by our own guard
- ↳ 🗂️ #110 Assigning a task told nobody
- ↳ 🚪 #107 Signed out while still working
- ↳ 📎 #103 "Share with Requester" reached nobody
- ↳ 🔍 #102 Search found nothing for hyphens
- ↳ 🪟 #101 Source code editor opened behind
- ↳ ☑️ #88 Subtasks could not be ticked off
- ↳ 💻 #84 Asset deep link selected nothing
- ↳ 🎫 #79 A new ticket arrived with no status
- ↳ 📧 #79 A ticket from email did not say so
- ↳ 🔔 #78 Bell opened to nothing
- ↳ 📬 #77 Mail only collected from Inbox
- ↳ 🔐 #74 The default password could not be changed
- ↳ 🚦 #70 Renaming an impact level
- ↳ 📤 #67 App-only mailboxes could not send
- ↳ 📭 #45 Verify only ever worked for Microsoft
- ↳ 📭 #45 IMAP reported as not authenticated
- ↳ ✉️ An email template stopped escaping itself
- ↳ 🕐 The portal dashboard showed the wrong time
- ↳ 🔢 The folder said 99 and the list showed 96