Quick Start
Copy this minimal template, swap in your module theme, and start building. All components auto-initialise — no JavaScript calls required for standard use.
<!DOCTYPE html>
<html lang="en" data-theme="light" data-ui-module="pulse">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="ui-theme" content="pulse">
<title>My App</title>
<!-- Vendor -->
<link rel="stylesheet" href="https://dev.lib.myoasishealth.ca/ui/v1/vendor/bootstrap/bootstrap.min.css">
<!-- UI Design System -->
<link rel="stylesheet" href="https://dev.lib.myoasishealth.ca/ui/v1/css/ui-core.css">
<link rel="stylesheet" href="https://dev.lib.myoasishealth.ca/ui/v1/css/ui-layout.css">
<link rel="stylesheet" href="https://dev.lib.myoasishealth.ca/ui/v1/css/ui-components.css">
<link rel="stylesheet" href="https://dev.lib.myoasishealth.ca/ui/v1/css/ui-utilities.css">
<!-- Module theme (swap for your module) -->
<link rel="stylesheet" href="https://dev.lib.myoasishealth.ca/ui/v1/themes/pulse.css">
</head>
<body>
<div class="ui-sidebar-overlay" id="ui-sidebar-overlay"></div>
<div class="ui-wrapper">
<!-- Header, sidebar, content, footer go here -->
<!-- See "Page Structure" section below -->
</div>
<!-- Vendor -->
<script src="https://dev.lib.myoasishealth.ca/ui/v1/vendor/bootstrap/bootstrap.bundle.min.js"></script>
<script src="https://dev.lib.myoasishealth.ca/ui/v1/vendor/datatables/dataTables.bootstrap5.min.js"></script>
<!-- UI Design System -->
<script src="https://dev.lib.myoasishealth.ca/ui/v1/js/ui-core.js"></script>
<script src="https://dev.lib.myoasishealth.ca/ui/v1/js/ui-forms.js"></script>
<script src="https://dev.lib.myoasishealth.ca/ui/v1/js/ui-tables.js"></script>
<script src="https://dev.lib.myoasishealth.ca/ui/v1/js/ui-notifications.js"></script>
</body>
</html>
dev.lib.myoasishealth.ca to lib.myoasishealth.ca when deploying to production. No other changes needed.
Asset Reference
All assets are served from the central library server. Load order matters — follow the sequence below.
CSS — load in <head>, in this order
| File | Purpose | Required |
|---|---|---|
vendor/bootstrap/bootstrap.min.css |
Bootstrap 5 base — grid, utilities, resets | Yes |
vendor/datatables/dataTables.bootstrap5.min.css |
DataTables Bootstrap 5 skin | Tables only |
css/ui-core.css |
Design tokens, typography, dark mode, base resets | Yes |
css/ui-layout.css |
App shell — header, sidebar, content area, footer | Yes |
css/ui-components.css |
All components — cards, buttons, alerts, badges, forms, modals, toasts | Yes |
css/ui-utilities.css |
Spacing, flex, text helpers — .ui-mb-md, .ui-d-flex, etc. |
Recommended |
themes/{module}.css |
Module primary colour — overrides --ui-primary |
Yes |
JS — load before </body>, in this order
| File | Purpose | Required |
|---|---|---|
vendor/bootstrap/bootstrap.bundle.min.js |
Bootstrap JS (includes Popper) | Yes |
vendor/datatables/dataTables.bootstrap5.min.js |
DataTables core + Bootstrap 5 integration | Tables only |
js/ui-core.js |
Bootstraps all components — sidebar, modals, alert dismiss, theme toggle. Exposes window.UI. Load first. |
Yes |
js/ui-forms.js |
Form validation. Auto-initialises [data-ui-validate] forms. |
If using forms |
js/ui-tables.js |
DataTables wrapper. Auto-initialises [data-ui-table] elements. |
If using tables |
js/ui-notifications.js |
Toast notifications. Exposes UI.notify(). |
If using toasts |
Minified files
Every CSS and JS file ships with a .min counterpart. Use minified files in production for performance.
<!-- CSS -->
https://lib.myoasishealth.ca/v1/css/ui-core.min.css
https://lib.myoasishealth.ca/v1/css/ui-layout.min.css
https://lib.myoasishealth.ca/v1/css/ui-components.min.css
https://lib.myoasishealth.ca/v1/css/ui-utilities.min.css
https://lib.myoasishealth.ca/v1/themes/pulse.min.css
<!-- JS -->
https://lib.myoasishealth.ca/v1/js/ui-core.min.js
https://lib.myoasishealth.ca/v1/js/ui-forms.min.js
https://lib.myoasishealth.ca/v1/js/ui-tables.min.js
https://lib.myoasishealth.ca/v1/js/ui-notifications.min.js
Page Structure
The app shell uses a fixed header, collapsible sidebar, scrollable content area, and sticky footer. Copy this HTML skeleton into every page.
<body>
<!-- Sidebar backdrop (mobile tap-to-close) -->
<div class="ui-sidebar-overlay" id="ui-sidebar-overlay"></div>
<div class="ui-wrapper">
<!-- ── Header ─────────────────────────────────────── -->
<header class="ui-header">
<a href="/" class="ui-header__brand">
<div class="ui-header__avatar">P</div>
<span class="ui-header__brand-name">My App</span>
<span class="ui-header__module-tag">Pulse</span>
</a>
<button class="ui-header__toggle" data-ui-sidebar-toggle aria-label="Toggle sidebar">
☰
</button>
<div class="ui-header__search">
<input type="search" class="ui-header__search-input" placeholder="Search…">
</div>
<div class="ui-header__actions">
<button class="ui-header__icon-btn" aria-label="Notifications">🔔</button>
<button class="ui-theme-toggle" data-ui-theme-toggle aria-label="Toggle theme">☀/☾</button>
<button class="ui-header__user" aria-label="User menu">
<div class="ui-header__avatar">RS</div>
<div class="ui-header__user-info">
<span class="ui-header__user-name">Raheel Sajid</span>
<span class="ui-header__user-role">Administrator</span>
</div>
</button>
</div>
</header>
<div class="ui-body">
<!-- ── Sidebar ──────────────────────────────────── -->
<aside class="ui-sidebar" id="ui-sidebar" aria-label="Main navigation">
<div class="ui-sidebar__inner">
<nav>
<ul class="ui-nav">
<li class="ui-nav__section">Main</li>
<li>
<a href="/dashboard" class="ui-nav__link ui-nav__link--active">
<span class="ui-nav__icon">◉</span>
<span class="ui-nav__text">Dashboard</span>
</a>
</li>
<li>
<a href="/patients" class="ui-nav__link">
<span class="ui-nav__icon">+</span>
<span class="ui-nav__text">Patients</span>
</a>
</li>
</ul>
</nav>
<div class="ui-sidebar__footer">
<span>v1.0.0</span>
</div>
</div>
</aside>
<!-- ── Content ──────────────────────────────────── -->
<main class="ui-content">
<!-- Page header (breadcrumb + actions) -->
<div class="ui-page-header">
<div class="ui-page-header__left">
<h1 class="ui-page-header__title">Page Title</h1>
<p class="ui-page-header__breadcrumb">Module · Section</p>
</div>
<div class="ui-page-header__actions">
<button class="ui-btn ui-btn--primary">New Record</button>
</div>
</div>
<!-- Page body -->
<div class="ui-content__body">
<!-- Your page content here -->
</div>
</main>
</div><!-- /ui-body -->
<!-- ── Footer ──────────────────────────────────────── -->
<footer class="ui-footer">
<span>© 2025 Oasis Family Health Centre</span>
<span class="ui-footer__env ui-footer__env--dev">Development</span>
<span class="ui-footer__version">v1.0.0</span>
</footer>
</div><!-- /ui-wrapper -->
</body>
Footer environment indicator
Control the coloured dot in the footer with the modifier class on .ui-footer__env:
<span class="ui-footer__env ui-footer__env--dev">Development</span>
<span class="ui-footer__env ui-footer__env--staging">Staging</span>
<span class="ui-footer__env ui-footer__env--prod">Production</span>
Module Themes
Each application loads its own theme CSS file. The theme overrides --ui-primary, --ui-primary-light, and --ui-primary-hover. All other tokens are shared.
themes/pulse.cssthemes/track.cssthemes/security.cssthemes/finance.cssthemes/reports.cssthemes/document-manager.css<!-- 1. Set the module attribute on <html> -->
<html lang="en" data-theme="light" data-ui-module="pulse">
<!-- 2. Set the meta tag (used by ui-core.js for future programmatic use) -->
<meta name="ui-theme" content="pulse">
<!-- 3. Load the theme CSS after ui-components.css -->
<link rel="stylesheet" href="https://dev.lib.myoasishealth.ca/ui/v1/themes/pulse.css">
data-ui-module attribute is informational only in v1 — it does not affect CSS. The theme file controls the visual accent colour.
Dark Mode
Two mechanisms work together. You do not need to write any dark-mode CSS — the design system handles it automatically.
| Mechanism | How it works |
|---|---|
| System preference | When <html> has no data-theme attribute (or data-theme="auto"), dark mode activates automatically if the OS is set to dark. No code needed. |
| Explicit toggle | Add data-ui-theme-toggle to any button. ui-core.js wires it up automatically and persists the choice to localStorage. |
| Programmatic | Call UI.theme.set('dark'), UI.theme.set('light'), or UI.theme.toggle(). |
UI.theme.get(); // returns 'light' | 'dark' | 'auto'
UI.theme.set('dark'); // force dark, persists to localStorage
UI.theme.set('light'); // force light
UI.theme.set('auto'); // remove explicit preference, defer to OS
UI.theme.toggle(); // switches between light and dark
// Listen for changes
document.addEventListener('ui:theme:change', function (e) {
console.log('Theme changed to:', e.detail.theme);
});
JavaScript API
All public methods live on window.UI. Auto-initialisation runs on DOMContentLoaded — for standard use, no manual calls are needed.
UI.modal
// Open by overlay element ID
UI.modal.open('my-modal');
// Close the currently open modal
UI.modal.close();
// Close a specific modal
UI.modal.close('my-modal');
// Attribute-driven (no JS needed)
<button data-ui-modal-open="my-modal">Open</button>
<button data-ui-modal-close>Close</button>
UI.notify
// Basic
UI.notify('Record saved.', 'success');
// All options
UI.notify('Message', 'warning', {
duration: 4000, // ms; 0 = sticky until dismissed
position: 'bottom-right', // bottom-right | bottom-left | top-right | top-center
dismissable: true // show close button
});
// Shorthand methods
UI.notify.success('Record saved.');
UI.notify.warning('Session expiring soon.');
UI.notify.danger('Connection failed.');
UI.notify.info('Update available.');
// Dismiss programmatically
var toast = UI.notify('Processing…', 'info', { duration: 0 });
toast.dismiss();
UI.form
// Auto-init runs on DOMContentLoaded for [data-ui-validate] forms.
// For forms added dynamically after page load:
var form = document.getElementById('my-form');
UI.form.init(form);
// Reset all validation state on a form
UI.form.reset(form);
// Validate a single field programmatically
var field = document.getElementById('email');
var isValid = UI.form.validateField(field); // returns true | false
// Custom error message via data attribute
<input type="email" class="ui-input" data-ui-error="Please enter a valid clinic email.">
UI.sidebar
// Toggle (attribute-driven — no JS needed)
<button data-ui-sidebar-toggle>☰</button>
// Programmatic
UI.sidebar.toggle(); // open/close on mobile; collapse/expand on desktop
UI.sidebar.close(); // always closes
// Desktop collapsed state persists in localStorage automatically
Event System
Components fire CustomEvent using the ui: prefix. Listen with addEventListener on the element or document.
| Event | Fires on | Detail payload |
|---|---|---|
| ui:form:submit | form element | { formData: FormData, form: HTMLElement } |
| ui:form:error | form element | { form: HTMLElement } |
| ui:table:row-click | table element | { row: HTMLElement } |
| ui:table:export | table element | { format: string } |
| ui:notification:dismissed | document | { id: number } |
| ui:theme:change | document | { theme: 'light' | 'dark' | 'auto' } |
// Handle form submit (receives validated FormData)
document.getElementById('patient-form').addEventListener('ui:form:submit', function (e) {
var data = Object.fromEntries(e.detail.formData);
fetch('/api/patients', { method: 'POST', body: JSON.stringify(data) })
.then(function () { UI.notify('Patient saved.', 'success'); });
});
// Handle table row click
document.getElementById('my-table').addEventListener('ui:table:row-click', function (e) {
var row = e.detail.row;
// open a modal, navigate, etc.
});
// React when a toast is dismissed
document.addEventListener('ui:notification:dismissed', function (e) {
console.log('Toast', e.detail.id, 'dismissed');
});
Form Validation
Built on the browser Constraint Validation API. No configuration needed beyond HTML attributes.
How it works
| Behaviour | Trigger |
|---|---|
| Validate field on first blur (if touched or required) | User leaves field |
| Re-validate while typing to clear error early | User types in an errored field |
| Validate all fields, block submit if any fail | Form submit |
Fire ui:form:submit with FormData | All fields valid on submit |
| Focus first invalid field | Submit with errors |
HTML attributes used
<form data-ui-validate>
<div class="ui-form-group">
<!-- Required -->
<input class="ui-input" required>
<!-- Email pattern -->
<input class="ui-input" type="email" required>
<!-- Min / max length -->
<input class="ui-input" minlength="3" maxlength="50">
<!-- Numeric range -->
<input class="ui-input" type="number" min="1" max="100">
<!-- Regex pattern -->
<input class="ui-input" pattern="[A-Z]{2}[0-9]{4}">
<!-- Custom error message (overrides browser default) -->
<input class="ui-input" type="email" required
data-ui-error="Enter a valid @clinic.ca email address.">
<!-- Required label asterisk -->
<label class="ui-label ui-label--required" for="name">Full Name</label>
<!-- Error message container (populated by ui-forms.js) -->
<span class="ui-form-error"></span>
</div>
</form>
Icons
Icons are inline SVG — copy from tabler.io/icons as needed. No file download or sprite required. Icons inherit colour via currentColor and scale with em units.
<!-- Copy SVG from tabler.io/icons. Size and colour are inherited. -->
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<circle cx="12" cy="7" r="4"/>
<path d="M6 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2"/>
</svg>
<!-- Inside a button -->
<button class="ui-btn ui-btn--primary">
<svg ...>...</svg>
Save Patient
</button>
Versioning
Assets are served under a version path (/v1/). Applications pin to a version and are not affected by updates to other versions.
<!-- Always reference a specific version -->
https://lib.myoasishealth.ca/v1/css/ui-core.css
^^^ pin here
<!-- When v2 ships, existing apps remain on /v1/ -->
<!-- Opt-in to /v2/ only after testing -->
https://lib.myoasishealth.ca/v2/css/ui-core.css
Dev vs Production
<!-- Development (change href only — all class names remain identical) -->
<link href="https://dev.lib.myoasishealth.ca/ui/v1/css/ui-core.css" ...>
<!-- Production -->
<link href="https://lib.myoasishealth.ca/v1/css/ui-core.css" ...>
Use a server-side variable or build configuration to swap the base URL. The class names, data attributes, and JavaScript API are identical across environments.
dev.lib.myoasishealth.ca from a production application. The dev server may be unstable or unavailable.
Adding a New Application
Follow this checklist when integrating a new application for the first time.
-
Choose a module theme colour. Check the existing themes — Pulse (teal), TRACK (indigo), Security (blue), Finance (green), Reports (orange), Document Manager (slate). Pick a distinct hue and add it to
docs/ui-design-system-todo.mdunder Module Branding. -
Create a theme file. Add
ui/v1/themes/{module}.csswith the three primary colour overrides::root { --ui-primary: #your-colour; --ui-primary-light: #your-colour-at-15%-opacity; --ui-primary-hover: #your-colour-darkened-10%; } [data-theme="dark"] { --ui-primary-light: rgba(r, g, b, 0.2); } -
Build the minified theme. Run
npm run buildfrom the repo root to generate{module}.min.css. -
Copy the page shell. Use the Quick Start template above. Set
data-ui-moduleon<html>and the<meta name="ui-theme">tag to your module name. -
Build the sidebar server-side. Render
<ul class="ui-nav">items from your application's route and permission model. Add.ui-nav__link--activeon the current page. The UI does not need to know about your routes. -
Point to the dev server. Use
dev.lib.myoasishealth.caURLs during development. Switch tolib.myoasishealth.cabefore go-live. -
Test core behaviours. Verify: sidebar toggle (mobile + desktop), dark mode toggle, form validation, toast notifications. Check at mobile (375px), tablet (768px), and desktop (1280px).
-
Mark integration complete. Check the application off in the Phase 1 Integration checklist in
docs/ui-design-system-todo.md.