verified_userExternal use: approval required. If you are outside Streets of Growth, any materials you create using these assets must be signed off before use. Please send your work to salik@streetsofgrowth.org for approval.
Design System v2.0
Last updated: 11 Aug 2026
Streets of Growth: Brand & UX/UI Style Guide
The single source of truth for all digital SOG products: covering visual identity, components, data visualisation, and accessibility standards.
palette
9
Brand colours with full tint scales
bar_chart
20+
Chart & data viz patterns
smart_button
12+
UI component groups
verified_user
WCAG
AA accessibility standards
These guidelines are the single source of truth for all digital SOG products: colour tokens, typography, interactive components, data visualisation, and accessibility so Streets of Growth shows up consistently everywhere.
palette
For Designers
Use SOG tokens (CSS variables) in all Figma variables
Never create one-off colours extend from the tint scale
Reference live examples to match spacing and typographic hierarchy exactly
Always verify contrast ratios against the Accessibility section
code
For Developers
Copy the CSS variables block from the Colour section onto :root
Always reference tokens as var(--sog-purple) never hardcode hex
Import Open Sans from Google Fonts (weights 300/400/600/700)
Use Chart.js 4.4.0 with the SOG_COLORS constant for all charts
Company
Who We Are
This section explains who Streets of Growth is, where we come from, and how we work the foundation that informs every design decision in this guide. Understanding our story and approach is essential context for applying the brand correctly.
diversity_3
Our Organisation
We are a dynamic grassroots charity with a pioneering approach for re-engaging young adults aged 15–25 to reduce harm and transform their lives through developing safer lifestyles, lived environments and maintaining education and career progression.
history
Our Roots
Established in 2001, Streets of Growth was born out of our own lived experiences of growing up and navigating the streets and council estate realities in east London. We are a multiculturally diverse team of Intervention Coaches and Entrepreneurs, some of whom were once young recipients of these incredible services ourselves.
route
Our Approach
Our innovative and profoundly dedicated team coach and support young adults over the long term and journey with them until they can maintain progression independently of us. This is who we are and why we have a multi-award-winning reputation and track record for doing it.
location_city
East London
Grassroots charity
calendar_today
Est. 2001
20+ years of impact
people
Ages 15–25
Young adults in harm
emoji_events
Multi-award winning
Proven track record
Company
Our Vision, Purpose & Mission
The statements that guide everything Streets of Growth does: a bold vision for the world we want to see, and a clear purpose and mission defining exactly how we act on it.
visibility
Our Vision
A future where poverty, violence, criminality and exploitation no longer define or destroy young people’s life chances, choices and potential.
my_location
Our Purpose & Mission
We exist to work with young people aged 15–21, experiencing and engaged in harms that extend beyond the home. We use long-term outreach and interventions that build Relationships for Change, address harm, develop behavioural skills and community leadership, and sustain young people’s progression in education, training and into work - until equipped to do so independently of us!
Company
Our Core Values
We strive to practice and hold ourselves and each other accountable to our organisational values - in how we operate, act with integrity, treat people, and make ethical decisions. Values are our moral compass, keeping us aligned to our vision, purpose, mission and impact culture.
verified
Being Professional
Professionalism is an expected essential for staff here. Holding ourselves and each other accountable and responsible for demonstrating the highest standard of employee skill competences and behavioural skill disciplines that we coach and equip young people with - is vital. Having lived experience and ‘being authentic’ alone is simply not enough, as it does not guarantee professionalism or a person’s ability to perform at a high level and contribute effectively to our professional working environment.
diversity_3
Belonging
People thrive on diversity here, and where belonging means removing a divisive ‘us versus them’ blame culture mentality. This is why safety, trust and honest communication are relentlessly encouraged. As important, being a multi-cultural employer, we intentionally engage young people from all different ethnicities and cultural backgrounds, removing barriers so they can access opportunities and resources for achieving fair outcomes, equality, equity and sustained progression.
bolt
Dynamic
We fully understand how important it is to stay agile and adaptable. Streets of Growth’s practitioners characterise ‘dynamic’ by their energy, enthusiasm, and having the willingness and creative ability to respond to change. Being proactive, emotionally resilient, open to challenge and make changes - we continually strengthen performance while staying focused on delivering safe, consistent outcomes for young people in complex contexts.
handshake
Collaborative
No matter how effective we are as an organisation, we achieve far more in strategic intentional partnerships than we can alone. Collaboration isn’t easy, especially when organisations, communities, and even our staff amongst ourselves, may have differing or opposing priorities and perspectives. But we see this creative tension and conflict as a natural and inevitable aspect of change and strive to be adept at navigating and negotiating through such challenges with respect, empathy and maturity.
analytics
Evidence-led
Real results matter! Our frontline practice works in tandem with our internal Centre for Applied Research and Evaluation, which informs and guides our work with the best available evidence derived from our frontline practice, applied research, science, and real experience. We take data input and extraction extremely seriously when measuring outcomes and impacts, and we are careful to make the clear distinction between ‘assumed evidence’ and ‘undeniable evidence’.
Foundations
Logo Usage
The SOG logo is the most visible expression of our brand. Consistent application builds recognition and trust. Follow these rules across all digital, print, and environmental applications.
folder_openLogo assets
All approved logo files in every format (SVG, PNG, EPS). External partners: contact salik@streetsofgrowth.org to request the asset pack.
Four approved logo configurations are available. Always use the original artwork do not recreate from type. Choose the variant most appropriate for the context and background.
When to use each variant
Choose the variant that suits the background. Never create new variants or recolour outside these four options.
Primary: Horizontal Lockup
Default for all digital products, documents, and presentations. Use on white or light backgrounds. Min width: 120px.
Reversed: White on Gradient
Use on brand gradient backgrounds only. Never on white or light backgrounds.
Reversed: On Charcoal
Use on dark charcoal (#323E48) backgrounds, dark-mode interfaces, or dark printed sections.
Mark Only: Icon / App Icon
Use only when the full logotype would be illegible: app icons, favicons, social profiles, placements under 60px. Never substitute for the full logo where space allows.
Clear Space
Always maintain a minimum clear space zone around the logo equal to the height of the "S" in Streets of Growth (denoted x). No other graphic elements, text, imagery, or page edges should encroach on this zone.
x
x
x
x
Minimum Size
To ensure legibility, never reproduce the logo smaller than the minimums below. Use the monogram variant when space is too constrained for the full lockup.
Full Logo: Digital
68px
/ 24mm print
Minimum width for the full horizontal lockup. Below this size the logotype becomes illegible.
Monogram: Digital
24px
/ 4mm print
Minimum size for the SOG monogram mark. Used for app icons, favicons, and social profile images.
Sizing & Positioning for Print
Consistent logo sizing across paper formats maintains a professional look and feel. The logotype should be prominently displayed on the front of all documentation and publicity materials. Maintain a minimum margin of 12.7 mm from the document edge.
Logo Width by Paper Size
Paper
Width
Height
A3
60mm
Relative to width
A4
50mm
Relative to width
A5
40mm
Relative to width
A6
30mm
Relative to width
Positioning Rules
Top or bottom left (default)
On most documents the logo should appear in the top or bottom left position within the margin.
Centre: use with caution
Centre alignment is only appropriate for specific formats such as roll-up banners, background slides, or full-bleed graphics.
12.7 mm from margin
Maintain a minimum distance of 12.7 mm between the logo and any document margin or edge.
Monogram Usage
When space is limited such as online profiles, social media avatars, app icons, or favicons use only the SOG monogram rather than the full lockup to ensure a clean, professional appearance and avoid clutter.
Standard Monogram
Colour gradient version. Use on white and light backgrounds. Min size: 24px / 4mm.
Reverse Monogram
All-white version for dark or coloured backgrounds. Min size: 24px / 4mm.
Use monogram for
Social media profile images
App icons & favicons
Online directory profiles
Badge & thumbnail contexts
Embroidery & small merchandise
Approved Colour Backgrounds
The SOG logo may be placed on the following backgrounds. Always verify sufficient contrast between the logo and the background before use.
White / Off-White
Primary (colour mark)
Purpose Black
Reversed (white wordmark)
Brand Gradient
All-white reversed variant
Purple Tint 10%
Light tint surfaces
What to Avoid
Never alter, distort, or misuse the SOG logo. These rules apply across all applications digital, print, environmental, and merchandise.
block
Don't resize or reposition elements
Never resize or change the position of any individual elements or words within the logo.
block
Don't change the colour
Never change the logo colours, even to a similar colour. Use only the approved variants.
block
Don't add effects or shadows
Never apply drop shadows, glows, gradients, emboss, or any other visual effects to the logo.
block
Don't rotate or skew
Always display the logo horizontally and upright. Never rotate, skew, or distort the lockup in any way.
block
Don't crop the logo
Always display the logo in full. Never crop, clip, or partially obscure any element of the lockup.
block
Don't add transparency
Never reduce the logo opacity or apply transparency to any part of the logo.
block
Don't frame within a shape
Never enclose the logo inside a circle, box, badge, or any other constrained shape or container.
block
Don't place on busy backgrounds
Never place the logo over complex imagery, patterns, or textures that reduce legibility.
File Formats: The master logo files are available in SVG (vector, for digital), EPS (vector, for print), and PNG (raster, for web/social). Always request the correct format for your use case from the brand team. Never export or convert logo files yourself without approval.
Foundations
Colour Palette
The SOG palette has six primary brand colours, each tied to a core brand value, plus four secondary utility colours for status and feedback states. Use the CSS custom properties consistently across all digital touchpoints, and never use the secondary colours as primary brand colours in UI.
Professional Purple
#625D9C
Contrast on white: 5.89:1
RGB
98, 93, 156
CMYK
37 / 40 / 0 / 39
Pantone
2105 C
Belonging Blue
#4197CB
Contrast on white: 3.22:1
RGB
65, 151, 203
CMYK
68 / 26 / 0 / 20
Pantone
7461 C
Collaborative Sky
#5EB3E4
Contrast on white: 2.32:1
RGB
94, 179, 228
CMYK
59 / 21 / 0 / 11
Pantone
292 C
Dynamic Teal
#0095A9
Contrast on white: 3.58:1
RGB
0, 149, 169
CMYK
100 / 12 / 0 / 34
Pantone
7711 C
Independence Green
#00AF9A
Contrast on white: 2.76:1
RGB
0, 175, 154
CMYK
100 / 0 / 12 / 31
Pantone
3265 C
Purpose Black
#323E48
Contrast on white: 10.95:1
RGB
50, 62, 72
CMYK
31 / 14 / 0 / 72
Pantone
432 C
Warning Red
#F32735
Danger / Error states
RGB
243, 39, 53
CMYK
0 / 84 / 78 / 5
Pantone
186 C
Caution Orange
#FF6C0E
Warning / Elevated risk
RGB
255, 108, 14
CMYK
0 / 58 / 94 / 0
Pantone
165 C
Alert Yellow
#F2CD00
Caution / Low-contrast use only
RGB
242, 205, 0
CMYK
0 / 15 / 100 / 5
Pantone
116 C
Off-White
#F5F5F7
Surface / Background
RGB
245, 245, 247
CMYK
1 / 1 / 0 / 3
Pantone
9144 C
Tint Scales
Each colour has four precomputed tints (10%, 20%, 40%, 70%). Use these for backgrounds, hover states, badges, and subtle UI elements. Never mix tints from different colour families in the same component.
Purple
10%
20%
40%
70%
100%
Blue
10%
20%
40%
70%
100%
Sky
10%
20%
40%
70%
100%
Teal
10%
20%
40%
70%
100%
Green
10%
20%
40%
70%
100%
Charcoal
10%
20%
40%
70%
100%
Red
10%
20%
40%
70%
100%
Orange
10%
20%
40%
70%
100%
Yellow
10%
20%
40%
70%
100%
Shade Scales
Shades are created by mixing each colour with black, producing darker variants. Use shades for pressed states, deep backgrounds, high-contrast borders, and text that must appear on coloured backgrounds. Five levels per colour: 10%, 20%, 40%, 70%, and 100% black added.
Purple
Base
+10% Black
+20% Black
+40% Black
+70% Black
Blue
Base
+10% Black
+20% Black
+40% Black
+70% Black
Sky
Base
+10% Black
+20% Black
+40% Black
+70% Black
Teal
Base
+10% Black
+20% Black
+40% Black
+70% Black
Green
Base
+10% Black
+20% Black
+40% Black
+70% Black
Charcoal
Base
+10% Black
+20% Black
+40% Black
+70% Black
Red
Base
+10% Black
+20% Black
+40% Black
+70% Black
Orange
Base
+10% Black
+20% Black
+40% Black
+70% Black
Yellow
Base
+10% Black
+20% Black
+40% Black
+70% Black
Tone Scales
Tones are created by mixing each colour with mid-grey, producing muted, desaturated variants. Use tones for disabled states, secondary content on coloured surfaces, and subtle decorative fills. Four levels per colour: 10%, 20%, 40%, and 70% grey added. Rule: use tints for backgrounds and UI surfaces, shades for hover and pressed states, and tones for disabled states and greyscale-adjacent contexts. Never mix scales from different colour families in the same component.
Purple
Base
+10% Grey
+20% Grey
+40% Grey
+70% Grey
Blue
Base
+10% Grey
+20% Grey
+40% Grey
+70% Grey
Sky
Base
+10% Grey
+20% Grey
+40% Grey
+70% Grey
Teal
Base
+10% Grey
+20% Grey
+40% Grey
+70% Grey
Green
Base
+10% Grey
+20% Grey
+40% Grey
+70% Grey
Charcoal
Base
+10% Grey
+20% Grey
+40% Grey
+70% Grey
Red
Base
+10% Grey
+20% Grey
+40% Grey
+70% Grey
Orange
Base
+10% Grey
+20% Grey
+40% Grey
+70% Grey
Yellow
Base
+10% Grey
+20% Grey
+40% Grey
+70% Grey
Colour Usage Rules
Do
Use 2–3 brand colours maximum per layout
Use Purpose Black (#323E48) for all body text
Use tints for backgrounds and subtle accents
Reserve secondary colours for status/feedback only
Test all colour combinations against WCAG AA
Don't
Don't use all 6 primary colours at once
Don't use brand colours as small body text on white
Gradients introduce depth and dynamism. Whether applied to backgrounds, cards, hero areas, or typography, they add an extra layer to visual communication. Use judiciously limit to one gradient per layout and never combine two gradient elements on the same spread.
Brand Gradients
Approved gradient pairings from the brand palette. The first five combine two primary values-led colours; the rest blend a secondary accent (red, orange or yellow) with a primary for higher-energy moments. Blend a secondary with a primary only, never two secondaries together.
Signature
#625D9C → #0095A9
Professional Purple to Dynamic Teal. Primary brand gradient for hero areas and key CTAs.
Each primary colour fading to its darkest shade (Shade 4). These monochromatic gradients work well for dark hero sections, card overlays, and backgrounds that need depth without mixing hues.
The Signature gradient shown at four standard angles. Use 45deg as the default; adjust only when layout requires.
north_east
Diagonal ✓
45deg (default)
east
Horizontal
to right
south
Vertical
to bottom
south_east
Diagonal Alt
135deg
Usage Rules
Do
Use only approved gradient pairings from this guide
Apply to full-bleed hero sections, card headers, or accent bars
Use white or off-white text on any gradient background
Limit to one gradient element per page or spread
Don't
Do not blend two secondary (alert) colours together; pair a secondary accent with a primary only
Do not place dark body text directly on a gradient
Do not combine two gradient elements on the same spread
Do not use more than two colour stops in a gradient
CSS values
Copy these values directly into your stylesheet. Always use the exact hex values from the brand palette never approximate gradient colours.
CSS
/* SOG Brand Gradients - copy these values directly */
/* Primary: Purple to Teal (hero, key CTAs) */
background: linear-gradient(45deg, #625D9C 0%, #0095A9 100%);
/* Secondary: Purple to Blue (section headers, cards) */
background: linear-gradient(45deg, #625D9C 0%, #4197CB 100%);
/* Growth: Blue to Green (progress, positive outcomes) */
background: linear-gradient(45deg, #4197CB 0%, #00AF9A 100%);
/* Dynamic: Teal to Green (engagement, action states) */
background: linear-gradient(45deg, #0095A9 0%, #00AF9A 100%);
/* Deep: Charcoal to Purple (dark sections, footers) */
background: linear-gradient(45deg, #323E48 0%, #625D9C 100%);
/* Direction variants */
background: linear-gradient(45deg, ...); /* diagonal - default */
background: linear-gradient(90deg, ...); /* horizontal */
background: linear-gradient(180deg, ...); /* vertical */
background: linear-gradient(45deg, ...); /* shallow diagonal */
/* Rule: gradients are for backgrounds only.
Never apply a gradient to text or interactive elements. */
Foundations
Typography
All digital SOG products use Open Sans exclusively never introduce a second typeface. The type scale runs from 10px captions to 56px display text across four weights: Light (300), Regular (400), Medium (600), and Bold (700). On mobile viewports (≤480px) scale the body and heading sizes down by one step to maintain readability at smaller widths.
Weight Specimens
Open Sans Light 300 The quick brown fox jumps over the lazy dog
Open Sans Regular 400 The quick brown fox jumps over the lazy dog
Open Sans Medium 600 The quick brown fox jumps over the lazy dog
Open Sans Bold 700 The quick brown fox jumps over the lazy dog
Type Scale
Display XL56px / 700 / -0.5px
Heading
Display LG48px / 700 / -0.3px
Heading
Display MD40px / 700 / -0.2px
Heading
Display SM32px / 700 / -0.1px
Section Heading
Headline LG24px / 700 / 0px
Card Title or Modal Heading
Headline MD20px / 600 / 0px
Subsection title or table heading group
Headline SM18px / 600 / 0px
Widget header or sidebar section label
Body LG16px / 400 / 0px
Primary body copy. Used in descriptions, long-form content, and modal bodies.
Body MD14px / 400 / 0px
Standard UI text. Used in form labels, table cells, cards, and navigation items.
Body SM12px / 400 / 0px
Helper text, metadata, secondary information below inputs.
Caption10px / 700 / +0.08em
TABLE HEADER / LABEL / CHIP TEXT
Type Pairings
SECTION LABEL
Programme Overview
Track the progress of all active participants across their programme journey, from initial referral through to Phase 3 completion and follow-up.
Card or Widget Heading
Supporting description text that provides context to the heading above. Keep to 1–2 lines.
METRIC LABEL
142
Active participants this quarter ↑ 12% vs last quarter
Leading & Tracking
Style
Size
Line Height
Letter Spacing
Display XL
56px
1.1 (62px)
−0.5px
Display LG
48px
1.1 (53px)
−0.3px
Display MD
40px
1.15 (46px)
−0.2px
Display SM
32px
1.2 (38px)
−0.1px
Headline LG
24px
1.3 (31px)
0px
Headline MD
20px
1.3 (26px)
0px
Headline SM
18px
1.4 (25px)
0px
Body LG
16px
1.6 (26px)
0px
Body MD
14px
1.6 (22px)
0px
Caption / Label
10–11px
1.4
+0.06em–0.1em
Do
Use only Open Sans for all digital UI
Use weights 300, 400, 600, 700 only
Use tight tracking (negative) for large display text
All spacing is derived from a 4px base grid. Use multiples of 4px for all padding, margin, and gap values. The layout system uses a 12-column grid with a maximum content width of 1280px and 24px gutters.
Spacing Scale (4px base)
4px / xs
space-1
8px / sm
space-2
12px
space-3
16px / md
space-4
20px
space-5
24px / lg
space-6
32px / xl
space-8
40px
space-10
48px / 2xl
space-12
64px / 3xl
space-16
CSS
/* Spacing token usage - always reference by name, never hardcode px */
.card { padding: var(--space-4); } /* 16px / md */
.card-header { padding: var(--space-4) var(--space-6); } /* 16px / 24px */
.section { margin-bottom: var(--space-8); } /* 32px / xl */
.icon-gap { gap: var(--space-2); } /* 8px / sm */
.form-field { margin-bottom: var(--space-4); } /* 16px / md */
.table-cell { padding: var(--space-2) var(--space-4); } /* 8px / 16px */
/* When to use each step */
/* space-1 (4px) - icon inner padding, tight row gaps */
/* space-2 (8px) - between inline elements, badge padding */
/* space-3 (12px) - compact card padding */
/* space-4 (16px) - standard padding, form field spacing */
/* space-6 (24px) - section gaps, card padding on desktop */
/* space-8 (32px) - section margin-bottom, large component gaps */
/* space-12 (48px) - page section separation */
/* space-16 (64px) - hero / full-bleed section padding */
/* Rule: use custom px values only when a token is not close enough.
Document the reason in a CSS comment. Never use non-4px-multiple values. */
Border Radius Scale
Five tokens cover every rounding need across the product. Choose by component size and visual weight: tighter radii for compact or inline elements, larger radii for containers and overlays. Never use arbitrary pixel values outside this scale.
sm 4px
md 8px
lg 12px
xl 16px
full 9999px
Token
Value
Used on
--radius-sm
4px
Inline code chips, small table accents, minor UI detail
--radius-md
8px
Buttons (default), text inputs, dropdowns, icon buttons, search fields
--radius-lg
12px
Cards, data panels, icon containers, calendar popovers, chart wrappers
--radius-xl
16px
Modal dialogs, sheet panels, large overlay containers
--radius-full
9999px
Badges, status pills, tag labels. Use 50% for circular avatars.
Grid System
12-column grid: Maximum content width of 1280px, centred with auto margins. Column gutter: 24px. Page-level section padding: 64px horizontal, 64–80px vertical. On mobile (below 768px), reduce to single column with 24px horizontal padding.
12-column grid preview
Full width (12 col)
6 col
6 col
4 col
4 col
4 col
3
3
3
3
Foundations
Material Icons
SOG uses Google Material Symbols Rounded a variable-weight icon system with 2,500+ icons. Two styles are approved for brand use: Outlined Rounded (weight 300, FILL 0) for UI and interactive contexts, and Filled Rounded (weight 300, FILL 1) for active states and high-emphasis moments. All icons use rounded, curved corners to match the SOG brand aesthetic.
Approved Styles
Both styles use the Rounded variant (curved corners) at weight 300. Only these two are approved for SOG products.
person
Outlined Rounded
Default style. Curved corners, weight 300, FILL 0. Use for all standard UI icons, navigation, and secondary actions.
FILL 0 · wght 300 · Rounded
person
Filled Rounded
Active / emphasis style. Curved corners, weight 300, FILL 1. Use for active nav states, selected items, and high-emphasis callouts.
18px (inline), 24px (default), 36px (feature), 48px (empty state)
Minimum size
18px
Colour
Inherit from brand palette; never use standalone grey on white below 3:1 contrast
Touch target
44×44px minimum (pad with CSS if icon is smaller)
Accessibility
Add aria-label or aria-hidden="true" + visible text label
Foundations
Presentation Icons
SOG's Presentation Icons are custom-designed visual assets distinct from UI icons. They are used as images in presentations, PowerPoint decks, websites, infographics, and printed communications. Each illustration is stylised in SOG brand colours and designed to be instantly recognisable as an extension of our brand identity.
Presentation Icon Specifications
When designing or reproducing these icons, use the following settings to ensure consistency across all brand applications.
Property
Value
Canvas Size
500 × 500 px
Clear Space
68 px
Stroke Width
5 px
Colours
3–6 tones including white
Minimum Display Size
50 px
Outer Radius
14 px
Inner Radius
Half the selected outer radius
Recommended Sizes
50px
Minimum
100px
Standard
200px
Feature
500px
Hero
Do Not
Recolour or alter the illustration
Stretch, crop, or rotate the artwork
Remove or change the background
Use below the 50px minimum size
Use as a substitute for UI icons
Sample Icons
A selection from the full library of 127 custom Presentation Icons. The complete set is available in the SOG Brand Assets folder on OneDrive.
Education
Handshake
Target
Empathy
Leadership
Safe Place
Family
Brain
Careers
Social Action
Strategy
Graduation Party
Impact
Running
Values
Patterns
Design Rules
The authoritative implementation rules for all SOG digital products. Every rule here applies across every screen, component, and chart. When a decision is not covered by a specific component section, this section is the deciding reference.
App Shell
The primary layout container for all SOG application screens. A sticky 260px sidebar on the left, a fixed 56px top nav, and a scrollable main content area filling the remaining viewport. The sidebar collapses to an icon-only rail at viewports below 1024px and hides entirely below 768px behind a hamburger toggle.
4-column KPI row at the top, then a 2-column chart grid below. At 768px the KPI row becomes 2-column, at 480px everything stacks to a single column. Always use the KPI row for headline metrics: never place a chart at the very top of a dashboard view.
Status flows encode the valid state transitions for participants and tasks. Node colour follows the SOG badge token system: charcoal for pre-programme states, blue/teal for active programme states, green for positive outcomes, amber for intervention states, red for at-risk. Arrows show permitted transitions: never show a direct path that skips a required stage.
Referral Status
Referred
→
Assessed
→
Enrolled
→
Active
→
Closed
→
On Hold
↕
At Risk
Task Status
Not Started
→
In Progress
→
Complete
SOG Gradient Usage
Use the SOG gradient for hero banners, milestone celebration cards, and section headers where visual emphasis is required. Always use white or reversed text on gradient backgrounds. Never use the gradient as a background for body text blocks, never tile it, and never apply it to interactive elements other than buttons. The gradient button variant is reserved for high-emphasis primary CTAs only.
Programme Dashboard
Use the SOG gradient for hero banners, milestone celebration cards, and section headers. Never use as a background for body text.
Gradient progress bar
Motion & Animation
All motion in SOG products follows Emil Kowalski's design engineering principles. The goal is for every transition to feel immediate and physical, not decorative.
Rules
Default easing: ease-out
Standard duration: 150ms
Maximum duration: 300ms
Button press: scale(0.97) at 160ms ease-out
Stagger increment: 40–60ms per item, max 4 items
Never
Never use transition: all
Never use bare ease - always ease-out
Never animate keyboard-initiated navigation
Never exceed 300ms on any UI transition
Never use ease-in - elements must not hesitate before moving
Always implement @media (prefers-reduced-motion: reduce) on every animation and transition. This is not optional - it is a WCAG 2.3.3 requirement.
CSS
/* SOG motion tokens */
--transition-fast: 150ms ease-out;
--transition-med: 250ms ease-out;
--transition-spring: 350ms cubic-bezier(0.34, 1.56, 0.64, 1);
/* Button press - always include transform on interactive elements */
.btn { transition: background 150ms ease-out, transform 160ms ease-out; }
.btn:active { transform: scale(0.97); }
/* Named properties only - never transition: all */
.nav-item { transition: background 150ms ease-out, color 150ms ease-out; }
.card { transition: box-shadow 200ms ease-out, transform 200ms ease-out; }
.dropdown { transition: opacity 120ms ease-out, transform 120ms ease-out; }
/* Stagger - 40-60ms per item, max 4 items, stop at 240ms total */
.card:nth-child(1) { animation-delay: 0ms; }
.card:nth-child(2) { animation-delay: 50ms; }
.card:nth-child(3) { animation-delay: 100ms; }
.card:nth-child(4) { animation-delay: 150ms; }
/* Required on all animations and transitions */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Breakpoints
SOG products are designed desktop-first. The minimum supported viewport width is 320px. Below 768px the sidebar hides, below 480px all grids collapse to single column. Always test at exactly 320px, 768px, and 1024px: these are the three critical transition points.
The z-index scale is the authoritative stacking order for all SOG UI layers. Never assign an arbitrary z-index value: always use one from this scale. If a new layer needs to sit between two existing layers, add an intermediate token rather than incrementing by one.
These rules consolidate the colour constraints scattered across Colour Palette, Gradients, and Data Visualisation into one authoritative reference. When in doubt about whether a colour use is correct, check here first.
Do
Use SOG Purple as the primary interactive colour for links, focus rings, and active states
Use SOG Orange only for semantic warning states
Use SOG Red only for errors, danger states, and high-risk indicators
Use Charcoal for all body text and headings
Use tint scales (-10 bg, -40 border) for component surfaces
Use the gradient for hero sections and celebration moments only
Never
Never use SOG Teal as a heading colour. It is for data and engagement metrics only
Never use SOG Orange as a general accent colour. It is for semantic warnings only
Never apply the gradient to body text. Use white text on gradient backgrounds only
Never use Red, Orange, or Yellow as chart series colours in multi-series charts
Never mix tint scales from different colour families in the same component
Never hardcode hex values. Always use CSS custom properties
Standards
Accessibility
All SOG digital products must meet WCAG 2.1 Level AA. This section covers focus management, contrast compliance, and key ARIA patterns. Use Purpose Black for all body text; brand colours are for accents only.
Focus Ring Specification
Focus rings must be visible and consistent. The SOG focus ring is 2px solid #625D9C with 2px offset. Tab through the demo below.
/* SOG Focus Ring - apply to all interactive elements */
:focus-visible {
outline: 2px solid #625D9C;
outline-offset: 2px;
}
/* Remove default outline only when :focus-visible is supported */
:focus:not(:focus-visible) {
outline: none;
}
Colour Contrast Audit
Colour
Hex
On White
Ratio
WCAG AA Small
WCAG AAA
AA-Compliant Shade
Purpose Black
#323E48
Sample text
10.95:1
✓ Pass
✓ Pass
-
Professional Purple
#625D9C
Sample text
5.89:1
✓ Pass
✗ Fail
#403C6510.23:1 ✓
Dynamic Teal
#0095A9
Sample text
3.58:1
⚠ Large text only
✗ Fail
#00616E7.15:1 ✓
Independence Green
#00AF9A
Sample text
2.76:1
✗ Fail
✗ Fail
#0072645.85:1 ✓
Belonging Blue
#4197CB
Sample text
3.22:1
⚠ Large text only
✗ Fail
#2A62846.61:1 ✓
Collaborative Sky
#5EB3E4
Sample text
2.32:1
✗ Fail
✗ Fail
#2A51678.50:1 ✓
Warning Red
#F32735
Sample text
4.08:1
⚠ Borderline
✗ Fail
#CE212D5.41:1 ✓
Caution Orange
#FF6C0E
Sample text
2.84:1
✗ Fail
✗ Fail
#A646096.00:1 ✓
Alert Yellow
#F2CD00
Sample text
1.55:1
✗ Fail
✗ Fail
#6D5C006.60:1 ✓
warning
Key Guidance
Always use Purpose Black (#323E48) for all body text. Brand colours should only appear as accents, badges, borders, and icons; never as small body text on white backgrounds. Alert Yellow (#F2CD00) must only be used on dark backgrounds.
Tints, Shades & Tones - Contrast on White
Contrast ratio of each scale step as text on white, with WCAG AA (4.5:1) pass/fail. Steps run lightest (1) to darkest (5) - Tints: 10→100% · Shades & Tones: Base → +70%.
Purple
1
2
3
4
5
Tint
1.14✗
1.32✗
1.81✗
3.13✗
5.89✓
Shade
5.89✓
7.43✓
10.23✓
13.75✓
17.60✓
Tone
5.89✓
5.32✓
4.81✓
4.37✗
4.09✗
Blue
1
2
3
4
5
Tint
1.10✗
1.23✗
1.54✗
2.18✗
3.22✗
Shade
3.22✗
4.33✗
6.61✓
10.34✓
15.59✓
Tone
3.22✗
3.45✗
3.61✗
3.80✗
3.91✗
Sky
1
2
3
4
5
Tint
1.08✗
1.17✗
1.37✗
1.77✗
2.32✗
Shade
2.32✗
3.18✗
5.09✓
8.50✓
14.25✓
Tone
2.32✗
2.66✗
3.03✗
3.46✗
3.75✗
Teal
1
2
3
4
5
Tint
1.12✗
1.27✗
1.63✗
2.41✗
3.58✗
Shade
3.58✗
4.73✓
7.15✓
11.01✓
16.18✓
Tone
3.58✗
3.78✗
3.93✗
4.02✗
4.00✗
Green
1
2
3
4
5
Tint
1.11✗
1.23✗
1.51✗
2.06✗
2.76✗
Shade
2.76✗
3.73✗
5.85✓
9.53✓
15.11✓
Tone
2.76✗
3.12✗
3.45✗
3.75✗
3.88✗
Charcoal
1
2
3
4
5
Tint
1.18✗
1.43✗
2.14✗
4.49✗
10.95✓
Shade
10.95✓
12.50✓
14.90✓
17.17✓
19.09✓
Tone
10.95✓
8.36✓
6.47✓
4.99✓
4.36✗
Red
1
2
3
4
5
Tint
1.17✗
1.35✗
1.85✗
2.93✗
4.08✗
Shade
4.08✗
5.37✓
8.03✓
12.00✓
16.83✓
Tone
4.08✗
4.56✓
4.66✓
4.44✗
4.18✗
Orange
1
2
3
4
5
Tint
1.11✗
1.23✗
1.54✗
2.13✗
2.84✗
Shade
2.84✗
3.83✗
6.00✓
9.67✓
15.26✓
Tone
2.84✗
3.24✗
3.55✗
3.83✗
3.91✗
Yellow
1
2
3
4
5
Tint
1.05✗
1.10✗
1.21✗
1.38✗
1.55✗
Shade
1.55✗
2.17✗
3.63✗
6.60✓
12.56✓
Tone
1.55✗
1.95✗
2.42✗
3.14✗
3.58✗
ARIA Patterns
HTML
<!-- Live region for dynamic alerts -->
<div role="alert" aria-live="assertive">
Your changes have been saved.
</div>
<!-- Icon-only button -->
<button aria-label="Close panel" class="btn btn-icon">
<svg ...></svg>
</button>
<!-- Active nav item -->
<a href="/dashboard" aria-current="page" class="nav-item active">
Dashboard
</a>
<!-- Required field -->
<input
type="text"
aria-required="true"
aria-describedby="name-error"
class="sg-input error"
>
<div id="name-error" class="sg-error-msg" role="alert">
This field is required.
</div>
<!-- Expanded disclosure -->
<button aria-expanded="false" aria-controls="panel-1">
Show details
</button>
<div id="panel-1" hidden>...</div>
Skip Navigation
A skip navigation link is included at the top of this page. It appears visually when focused via keyboard (Tab key). This allows keyboard and screen reader users to jump directly to the main content area.
HTML + CSS
<!-- Place immediately after <body> -->
<a href="#main-content" id="skip-link">Skip to main content</a>
<style>
#skip-link {
position: absolute;
top: -100%;
left: 16px;
background: var(--sog-purple);
color: white;
padding: 8px 16px;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
z-index: 999;
transition: top 0.15s;
}
#skip-link:focus { top: 16px; }
</style>
Components
Buttons
Buttons trigger actions. Use the primary button for the single most important action on a page. Limit destructive buttons to genuinely irreversible actions.
Variants
When to use each variant
Choose the variant that matches the action's importance and context. Only one Primary button should appear per view it is the single most important action. Never use Danger for anything other than destructive irreversible actions.
Primary
The single most important action on a view. Save, Submit, Confirm. One per view maximum.
Secondary
Supporting actions that complement Primary. Export, Download, Add Another.
Outline / Ghost
Low-emphasis actions. Cancel, Back, View Details. Never for destructive actions.
Every button has four states: default, hover (background lightens), active/pressed (scales to 0.97 - physical press feedback confirming the action registered at 160ms ease-out), and disabled (40% opacity, not-allowed cursor). The scale feels like a physical response, not an animation - it should be instant enough that users never consciously notice it, only feel it.
Loading State
Buttons with Icons
Icons can be placed before or after the label. Icon-only buttons (no visible text label) must always have an aria-label attribute describing the action without it they are completely inaccessible to screen readers. Never rely on a tooltip alone as a substitute for an aria-label.
Form inputs use a consistent 38px height, 8px border radius, and a 3px purple focus ring. Error states use Warning Red with matching focus ring.
Text Input States
Please enter a valid email address
This field cannot be edited
Text Area
Maximum 2,000 characters
Custom Dropdown
Use the custom dropdown instead of a native <select> element anywhere in the product. Native selects render using the operating system's default styling on macOS dark mode this produces a dark popup that is off-brand and uncontrollable with CSS. The custom dropdown is fully styled, keyboard accessible, and works in both light and dark contexts. Three interaction states: default, open (purple border + focus ring), and selected item (purple-10 background + SOG purple bold text).
function toggleDropdown(id) {
const dd = document.getElementById(id);
if (!dd) return;
const isOpen = dd.classList.contains('open');
// Close all open dropdowns first
document.querySelectorAll('.sg-dropdown.open').forEach(d => {
d.classList.remove('open');
d.querySelector('.sg-dropdown-trigger')
.setAttribute('aria-expanded', 'false');
});
if (!isOpen) {
dd.classList.add('open');
dd.querySelector('.sg-dropdown-trigger')
.setAttribute('aria-expanded', 'true');
}
}
function selectOption(id, item, label) {
const dd = document.getElementById(id);
if (!dd) return;
// Clear all selected states
dd.querySelectorAll('.sg-dropdown-item').forEach(i => {
i.classList.remove('sg-dropdown-item--selected');
i.setAttribute('aria-selected', 'false');
});
// Set selected
item.classList.add('sg-dropdown-item--selected');
item.setAttribute('aria-selected', 'true');
// Update trigger label and colour
const lbl = dd.querySelector('.sg-dropdown-label');
lbl.textContent = label;
lbl.style.color = ''; // reset placeholder colour
// Close
dd.classList.remove('open');
dd.querySelector('.sg-dropdown-trigger')
.setAttribute('aria-expanded', 'false');
}
// Close on outside click
document.addEventListener('click', e => {
if (!e.target.closest('.sg-dropdown')) {
document.querySelectorAll('.sg-dropdown.open').forEach(d => {
d.classList.remove('open');
d.querySelector('.sg-dropdown-trigger')
.setAttribute('aria-expanded', 'false');
});
}
});
Checkbox & Radio
Multi-step Form Indicator
check
Basic Info
2
Programme Details
3
Review & Submit
Step 2: Programme Details
Sarah Mitchell
James Thornton
Aisha Patel
Components
Cards
Cards group related content and actions. All cards use a 12px border radius and shadow-only treatment - no flat borders. KPI Cards are the exception: they use a 2px gradient border that fades from the full brand colour at the top to a 40% tint at the bottom.
KPI Cards
Use a gradient border that fades from the full brand colour at the top to a lighter 40% tint at the bottom - achieved via padding-box / border-box background layering on a 2px transparent border. Each card lifts on hover with a colour-tinted shadow. Pair a trend indicator with every metric to show direction of change.
Risk Score
22↓
0
Eliminated
Lower is better · Max 25 · Full risk reduction achieved at exit
Safety Score (YP)
from 38↑
108
+184%
Higher is better · Max ~120 · YP self-reported safety and stability
Emotional Regulation
from 11↑
33
+200%
Tripled across programme · Feeds directly into EET Readiness index
The base card is the default container for bounded content participant records, widget summaries, and metric displays. Use .sg-card with the content classes below. Never use inline styles for typography reference the documented classes to stay consistent with the token system.
Every slot in the card hierarchy has a fixed spacing value. Use these tokens consistently - never override with arbitrary pixel values.
Element
Property
Token
Value
Card container
padding (all sides)
space-6
24px
Eyebrow → Title
margin-bottom
space-1 + 2px
6px
Title → below
margin-bottom
space-1
4px
Meta → next section
margin-bottom
space-4
16px
Stat value → below
margin-bottom
space-2
8px
Icon + text (side by side)
gap
space-3
12px
Badge row
gap
space-2
8px
Card grid (card to card)
gap
space-6
24px
Section divider within card
gap / margin-top
space-4
16px
Gradient Card
Use for milestone moments, celebration banners, and hero call-outs. The static gradient follows the SOG signature direction; the animated variant cycles continuously and is best reserved for standout UI moments avoid using both on the same page.
Static
Streets of Growth
Programme Milestone Reached
Jordan Smith has successfully completed Phase 2 of the Streets of Growth programme. Outstanding work from both participant and key worker.
Animated
Live
Streets of Growth
Annual Impact Report 2024
Download the full SOG Impact Report to explore outcomes, stories, and data from our work with young people across east London this year.
Use empty states when a list, table, or data view has no content to show. Always include an icon, a clear heading, a one-line explanation, and a single primary action. Never leave a blank container without guidance.
person_search
No participants found
No results matched your filters. Clear them to see all participants, or add a new one.
Use for high-level programme summary figures - a single icon, a large numeric value, and a short supporting note. Works in a 3-column grid as the opening row of a report section. The icon colour and value colour should match the metric's brand colour. No border, no hover state.
schedule
Programme Duration
24 months
From initial assessment to programme exit
crisis_alert
Crisis Recovery Time
3 months
M13 event resolved by M16 - vs. 6+ month projection
Used to explain a composite index - full name, abbreviation badge, a one-line description, and a weighted breakdown visualised as progress bars. The badge colour matches the index's brand colour. Use in a 3-column grid, one card per index.
A three-column card for programme phase documentation. The left header column uses the full brand colour with white text; the centre body column holds the phase description; the right footer column uses the 10% tint and lists tools and activities. Add a colour class (.phase-1 through .phase-5) to the outer container. Phase 3 (Relapse/Crisis) uses Red - include a warning icon inside .phase-num to signal severity.
Phase 1
Initiating
Months 1–3
Initial assessment, trust-building and safety planning. Establishing baseline indicators and identifying primary risk factors.
Phase 2
Engaging
Months 4–12
Active intervention and skills development. Consistent upward trajectory across all indices as participant engages with structured support.
warningRelapse
Crisis & Relapse
Months 13–15
External stressors triggered a temporary regression. Intensive support deployed. Risk score spiked then was swiftly managed through rapid response protocols.
Phase 3
Advancing
Months 16–24
Sustained recovery and consolidation following crisis. Strong upward momentum with participant demonstrating resilience and autonomous coping strategies.
Post Programme
Sustained Outcomes
Months 25–37
Risk fully eliminated. All indices reaching ceiling. Participant transitioning to independence with ongoing light-touch support and community integration.
HTML
<!-- Add .phase-1 through .phase-5 for colour variants -->
<div class="phase-card phase-1">
<div class="phase-header">
<div class="phase-num">Phase 1</div>
<div class="phase-title">Initiating</div>
<div class="phase-months">Months 1–3</div>
</div>
<div class="phase-body">
<div class="phase-focus">Description of phase activities and goals.</div>
</div>
<div class="phase-footer">
<div class="phase-tools-label">Tools & Activities</div>
<div class="phase-tools">
<span class="phase-tool">Tool name</span>
<span class="phase-tool">Tool name</span>
</div>
</div>
</div>
The milestone timeline documents key events in a participant journey. A vertical spine (2px --charcoal-20) runs through coloured icon circles; each node connects to a milestone card. Use icon circle colour to signal the programme phase; use the phase badge (top-right of card) to label it. Score badges at the card bottom show raw assessment values at that moment.
Milestone Rows
Each row pairs a spine icon circle with a card. The card header shows a MONTH eyebrow on the left and a phase badge on the right. Card body holds the milestone title and description. Score badges sit in a flex-wrap row at the bottom - colour-coded by metric.
assignment_ind
Month 1
Phase 1
Baseline Assessment
Young person presenting at high risk with no formal support in place. Initial surveys completed; risk score 22, safety score 38.
Risk 22Safety 38ER 11CJI 31.4
trending_up
Month 9
Phase 2
Mid-Programme Review
Sustained engagement across all sessions. All composite indices tracking above target. Emotional regulation tripled since baseline.
Badges surface short categorical metadata: status, phase, brand value. Always use the SOG tint token system tinted background, matching border, and colour text. Never use badges for body text or numeric scores (use a risk chip instead). Badges are inline elements; always keep label text under 15 characters.
Brand Value Badges
One badge per SOG brand value. Each badge colour maps to the value's associated brand colour. Use in participant records, reports, and dashboards to tag which value a session or outcome relates to.
Status badges communicate a participant's current programme state. Green = active and progressing. Orange = at risk, intervention may be needed. Red = high risk, immediate review required. Yellow = on hold. Charcoal = closed. Always pair a status badge with a risk chip when displaying in a table they communicate different things: the badge is the state label, the chip is the numeric score.
✓ Active⚠ At Risk✕ High Risk◐ On HoldClosed
Phase Badges
Phase badges indicate which programme phase a participant is currently in. The colour mapping follows the SOG phase colour system: Phase 1 = Purple (Professional), Phase 2 = Blue (Belonging), Phase 3 = Teal (Dynamic), Follow-up = Green (Independence), Relapse = Red. Never use a phase badge to indicate a numeric risk level - use a risk chip or status badge instead.
Phase 1Phase 2Phase 3Follow-upRelapse
Filled Variants
Filled badges use a solid colour background with white text. Use sparingly for high-emphasis tags that need more visual weight than the tinted variant for example, in a dark-background panel or where the tint colour would not have sufficient contrast. Never mix filled and tinted badges in the same table column.
ProfessionalDynamicBelongingIndependencePurpose
When to use
Use a badge for short categorical labels (phase, status, value, type). Use a risk chip for numeric scores on the SOG risk scale. Use a pill or tag for user-applied labels that can be added and removed. Never use a badge where a plain text label would suffice badges draw attention and should only be used where the category is meaningful at a glance.
Alert banners communicate important feedback, confirmations, warnings, and errors. Use sparingly over-alerting reduces attention to critical notices. This section covers general UI alerts. For clinical participant-level risk flags, see Alerts & Flags below.
Variants
Five variants mapped to semantic intent. Default and Info are passive they do not interrupt the user. Success confirms a completed action. Warning requires attention but is not blocking. Danger indicates a failure that requires action before proceeding. Always choose the variant that matches the actual severity never use Warning for an error, or Danger for a passive notice.
info
Default / Informational
This session has been scheduled for review by the programme lead on 15 March.
chat
Information
A new version of the risk assessment tool is available. Existing assessments are not affected.
check_circle
Success
Phase 2 milestones have been saved and the participant record has been updated.
warning
Warning
This participant's risk score has increased by 18 points since the last assessment. A review may be required.
dangerous
Danger / Error
Unable to save changes. Please check your connection and try again. If the problem persists, contact support.
Alert without icon
Icons are optional: omit when the alert type is clear from context or space is limited.
ARIA: role="alert" vs role="status"
Use role="alert" on Warning and Danger variants screen readers will interrupt whatever the user is doing and announce the alert immediately. Use role="status" on Default, Info, and Success variants screen readers will wait for a natural pause before announcing. Never use role="alert" for passive notices, as it creates unnecessary interruptions.
When to use
Use alert banners for system-level feedback that affects the current task: save confirmations, validation errors, status changes. Do not use alerts for persistent guidance or help text use an info callout instead. Do not stack more than two alerts on a single view.
A card-based alert grid for clinical dashboards. Use for participant-level risk flags within clinical records - not for general UI feedback. For save confirmations, validation errors, and status changes, use the Alerts component above. Built from three composable components: Alert Card, Alert Icon, and Alert Grid Layout. Always driven by data cards are only rendered when conditions are flagged, never as placeholders.
Alert Icon
Two severity tiers, visually distinct at a glance. Critical filled red circle with SVG × mark. Used for overdue, missed, or breached conditions requiring immediate action. Warning outlined orange circle with SVG triangle. Used for approaching thresholds or conditions requiring attention. The filled/outlined distinction encodes severity before colour ensuring it works for colour-blind users.
A tint-background card surfacing a single flagged condition. Two semantic variants: Critical (red-10 background, red border) and Warning (orange-10 background, orange border). The card lifts on hover with a colour-tinted shadow matching its severity this gives visual feedback without requiring a button. Cards are not interactive by default; if a card should navigate to a detail view, wrap the title in an anchor.
A 4-column responsive grid for alert cards. Handles variable alert counts cleanly critical cards always precede warning cards within the grid, established at render time by sorting the alerts array by severity before injecting cards. The grid collapses to 2 columns below 640px and 1 column below 400px. Never leave orphan single cards in the last row if the count is odd, consider whether the last alert genuinely needs a card or could be promoted to an inline banner.
Alerts & Flags
6 Active
Last Intervention
61 days since last Last: 22 Jan 2026
Safety Survey Overdue
Last: 29 Jan 2026 Due by: 08 Feb 2026
Self Efficacy Overdue
Last: 25 Feb 2026 Due by: 07 Mar 2026
Intervention Gaps
2 gaps > 30 days Longest: 128 days
Safety Score Gap
YP: 96 vs Coach: 75 28% gap (threshold 10%)
Next Self Efficacy Due
Due soon Next: 27 Mar 2026
HTML
<div class="alert-card-grid">
<!-- Critical cards first, warning cards after -->
<div class="alert-card alert-card--critical"
role="alertdialog" aria-labelledby="a1-title">
<div class="alert-card-icon alert-icon--critical" aria-label="Critical">
<svg ...></svg>
</div>
<div id="a1-title" class="alert-card-title">Safety Survey Overdue</div>
<div class="alert-card-detail">
Due by: <strong>08 Feb 2026</strong>
</div>
</div>
<!-- ... repeat for each alert -->
</div>
Consistent navigation patterns help users orient themselves within the SOG application. Use the sidebar for primary navigation between views, tab pills for switching between sub-views on the same page, breadcrumbs for deep hierarchies, and pagination for large datasets.
App Sidebar
The primary navigation container. Each nav item has a Material Symbols Rounded icon at 20px and a text label. The active item uses the active class which applies the purple-10 background and SOG purple text. Always add aria-current="page" to the active item so screen readers announce the current location.
Use for switching between sub-views on the same page. Wrap in role="tablist" with role="tab" and aria-selected on each pill. The associated content panel uses role="tabpanel".
Overview
Sessions
Assessments
Documents
Showing: Overview
Breadcrumb
Shows the user's location in the app hierarchy. Always wrap in a <nav> with aria-label="Breadcrumb". Add aria-current="page" to the last item. Previous items are links; the current item is a <span>, not a link. Add aria-hidden="true" to separator characters so screen readers skip them.
Pagination
Use for large datasets. Always disable the previous button on page 1 and the next button on the last page using both the disabled HTML attribute and the disabled CSS class. Add aria-current="page" to the active page button. All page buttons need descriptive aria-label values.
Navigation Spacing Anatomy
Spacing values for every navigation component. Apply these consistently - mixing different padding or gap values across navigation contexts breaks visual rhythm.
Component
Element
Property
Token
Value
Sidebar nav item
Item
padding
7px 10px
7px top/bottom, 10px left/right
Icon + label
gap
space-2
8px
Tab bar
Bar container
padding
space-1
4px
Tab pills
gap between pills
space-0 + 2px
2px
Tab pill
Pill itself
padding
6px 16px
6px top/bottom, 16px left/right
Breadcrumb
Items
gap
space-1 + 2px
6px
Pagination
Buttons
gap between buttons
space-1
4px
Button
width & height
32 × 32px
fixed size, not token-derived
Components
Data Tables
Tables are the primary vehicle for clinical and programme data across SOG products. The table system is built from composable sub-components panel header, column headers, cell types, empty states, footer legend each documented below so they can be combined consistently across any table in the product.
Panel Header
Every data table sits inside a panel. The panel header provides context: an icon container, a title, and optional right-side metadata. Two variants default (purple-tinted icon for data panels) and warning (orange-tinted icon when the panel's content requires urgent attention). Always place the toolbar row below the panel header as a separate row never place filter controls or search inputs inside the panel header itself.
The base table pattern for participant lists, session logs, and any structured dataset. Always pair with the panel header. Always add scope="col" to every <th> screen readers use this to associate header cells with data cells. Wrap the table in an overflow container so it scrolls horizontally on narrow viewports without breaking the layout.
A data-encoding chip not a badge. Risk chips communicate a numeric score's severity using colour, not just a label. Four states: three semantic threshold states plus an unscored state for participants who have never been assessed. Never use a plain number or coloured text for risk scores always use this chip so severity is immediately readable without processing the number. Rule: use a risk chip for numeric or unscored values on the SOG risk scale only. Use a badge for categorical labels (phase, status, type) that do not imply a numeric score. Never substitute one for the other.
Low ≤ 15
3815
Medium 16–25
182225
High 26+
295891
Unscored
–
Never assessed. Distinct from a score of 0 (genuinely low risk).
HTML
<span class="risk-chip risk-low">3</span> <!-- ≤ 15 -->
<span class="risk-chip risk-med">22</span> <!-- 16–25 -->
<span class="risk-chip risk-hi">58</span> <!-- 26+ -->
<span class="risk-chip risk-unscored">–</span> <!-- never assessed -->
<!-- Small variant for use in table footer legends -->
<span class="risk-chip risk-low risk-chip-sm">3</span>
Two distinct empty cell states carry different clinical meanings and must never be conflated. Pending means data exists but has not yet been collected for this record a dot indicator prompts the user to complete it. N/A means the field is not applicable to this row an en-dash signals no action required. Using the wrong state gives the wrong clinical signal.
Pending: not yet collected
PendingPendingPending
Use when the data should be collected but hasn't been yet. Prompts action.
N/A: not applicable
–––
Use when the field genuinely does not apply to this record type. No action needed.
HTML
<!-- Pending: data not yet collected -->
<span class="cell-pending" aria-label="Data pending">
<span class="cell-pending-dot"></span>Pending
</span>
<!-- N/A: not applicable to this record -->
<span class="cell-na" aria-label="Not applicable">–</span>
Both states can appear in the same row. Pending means the data exists but has not been collected yet. N/A means the data point is not applicable for this participant. Never use one where the other is correct.
Participant
Self-Efficacy Score
Safety Score
Employment Score
Jordan Smith
Phase 2: session data exists
82
Pending
–
Pending
The session happened and this score should be here, but has not been entered yet. The dot signals "come back to this."
N/A
Employment score is not applicable for a Phase 2 participant. There is nothing to collect; the field is structurally irrelevant for this row.
Filter & Toolbar
The toolbar row sits between the panel header and the table. It holds search inputs, filter dropdowns, and active filter chips. It is always a separate row from the panel header never place filters inside the panel header flex row, as they compete for space at narrow widths. When filters are active, show a chip for each applied filter with a remove button. Always provide a "Clear all" action when more than one filter is active.
Participants
142 participants
All phases
Phase 1
Phase 2
Phase 3
All statuses
Active
At risk
Closed
Filters:
Status: At risk
Showing 18 of 142
Participant
Phase
Status
Risk Score
Marcus Obi
Phase 3
At Risk
82
Dani Torres
Phase 1
At Risk
91
HTML
<!-- Toolbar: always a separate row below the panel header -->
<div class="table-toolbar">
<div class="search-wrap">
<span class="search-icon">...</span>
<input type="search" class="sg-input search-input"
placeholder="Search participants…"
aria-label="Search participants">
</div>
<!-- Custom dropdown - use instead of native <select> -->
<div class="sg-dropdown" id="dd-phase">
<button class="sg-dropdown-trigger"
onclick="toggleDropdown('dd-phase')"
aria-haspopup="listbox" aria-expanded="false">
<span class="sg-dropdown-label">All phases</span>
<svg ...chevron.../>
</button>
<ul class="sg-dropdown-menu" role="listbox">
<li class="sg-dropdown-item sg-dropdown-item--selected"
role="option" aria-selected="true"
onclick="selectOption('dd-phase', this, 'All phases')">
All phases
</li>
<li class="sg-dropdown-item" role="option" aria-selected="false"
onclick="selectOption('dd-phase', this, 'Phase 1')">
Phase 1
</li>
</ul>
</div>
</div>
<!-- Active filter chips - only shown when filters are applied -->
<div class="table-filter-chips">
<span class="filter-chip">
Status: At risk
<button aria-label="Remove status filter">✕</button>
</span>
<button class="btn btn-ghost">Clear all</button>
<span class="filter-count">Showing 18 of 142</span>
</div>
// Filter state - one key per filter dimension
const filters = { phase: null, status: 'at-risk' };
let allRows = []; // store on init
function initFilters(tableId) {
const tbody = document.querySelector(`#${tableId} tbody`);
allRows = Array.from(tbody.querySelectorAll('tr'));
}
function applyFilter(dimension, value) {
filters[dimension] = value || null;
renderFilterChips();
filterRows();
}
function removeFilter(dimension) {
filters[dimension] = null;
renderFilterChips();
filterRows();
}
function clearAllFilters() {
Object.keys(filters).forEach(k => filters[k] = null);
renderFilterChips();
filterRows();
}
function filterRows() {
const visible = allRows.filter(row => {
return Object.entries(filters).every(([dim, val]) => {
if (!val) return true;
// Match against data-* attributes on the row
return row.dataset[dim] === val;
});
});
allRows.forEach(r => r.style.display = 'none');
visible.forEach(r => r.style.display = '');
// Update result count
document.querySelector('.filter-count').textContent =
`Showing ${visible.length} of ${allRows.length}`;
}
function renderFilterChips() {
const container = document.querySelector('.table-filter-chips');
const active = Object.entries(filters).filter(([,v]) => v);
container.style.display = active.length ? 'flex' : 'none';
// Rebuild chips for each active filter
// ...
}
Sortable Column Headers
Sortable columns use a sort icon in the column header to communicate the current sort state. Three states: unsorted (neutral icon, both arrows visible), ascending (up arrow active), descending (down arrow active). Only one column can be sorted at a time this pattern supports single-column sort only. Multi-column sort (shift+click) is out of scope; if required, use a sort configuration panel rather than column headers activating a new column resets the previous. Clicking an already-sorted column toggles between ascending and descending. Always sort the data immediately on click never require a separate "Apply" action. Include aria-sort on the active column header for screen readers.
let sortCol = null, sortDir = 'asc';
function sortTable(col, tbody, getValue) {
// Toggle direction if same column, else reset to asc
if (sortCol === col) {
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
} else {
sortCol = col; sortDir = 'asc';
}
// Update header aria-sort and visual state
document.querySelectorAll('.sg-th-sort').forEach(th => {
th.removeAttribute('aria-sort');
th.classList.remove('sg-th-sort--asc', 'sg-th-sort--desc');
});
const activeHeader = document.querySelector(
`.sg-th-sort[data-col="${col}"]`
);
if (activeHeader) {
activeHeader.setAttribute('aria-sort', sortDir + 'ending');
activeHeader.classList.add('sg-th-sort--' + sortDir);
}
// Sort rows
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a, b) => {
const av = getValue(a, col);
const bv = getValue(b, col);
return sortDir === 'asc' ? av - bv : bv - av;
});
rows.forEach(r => tbody.appendChild(r));
}
Resizable Columns
Column resize handles sit on the right edge of each <th>. Dragging the handle updates the column width live with the left boundary fixed only the right edge moves. Each column has a minimum width of 60px. Column widths are persisted to localStorage and restored on next page load. The table must use table-layout: fixed with explicit column widths set via <colgroup>. Touch devices: resize handles use mouse events only and are not available on touch screens on mobile, consider omitting handles or replacing them with a column visibility panel.
view_column
Participants
Drag column edges to resize
Participant
Phase
Status
Last session
Risk
Amara Kofi
Phase 1
Active
11 Mar 2024
3
Jordan Smith
Phase 2
At risk
12 Mar 2024
58
Marcus Obi
Phase 3
At risk
8 Mar 2024
82
HTML
<!-- table-layout:fixed is required for resize to work -->
<table style="table-layout:fixed;width:100%">
<colgroup>
<col id="col-0" style="width:180px">
<col id="col-1" style="width:120px">
<col id="col-2" style="width:120px">
</colgroup>
<thead>
<tr>
<th style="position:relative;user-select:none">
Participant
<!-- Resize handle - one per th, right-aligned -->
<span class="col-resize-handle" data-col="0"></span>
</th>
<th style="position:relative;user-select:none">
Phase
<span class="col-resize-handle" data-col="1"></span>
</th>
</tr>
</thead>
<tbody>...</tbody>
</table>
CSS
/* Resize handle - sits on the right edge of each th */
.col-resize-handle {
position: absolute; right: 0; top: 0;
width: 6px; height: 100%;
cursor: col-resize;
user-select: none;
background: transparent;
transition: background .15s ease-out;
}
.col-resize-handle:hover,
.col-resize-handle.resizing {
background: var(--sog-purple);
opacity: .25;
}
/* Prevent text selection during drag */
body.col-resizing {
cursor: col-resize;
user-select: none;
}
The table footer strip serves two purposes: left side holds pagination or row counts; right side holds an inline legend when the table uses any data-encoding component (risk chips, coloured values, or status icons) that requires a key. The legend explains the colour system without interrupting the table header.able data. The two zones use display:flex with justify-content:space-between to push them to opposite ends. Never place action buttons in the footer those belong in the panel header or toolbar.
Use table-layout:fixed with a <colgroup> whenever the table has both narrow data columns (date, duration, risk) and a wide content column (tools, notes, description). Without it, a row with long tool names will push the date column wider, destroying the layout. Column widths are derived from content minimum + 2× cell padding. The tools column takes remaining width with no width set. Important: if the table contains dropdowns or tooltips, set their z-index higher than the fixed column (z-index: 10 or above) so they render over the sticky cells rather than behind them.
Full Interaction History
8 records
Date
Type
Duration
Risk
Tools / Session Type
Safety YP
Safety Coach
Self Efficacy
14 May 2025
Intervention
75 m
29
CBS Techniques (CBT), MI Techniques
Pending
Pending
Pending
17 May 2025
Intervention
15 m
24
MI Techniques
Pending
Pending
Pending
20 May 2025
Intervention
5 m
29
–
Pending
Pending
Pending
03 Jun 2025
Intervention
120 m
24
CBS Techniques (CBT), MI Techniques, Solution-Focused Brief Therapy
Two distinct empty state variants never use the wrong one. Filtered empty: the table has data but no rows match the current search or filter. Always show a "Clear filters" action. Genuinely empty: no records exist yet for this participant or view. Show an "Add" or "Record" action instead never suggest filtering when there's nothing to filter.
Filtered empty
Participant
Phase
Status
No results found
No records match your current filters
Genuinely empty
Date
Type
Duration
No sessions recorded
This participant has no interaction history yet
Clickable Row Patterns
Use a chevron column to indicate every row is clickable. The affordance is permanent and visible at a glance - no hover or discovery required, including on touch devices. Clicking a row opens a sheet panel with the full record, keeping the table visible in the background for context.
Participant
Phase
Status
Risk Score
Jordan Davies
Phase 2
Active
41
chevron_right
Marcus Reid
Phase 1
At Risk
67
chevron_right
Priya Sharma
Phase 3
Active
22
chevron_right
Table Spacing Anatomy
Each zone of the table composite has a fixed padding. Never adjust these - the visual rhythm relies on all zones using the same horizontal gutter (20px) so content aligns column-to-column across the panel header, toolbar, and data rows.
Zone
Property
Value
Notes
Panel header
padding
14px 20px
14px top/bottom keeps it compact against the table header row
Toolbar
padding
10px 16px
Slightly narrower than panel header - toolbar is secondary
Toolbar actions
gap
8px (space-2)
Between buttons, dropdowns, and search field
Column header cells (th)
padding
10px 14px
Reduced vertical padding keeps the header visually distinct from data rows
Data cells (td)
padding
11px 14px
1px taller than header to give data breathing room
Table wrapper
border-radius
12px (radius-lg)
Applied to the outer container div, not the table element itself
Table in a page
gap to adjacent elements
24px (space-6)
Between the table panel and KPI cards, filters, or other sections
Footer strip
padding
12px 20px
Matches the panel header horizontal gutter for alignment
Components
Progress & Loading
Use progress indicators to communicate system state. Never block the UI without a loading indicator; always show users that work is in progress.
Progress Bars
Use for tasks with a known completion percentage. The fill animates at 250ms ease-out - fast enough to confirm the value has updated, slow enough to be perceptible. Never use durations above 300ms on progress fills; a bar that arrives late feels like it is lagging behind reality. Always show the percentage as a text label alongside the bar so users who cannot perceive colour still get the value. Add role="progressbar", aria-valuenow, aria-valuemin="0", and aria-valuemax="100" to the track element.
Phase 1 Milestones25%
Phase 2 Milestones60%
Phase 3 Milestones90%
Skeleton Loader
Skeleton loaders show placeholder shapes while content loads, reducing perceived wait time by revealing structure before data arrives. See the dedicated Skeleton section for all patterns - list item, KPI card, data table, and participant detail panel - plus implementation guidelines and accessibility requirements.
Spinners & Loading Buttons
Use a spinner for indeterminate waits where the completion time is unknown. Spin speed matters for perceived performance: a faster spinner (600ms per revolution) makes loading feel faster even when actual load time is identical - the same psychological principle that makes a fast-spinning indicator feel more active. Always disable the button to prevent duplicate submissions. Add aria-label="Loading" to the spinner element so screen readers announce the state.
When to use each type
Use a progress bar when you know the percentage complete (file upload, form step, milestone tracking). Use a skeleton loader when content has a defined shape that will fill a specific layout. Use a spinner for indeterminate waits where duration is unknown. Never show a blank screen without a loading indicator.
Also known as a stepper, progress tracker, or steps component. It represents a user's current position within a defined sequence of discrete stages, showing which step in a journey they have reached. This is distinct from a progress bar, which communicates how much of a task is complete. SOG's four programme phases are a natural application of this pattern.
Horizontal Stepper
Use for short sequences of 2–5 steps shown across the top of a form, wizard, or intake flow. The connector line between completed steps fills with brand purple. The active step gains a focus ring. Always add aria-current="step" to the active item and wrap the list in role="list".
check
Referral
2
Assessment
3
Phase Plan
4
Enrolment
Horizontal with Descriptions
Add a sublabel beneath the step name for context such as a date, duration, or status note. Keep sublabels to a single short line. The horizontal layout does not support wrapping text. Completed sublabels render in green; active sublabels in purple.
check
Initial Meeting
12 Mar 2025
2
Risk Assessment
In progress
3
Case Review
w/c 19 Apr
Vertical Stepper
Use when steps carry meaningful descriptions, or when showing a participant's history down a sidebar or panel. The vertical layout accommodates multi-line content and degrades gracefully on narrow viewports. The connector line extends from each node downward and fills purple on completed steps.
check
Initial Referral
Referred by secondary school safeguarding lead. Risk assessment completed, initial score 74. Paperwork and consent forms signed.
check_circleCompleted 14 Feb 2025
2
Phase 1: Belonging
Building trust and safety. 6 of 12 sessions completed. Risk score improving: 74 → 58. Key worker: Amara B.
pendingIn progress
3
Phase 1 Review
Milestone review scheduled once all 12 sessions are complete. Outcome determines readiness for Phase 2.
SOG Programme Phase Tracker
A phase-coloured variant of the vertical stepper mapped to SOG's four programme stages. Each node uses the phase brand colour (Phase 1 Purple, Phase 2 Blue, Phase 3 Teal, Follow-up Green) and the completed connector inherits that phase colour. Use in participant dashboards, case profiles, and programme reports.
4 of 10 sessions completed · Risk score 41 · In progress
pendingIn progress · Key worker: Amara B.
3
Phase 3
Independence
Pending Phase 2 completion and milestone review
star
Follow-up
Professional
6-month follow-up check-in after programme completion
Step States
Four states cover every scenario. Never create a fifth state by repurposing colour. Use these semantic states only.
check
Completed
Step finished successfully
2
Active
Current step in progress
3
Upcoming
Not yet reached
close
Error
Action needed, step blocked
When to use
Use a horizontal stepper for short linear flows (2–5 steps) at the top of a form or wizard. Use a vertical stepper when steps have descriptions or when displaying timeline-style history in a panel or sidebar. Use the SOG Phase Tracker variant for programme phase progression in participant records and dashboards. Never use a stepper for non-linear flows where users can skip steps freely. Use tabs instead. Never use a progress bar where the intent is "which stage" rather than "how much is complete."
Avatars represent users and entities throughout the application. Use initials when no photo is available. Status dots indicate availability or record status.
Initials Avatars
JD
SM: 32px
SM
MD: 40px
AK
LG: 64px
TW
Blue variant
MO
Teal variant
LR
Green variant
Avatar with Status Dot
JS
AK
MO
Marcus Obi
Active in programme
App Logo Mark
32px
48px
64px
Gradient
Components
Skeleton
Skeleton screens replace content with animated placeholder shapes while data loads. They reduce perceived wait time by showing structure before the data arrives. The user can see the layout before any content fills in. Use skeletons whenever content has a predictable shape; use a spinner when the shape is unknown or the wait is very short.
List Item
Use for participant lists, session logs, or any repeating row-based layout. Pair a circular avatar placeholder with text lines at varying widths. Never use uniform widths, which look artificial and draw attention to the skeleton.
KPI Card
Match the skeleton proportions exactly to the real card so the layout shift on load is imperceptible. The large number placeholder should match the font-size of the real metric.
Data Table
Render 4–6 placeholder rows. Do not skeleton the header row, as column labels are static and can load immediately. Use pill-shaped placeholders for badge columns to mirror the real badge shape.
Participant
Phase
Status
Risk
Last Session
Participant Detail Panel
For complex layouts, break the skeleton into named zones mirroring the real structure. Users can recognise the page structure before any data appears, which reduces anxiety during longer loads.
Implementation Rules
Match structure exactly. Every skeleton placeholder corresponds 1:1 with a real element - same size, same position. Layout shift on load is disorienting.
Add aria-busy="true" to the skeleton container and remove it when content loads. Pair with aria-label="Loading [content name]".
Vary widths. Never use identical widths on multiple text placeholders - a uniform skeleton looks artificial.
Use .skeleton-circle for avatar and icon placeholders - it overrides the default border-radius to 50%.
A sheet slides in from the right edge of the screen, overlaying the current view without replacing it. Use sheets for contextual detail or supplementary actions that do not require a full page navigation - the user can see and return to the underlying content. Common SOG uses: participant detail, session notes, filter drawers, and export options.
Interactive Demo
Open a live sheet. Dismiss by clicking the × button, pressing Escape, or clicking the backdrop.
Width Variants
Three widths cover most use cases. All sheets span full viewport height. On narrow viewports the sheet takes the full width minus a 48px left margin so the user can always see and tap the backdrop to close.
Narrow · 320px
Filters, quick actions
Standard · 480px ✦
Participant detail, notes
Wide · 640px
Forms, reports
Anatomy
Every sheet uses the same four-zone structure. Only the body scrolls. The header and footer remain fixed so the title and primary actions stay accessible regardless of content length.
Sheet Title
Subtitle / context
close
Header - fixed
Title, optional subtitle, close button. Always visible at the top.
Body - scrollable
All content, sections, and forms. Scrolls independently of header/footer.
Footer - fixed
Primary and secondary actions, right-aligned. Always within reach.
When to use
Use a sheet when the user needs contextual detail without losing their place. For example, viewing a participant's history while staying on the participant list. Use a modal instead when you need the user's full attention or a definitive decision before they can continue. Never use a sheet for destructive confirmations. Keep sheet content focused. If it requires more than three content sections, consider a dedicated page instead.
function openSheet(id) {
document.getElementById(id).classList.add('open');
document.body.style.overflow = 'hidden';
}
function closeSheet(id) {
document.getElementById(id).classList.remove('open');
document.body.style.overflow = '';
}
// Backdrop click closes sheet
document.querySelectorAll('.sheet-backdrop').forEach(el => {
el.addEventListener('click', e => {
if (e.target === el) closeSheet(el.id);
});
});
// Escape key closes all open sheets
document.addEventListener('keydown', e => {
if (e.key === 'Escape')
document.querySelectorAll('.sheet-backdrop.open')
.forEach(el => closeSheet(el.id));
});
Components
Modal
Modals interrupt the current flow to require attention or a decision before the user can continue. Use them sparingly. Every modal is an interruption. Reserve modals for confirmations, destructive actions, and focused forms where the user must respond before proceeding. For contextual detail that does not require a decision, use a Sheet instead.
Interactive Demos
Three common modal patterns in the SOG system. Each demo shows the entrance animation, backdrop blur, and dismiss interactions.
Size Variants
Three widths cover the full range of modal content. All modals are vertically centred and constrained to calc(100vh - 48px) so they never exceed the viewport.
Small · 360px
Confirmations, yes/no prompts
Single-question prompts, short alerts, simple confirmations with one paragraph of explanation.
Default · 480px ✦
Most modals, short forms
Confirmations with detail, 1–3 field forms, info dialogs, SOG session log, destructive actions with consequences listed.
Large · 600px
Multi-field forms, rich content
Complex forms with 4+ fields, document preview, data entry that requires more horizontal space than the default modal provides.
Anatomy
Every modal has four parts. The backdrop always blurs and dims. The container holds all content and animates in on a spring curve. The close button is mandatory. Never remove it.
Modal Title
close
Backdrop
Fixed overlay: rgba(0,0,0,.45) + backdrop-filter blur. Click to close.
Header
Title + close button. Always visible. Never omit the close button.
Body
Scrollable content. Max-height is capped so it never overflows the viewport.
Use a modal when the user must respond before continuing: confirmations, destructive actions, required input. Use a sheet for contextual detail that does not demand a decision. Use an alert or toast for non-blocking feedback. Never nest a modal inside another modal. Never open a modal as the result of another modal. Use a multi-step form or wizard instead. Always support Escape, the close button, and backdrop click as dismiss mechanisms.
function openModal(id) {
document.getElementById(id).classList.add('open');
document.body.style.overflow = 'hidden';
}
function closeModal(id) {
document.getElementById(id).classList.remove('open');
document.body.style.overflow = '';
}
// Backdrop click closes modal
document.querySelectorAll('.modal-backdrop').forEach(el => {
el.addEventListener('click', e => {
if (e.target === el) closeModal(el.id);
});
});
// Escape closes all open modals
document.addEventListener('keydown', e => {
if (e.key === 'Escape')
document.querySelectorAll('.modal-backdrop.open')
.forEach(el => closeModal(el.id));
});
Patterns
Flywheel
A composable pattern for communicating a cyclical process. Built from three parts: an interactive SVG ring diagram, a responsive step card grid, and an info callout summary. The ring nodes are interactive clicking a node highlights the corresponding step card. Full documentation is in the Flywheel component section. The sub-sections below show the assembled example only.
Ring Diagram
The SVG ring encodes the loop visually. Nodes are numbered and colour-coded by category. Arcs between nodes carry directional arrows. The ring is interactive each node is a focusable button that highlights the corresponding step card when clicked. The centre label names the subject of the loop. Always include the colour legend below the ring when using more than one colour.
// Node → card highlight connection
let activeStep = null;
function activateStep(idx) {
// Clear all active states
document.querySelectorAll('.ring-node')
.forEach(n => n.classList.remove('active'));
document.querySelectorAll('.fw-card')
.forEach(c => c.classList.remove('highlighted'));
// Toggle off if same node clicked twice
if (activeStep === idx) { activeStep = null; return; }
activeStep = idx;
// Highlight ring node
const node = document.querySelector(
`.ring-node[data-step="${idx}"]`
);
if (node) node.classList.add('active');
// Highlight and scroll to card
const card = document.getElementById(`fw-card-${idx}`);
if (card) {
card.classList.add('highlighted');
card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
// Keyboard support
document.querySelectorAll('.ring-node').forEach(node => {
node.addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
activateStep(parseInt(node.dataset.step));
}
});
});
Step Cards
Step cards display the detail for each ring node. They use the SOG tint token system purple-10 for YP/SoG-facing steps, teal-10 for external steps, green-10 for network/outcome steps. The grid is responsive: 3 columns at desktop, 2 at tablet (≤768px), 1 at mobile (≤480px). Each card carries a step kicker, an actor badge, a title, and body copy. Step 06 uses an "Outcome" badge to distinguish a result state from an action state.
Step 01YP
Young Person Posts
Yasmin shares an honest update about Phase 3.
Step 03Partners
Partners Notice
Employer partners see authentic voice. Yasmin becomes visible.
Step 05Network
SoG's Reach Grows
Authentic youth voices build credibility with funders and press.
HTML
<ol class="fw-grid" aria-label="Six steps of the flywheel">
<li class="fw-card fw-card-p" id="fw-card-0">
<div class="fw-card-head">
<span class="fw-kicker">Step 01</span>
<span class="badge badge-purple">YP</span>
</div>
<h3 class="fw-card-title">Young Person Posts</h3>
<p class="fw-card-body">...</p>
</li>
<!-- fw-card-p = purple, fw-card-t = teal, fw-card-g = green -->
</ol>
The info callout sits below the step card grid as a summary conclusion. It uses the documented info-callout component exactly no modifications. The panel header icon uses the autorenew Material Symbols Rounded icon in a purple-20 icon container. This treatment communicates the loop concept without needing extra explanation.
autorenew
Starting The Wheel Turning
Yasmin doesn't need to be polished: authenticity is the asset. A single honest post triggers a chain reaction that benefits Yasmin, her peers, SoG, and the wider employer network.
HTML
<div class="info-callout" role="note">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:5px">
<div style="width:28px;height:28px;border-radius:7px;
background:var(--purple-20);display:flex;
align-items:center;justify-content:center"
aria-hidden="true">
<span class="material-symbols-rounded"
style="font-size:16px;color:var(--sog-purple)">
autorenew
</span>
</div>
<strong>Starting The Wheel Turning</strong>
</div>
Yasmin doesn't need to be polished: authenticity is the asset.
</div>
Full Assembly: LinkedIn Flywheel
All three components assembled into the complete pattern. The ring is centred above the card grid. The callout sits below with a margin-top: 20px gap. The page container is constrained to 850px for desktop reading comfort.
Streets of Growth · LinkedIn Programme
How One Young Person's Posts Create Value For Everyone
YP & SoG
External
Network
Step 01YP
Young Person Posts
Yasmin shares an honest update about Phase 3.
Step 02SoG
SoG Staff Engage
Staff comment, share and amplify the post.
Step 03Partners
Partners Notice
Employer partners see authentic voice.
Step 04Peers
Peers Are Inspired
Others start posting too.
Step 05Network
SoG's Reach Grows
Credibility grows with funders and press.
Step 06Outcome
Prospects Improve
Employers reach out to Yasmin.
autorenew
Starting The Wheel Turning
Yasmin doesn't need to be polished: authenticity is the asset. A single honest post triggers a chain reaction that benefits Yasmin, her peers, SoG, and the wider employer network.
Patterns
Programme Position
A compact progress tracker that shows a participant's current position within the 24-month SOG programme. The bar visualises three equal phase segments with relapse events marked as orange diamonds and the current month as a pulsing blue dot. Use at the top of a participant detail view or case summary panel.
24-Month Programme Timeline
Phase segments are equal thirds of the 24-month bar. Marker positions are calculated as (month / 24) × 100%. Active phase is full opacity; future phases dim to 35%. Relapse is an event on the bar; it does not reset or rewind the phase.
/* Uses identical ptc-* CSS as the Cohort View above. */
/* The only difference is 4 bar segments and (month / 36) × 100% for positions. */
/* Post-programme segment states */
/* Upcoming: background: var(--sog-green); opacity: 0.2 */
/* Active: background: var(--sog-green) */
/* Now-dot colour for post-programme participant */
/* background: var(--sog-green) */
Usage Rules
Bar positions: Always calculate as (month / 24) × 100 applied as a CSS left percentage. Never hardcode positions.
Phase colours: Phase 1 = var(--sog-purple). Phase 2 = var(--sog-blue). Phase 3 = var(--sog-teal) at opacity: 0.35 until active. Do not use red as a phase segment - relapse is an event marker, not a phase.
Relapse markers: Red diamonds (var(--sog-red), rotate(45deg)) placed at their exact month position on the bar. No phase reset - the bar does not rewind. The header badge shows the total relapse count.
Survey urgency: Flag any survey within 12 weeks of today as pt-s-soon and colour the meta card date var(--color-warning-text). Post-programme surveys use pt-s-upcoming with the label "Post-programme".
Data Visualisation
Overview & Tokens
All SOG charts use Chart.js 4.4.0 with Open Sans applied globally. The SOG_COLORS array is the single source of truth for chart colours: always use this constant, never hardcode hex values directly in chart configs. Import the global Chart.js defaults once before any chart initialisation so every chart inherits consistent typography, tooltip styling, and legend sizing automatically.
Colour to Role Mapping
Each colour maps to a SOG brand value and a data role. The order in SOG_COLORS is intentional: Purple is always the primary series, Blue the secondary, and so on. Use colours in index order for multi-series charts so the palette is consistent across every chart in the product.
Purple
Blue
Sky
Teal
Green
Charcoal
Red
Orange
Yellow
Red, Orange, and Yellow (SOG_COLORS[6–8]) are semantic colours reserved for risk and status encoding. Never use them as general series colours in multi-series charts.
Sequential & Monochromatic Scales
The SOG_COLORS array is for categorical data (one colour per series). For ordered, intensity, or single-series data (heatmaps, calendar heatmaps, treemaps, and single-series bar or area charts) use the tint or shade steps of one colour family from lightest to darkest instead: tints for light-background ramps, shades for deeper saturated fills. Never mix different hues to encode ordered data; pick the family whose brand role matches the metric. Full step values live in the Tint & Shade scales under Colour Palette.
Purple
Tints (light → base)
Shades (base → dark)
Teal
Tints (light → base)
Shades (base → dark)
Green
Tints (light → base)
Shades (base → dark)
Use a single family's tint ramp for intensity or ordered scales, and keep flat fills for all data marks so magnitude reads accurately.
Chart.js Version & Setup
All charts use Chart.js 4.4.0 with the date-fns adapter for time-axis charts. Load both scripts before any chart initialisation. The global defaults block must run once after Chart.js loads and before any new Chart() call: it sets Open Sans, the SOG text colour, and consistent tooltip and legend sizing across every chart automatically.
Copy both blocks into a shared charts.js file that every chart module imports. Never paste SOG_COLORS inline into individual chart configs: a single source of truth means a colour update propagates everywhere automatically.
JS
// ── SOG_COLORS - single source of truth for all chart colours ──
const SOG_COLORS = [
'#625D9C', // Purple · Professional · primary series
'#4197CB', // Blue · Belonging · secondary series
'#5EB3E4', // Sky · Collaborative· tertiary series
'#0095A9', // Teal · Dynamic · highlight / accent
'#00AF9A', // Green · Independence · positive / on target
'#323E48', // Charcoal · Purpose · neutral / baseline
'#F32735', // Red · SEMANTIC · alert / high risk only
'#FF6C0E', // Orange · SEMANTIC · warning / medium risk only
'#F2CD00', // Yellow · SEMANTIC · on hold / low risk only
];
// Shorthand alias used throughout the guide
const C = {
purple: SOG_COLORS[0],
blue: SOG_COLORS[1],
sky: SOG_COLORS[2],
teal: SOG_COLORS[3],
green: SOG_COLORS[4],
charcoal: SOG_COLORS[5],
red: SOG_COLORS[6],
orange: SOG_COLORS[7],
yellow: SOG_COLORS[8],
};
// ── Global Chart.js defaults - run once after Chart.js loads ────
Chart.defaults.font.family = "'Open Sans', sans-serif";
Chart.defaults.font.size = 12;
Chart.defaults.color = '#70787F'; // var(--text-muted)
Chart.defaults.plugins.legend.labels.boxWidth = 12;
Chart.defaults.plugins.legend.labels.padding = 16;
Chart.defaults.plugins.tooltip.cornerRadius = 6;
Chart.defaults.plugins.tooltip.padding = 10;
Chart.defaults.plugins.tooltip.backgroundColor = '#323E48';
Chart.defaults.plugins.tooltip.titleFont.weight = '600';
Chart.defaults.scale.grid.color = 'rgba(0,0,0,0.06)';
Chart.defaults.scale.border.dash = [4, 4];
How each chart section is structured
Every chart section in this guide follows the same four-part structure so developers know exactly where to look regardless of which chart they are reading.
① Section desc
What the chart shows and which section of the product it lives in
② Live chart demo
Interactive Chart.js or HTML render using real SOG data
③ Encoding guide + Design rules
How to read the visual encoding. Constraints, max values, and what not to do
④ When to use + Code
Decision guidance and copy-pasteable JS, HTML, and CSS
Data Visualisation
Timeline (Bubble Chart)
A bubble chart repurposed as a programme timeline. Each bubble represents a session or intervention event. X axis = date, Y axis = participant or programme category, bubble radius = session duration or intensity. Reveals patterns in when interventions cluster and which participants receive the most contact.
Programme Session Timeline: Jan–Mar 2024
Each bubble is one session. X = date, Y = participant, radius = session duration (minutes). Colour encodes phase.
Intervention
Group Session
Phase Session
Assessment
Encoding guide
X axis encodes date (time). Y axis encodes participant or category. Bubble radius encodes a third quantitative variable (session duration, intensity score, or contact hours). Colour encodes phase using SOG_COLORS. Bubbles use 60% opacity to handle overlap. Minimum radius: 4px. Maximum radius: 24px.
Design rules
Keep Y axis entries to 10–15 maximum for legibility. Use a legend for colour encoding. Add a tooltip showing all three encoded values on hover. Never use bubble charts for more than 4 categories of colour encoding the chart becomes unreadable. If session duration is not meaningful, use a standard scatter plot instead.
When to use
Use timeline bubble charts to reveal patterns in intervention frequency, intensity, and clustering across participants or time periods. Particularly useful for programme managers reviewing caseload distribution and identifying participants with gaps in service. Not suitable for precise date-to-date comparisons use a calendar heatmap for daily granularity.
A mixed bar and line chart showing individual participant risk scores (bars) against the programme average (line). Used in clinical dashboards to identify participants requiring immediate review and track risk trend over time.
Participant Risk Scores: Current Cohort
Bars show each participant's current risk score. Red line shows programme average. Bars above 70 are flagged red.
High risk (≥70)
Elevated (40–69)
Low risk (<40)
Trend
Encoding guide
Bars encode individual risk scores. Bar colour encodes risk band: SOG Green (0–30 low), SOG Orange (31–69 medium), SOG Red (70–100 high). The programme average line uses SOG Purple at 2px with a dashed stroke. The Y axis runs 0–100. X axis labels are participant IDs, never full names maintain data privacy in shared dashboards.
Design rules
Always show the risk band thresholds as horizontal reference lines (30 and 70). Sort participants by risk score descending so the most at-risk appear first. Truncate to 20 participants maximum per view use pagination for larger cohorts. The average line must always be visible; if the chart is too narrow, move it to a separate KPI card above.
When to use
Use at the top of a clinical dashboard or caseload review screen to give key workers an immediate read of which participants need attention. Always pair with a data table showing participant names and last contact dates the chart identifies who, the table explains what to do next. Never use risk score charts in public-facing reports.
<!-- Mixed bar + line - risk scores with trend overlay -->
<div style="position:relative;height:340px">
<canvas id="chart-risk-canvas"
role="img"
aria-label="Bar chart showing participant risk scores with trend line">
</canvas>
</div>
Data Visualisation
Tool Frequency Chart
A horizontal bar chart showing how often each intervention tool or resource is used across the programme. Helps programme managers identify over-reliance on specific tools and gaps in the toolkit.
Intervention Tool Usage: Last 90 Days
Horizontal bars show session count per tool. Sorted by frequency descending.
Encoding guide
Horizontal bars encode frequency (session count). Bar length is proportional to count. All bars use SOG Purple. Category labels sit left of the bars in a fixed 160px column. Count labels appear at bar tips. The chart uses a horizontal layout because tool names are long vertical labels would be unreadable.
Design rules
Always sort by frequency descending. Show a maximum of 12–15 tools. If more tools exist, group the tail as "Other" with a tooltip breakdown. Use a single colour (SOG Purple) do not colour-encode categories in this chart as that implies a meaning the data does not have. Start the X axis at zero always.
When to use
Use in programme quality reviews and training planning sessions to understand which tools are over or under-used. Not suitable for participant-level reporting. Always show the time period in the chart title so comparisons across reports are unambiguous.
<!-- Horizontal bar - single colour, sorted descending -->
<div style="position:relative;height:360px">
<canvas id="chart-tools-canvas"
role="img"
aria-label="Horizontal bar chart showing intervention tool usage frequency">
</canvas>
</div>
Data Visualisation
Monthly Usage Chart
A grouped or stacked bar chart showing session volume and participant engagement by month. Used for operational reporting, board dashboards, and funder reports to show programme activity trends over time.
Monthly Session Volume & Active Participants
Grouped bars: purple = sessions delivered, teal = active participants. Last 12 months.
Intervention
Group Session
Assessment
Employment
Mentoring
Encoding guide
Grouped mode: two bars side by side per month SOG Purple for sessions, SOG Teal for active participants. Stacked mode: bars subdivided by phase Phase 1 (purple), Phase 2 (blue), Phase 3 (teal). Use grouped when comparing two different metrics; use stacked when showing composition of a single metric. Always show a legend.
Design rules
Show a rolling 12-month window. Use abbreviated month labels (Jan, Feb…). Start Y axis at zero. Add a data table below the chart for reports that will be printed or shared as PDFs the bars are hard to read precisely. Never stack more than 4 series; if more phases exist, group them.
When to use
Use monthly usage charts in board packs, quarterly reports, and funder dashboards to show activity trends over time. Pair with a year-on-year comparison line chart when funders ask about growth. Avoid using monthly bars for data that varies more meaningfully at a weekly or daily scale.
<!-- Grouped bar - two datasets side by side per month -->
<div style="position:relative;height:340px">
<canvas id="chart-monthly-canvas"
role="img"
aria-label="Grouped bar chart showing monthly sessions and active participants">
</canvas>
</div>
Data Visualisation
Effectiveness Chart
A multi-series line chart tracking outcome scores across multiple domains over the course of the programme. Shows whether intervention is producing measurable improvement and at what pace across different areas of a young person's life.
Outcome Domain Scores: Programme Progress
Each line tracks one outcome domain from programme start to most recent assessment. Higher = better.
Composite Score
Risk Score
Safety Score (YP)
Safety (Coach)
Self-Efficacy
Encoding guide
Each line encodes one outcome domain (Safety, Employment, Education, Housing, Wellbeing, Social). Colour uses SOG_COLORS in sequence. Lines are 2px with circular point markers at each assessment. X axis encodes assessment date or programme phase. Y axis runs 0–100 (outcome score). A shaded target band (green-10, 70–100) shows the target range.
Design rules
Show a maximum of 6 lines. If more domains exist, allow the user to toggle them. Always show the target band. Use point markers at assessment points do not interpolate between assessments as if change is continuous. Add a legend. Lines that reach the target band at exit should be highlighted with a filled endpoint marker.
When to use
Use effectiveness charts in participant review panels, case conferences, and programme impact reports to show whether the intervention is working and which domains need more support. This is the primary chart for evidence of programme effectiveness use it prominently in funder reports. Pair with a diverging bar chart showing net change for a complementary view.
<!-- Multi-line - one line per outcome domain -->
<div style="position:relative;height:360px">
<canvas id="chart-effectiveness-canvas"
role="img"
aria-label="Line chart comparing intake and exit outcome scores across domains">
</canvas>
</div>
Data Visualisation
Session Breakdown & Assessment Scores
A composable panel pattern combining horizontal bar chart rows, circular gauges, and a threshold callout. Built from four standalone components that can be used independently or assembled into the full session breakdown panel shown at the end of this section.
Horizontal Bar Chart Row
A CSS-native bar row for categorical counts label, track, fill, and count. Not a chart library component: it's a design system primitive that's faster to render, fully accessible, and easier to style than a canvas chart. Use for session type breakdowns, tool usage counts, and any ordered categorical list. Bars are proportional to the maximum value in the set, calculated in JS. Use the Zero-state Bar variant when a category genuinely has zero occurrences never omit the row, as absence of a bar looks like missing data.
Session Types
Interventions
11
Group Sessions
0
Phase Sessions
4
Safety Survey
1
Self Efficacy
1
HTML
<!-- Standard bar row -->
<div class="hbar-row">
<span class="hbar-label">Interventions</span>
<div class="hbar-track">
<div class="hbar-fill" style="width:100%;background:var(--sog-purple)"></div>
</div>
<span class="hbar-count">11</span>
</div>
<!-- Zero-state bar - value is genuinely 0, not missing -->
<div class="hbar-row">
<span class="hbar-label">Group Sessions</span>
<div class="hbar-track"><div class="hbar-zero"></div></div>
<span class="hbar-count hbar-count-zero">0</span>
</div>
// Calculate proportional bar widths from a data array
function renderHBars(data, containerSelector) {
const max = Math.max(...data.map(d => d.value));
const container = document.querySelector(containerSelector);
data.forEach(({ label, value, colour }) => {
const pct = max === 0 ? 0 : (value / max) * 100;
const row = container.querySelector(`[data-label="${label}"]`);
if (!row) return;
const fill = row.querySelector('.hbar-fill');
const zero = row.querySelector('.hbar-zero');
if (value === 0) {
if (fill) fill.style.width = '0';
if (zero) zero.style.display = 'block';
} else {
if (fill) { fill.style.width = pct + '%'; fill.style.background = colour; }
if (zero) zero.style.display = 'none';
}
});
}
Circular Gauge
An SVG ring gauge for assessment scores. The ring fills proportionally from the top (12 o'clock) using stroke-dashoffset. Three colour variants mapping to SOG tokens Sky for Safety YP, Blue for Safety Coach, Teal for Self Efficacy. Always show both the value and the maximum (96 / 100) so the proportion is readable without mental arithmetic. Never use a gauge when a plain number would suffice only use when the proportion relative to maximum carries meaning.
The gauge card wraps a circular gauge with a label into a contained, composable unit. Use in a flex or grid row when showing multiple scores side by side. The card provides the surface, border, and spacing the SVG gauge sits inside it. Always use in groups of 2–4 for assessment panels; a single gauge card in isolation should use the base card pattern instead.
A semantic warning callout for data panels distinct from the general info callout. Use when a calculated value (gap, ratio, rate) exceeds a defined threshold and requires clinical or operational attention. The callout is always data-driven: it only appears when the value exceeds the threshold, never as a static decoration. The orange treatment is semantically reserved do not use it for notes or general guidance. The threshold and current value must both appear in the copy.
All four components assembled into the full clinical panel. The panel header uses the default (purple) variant. Section labels divide the bar rows from the gauge cards. The threshold callout appears below the gauges only when a threshold is breached it is not visible at all when all scores are within range. This is the reference implementation for the SOG participant dashboard.
Horizontal bar rows use an 8px track with a coloured fill proportional to the value. Assessment score rings use a 240° arc sweep centred below the value label. Gauge cards combine the ring with a title and sub-label in a white contained card. Threshold callouts use a colour-coded left border (green/amber/red) with an icon and a short summary sentence.
When to use
Use the Session Breakdown panel on participant detail pages to give key workers a fast read of recent session quality and assessment scores. The assembled panel combines all four sub-components into a single coherent block. Never show assessment score rings without a corresponding threshold callout the score alone has no context.
Data Visualisation
Engagement Heatmap
A grid heatmap showing engagement intensity across two categorical dimensions typically participant by week, or cohort by intervention type. Cell colour intensity encodes the engagement score or contact frequency. Reveals patterns of uneven engagement that would be invisible in summary statistics.
Participant Engagement by Week: Q1 2024
Each cell = one participant × one month. Deeper purple = higher engagement score. Grey = no contact.
Encoding guide
Rows = participants (or cohorts), columns = time periods (or intervention types). Cell colour encodes engagement score using a five-stop purple ramp drawn from the brand token system: muted-bg (0, no contact) → purple-10 → purple-40 → purple-70 → SOG Purple (maximum). Solid token colours are used at every stop - not alpha transparency - so cells render consistently on any background. Cells are 44px × 40px with 1px borders. Row and column labels are 11px charcoal text.
Design rules
Always use a single-hue colour ramp never use a diverging scale for engagement (there is no meaningful negative). Show no more than 20 rows before paginating. Sort rows by total engagement score descending so the most engaged participants appear first. Add tooltips showing participant name, week, and exact score. Never show participant names in public-facing reports use anonymised IDs.
When to use
Use engagement heatmaps in caseload management reviews and programme quality audits to identify participants with consistent low engagement before they disengage entirely. The grid format makes it immediately obvious which participants have gaps in contact. Not suitable for funder reports replace with a summary KPI or monthly usage chart for external audiences.
JS
// Engagement heatmap - pure HTML/CSS grid, no canvas
function renderEngagementHeatmap(containerId, data) {
// data: { participants: string[], periods: string[], values: number[][] }
const container = document.getElementById(containerId);
if (!container) return;
// Five-stop purple ramp using brand tokens (no alpha transparency)
const RAMP = ['#F5F6F7','#F0EFF6','#C0BED7','#918DB9','#625D9C'];
const TEXT = ['#70787F','#625D9C','#323E48','#fff','#fff'];
const max = Math.max(...data.values.flat());
let html = '<div style="display:grid;gap:4px">';
data.participants.forEach((name, row) => {
html += '<div style="display:flex;align-items:center;gap:6px">';
html += `<span style="font-size:11px;color:var(--text-muted);width:100px;text-align:right;flex-shrink:0">${name}</span>`;
data.periods.forEach((period, col) => {
const v = data.values[row][col];
const stop = v === 0 ? 0 : Math.max(1, Math.ceil((v / max) * 4));
html += `<div style="width:28px;height:28px;border-radius:4px;background:${RAMP[stop]};color:${TEXT[stop]}"
title="${name}: ${period} - ${v} sessions"
role="img" aria-label="${name} ${period}: ${v} sessions"></div>`;
});
html += '</div>';
});
html += '</div>';
container.innerHTML = html;
}
renderEngagementHeatmap('heatmap-container', {
participants: ['Jordan Smith', 'Amara Kofi', 'Marcus Obi'],
periods: ['W1','W2','W3','W4','W5','W6','W7','W8'],
values: [[3,2,0,4,3,1,0,3],[1,3,2,3,0,4,3,2],[2,0,3,1,2,3,2,0]]
});
A composable scorecard system for tracking milestone completion across programme phases. Built from five standalone components: phase column, phase status label, criterion row, progress row, and inline warning banner. Each component can be used independently or assembled into the full three-phase scorecard shown at the end of this section.
Phase Status Label
Three states encoding the completion status of a programme phase. Each state has a distinct colour, icon, and top-border colour on the phase column. Complete (green) all required criteria met. Early exit (orange) phase exited before completion due to a qualifying event. Active (purple) the current in-progress phase. Never use green for early exit the distinction carries clinical meaning.
The atomic unit of the scorecard. Each row combines a pass/fail status icon with a labelled criterion and optional sub-text. The icon is always 16px and uses a tint circle filled red for fail, green for pass never raw colour or Unicode symbols. Sub-text appears beneath the label for context (dates, notes, measurements).
An annotated progress bar indented beneath a criterion row, showing numeric progress toward a target. The fill animates in on scroll via IntersectionObserver this gives the scorecard a data-loading feel without being gratuitous. Use green fill for criteria in progress, purple fill for phase-level metrics. Always show a left-aligned annotation beneath the bar.
// Animate progress bars when they scroll into view
function initProgressBars() {
const bars = document.querySelectorAll('.sg-animate-bar');
if (!bars.length) return;
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) {
const bar = e.target;
// Tiny delay so CSS transition fires after width:0 has painted
requestAnimationFrame(() =>
requestAnimationFrame(() => {
bar.style.width = reduced ? bar.dataset.w : bar.dataset.w;
})
);
io.unobserve(bar);
}
});
}, { threshold: 0.1 });
bars.forEach(b => io.observe(b));
}
Inline Warning Banner
A compact contextual warning for use inside a phase column not a full-width alert. Use only for qualifying events that changed the phase outcome. Two variants: the default (orange) for stage triggers and early exits - events that occurred and are managed; the critical modifier (red) for escalations that require immediate action, such as a clinical review. The banner always names the specific event. A phase with no qualifying events never shows a banner regardless of how many criteria failed.
A muted strip at the base of a phase column carrying summary metadata window duration, interaction count, days remaining. Visually separates the criteria list from panel-level stats. Always uses var(--muted-bg) background and var(--charcoal-40) text. Never put actionable content in the footer strip.
All five components assembled into the full three-phase clinical scorecard. Top borders encode status (green = complete, orange = early exit, purple = active). Progress bars animate in on scroll. The warning banner only appears in Phase 2 because a qualifying event occurred. Phase 3 is the active phase the pulse dot animates continuously.
Phase Readiness Scorecard
Phase 1
check_circle
Complete
14 May 2025 → 12 Aug 2025
check
Interventions conducted
6 of 3 target
check
Risk baseline recorded
Score captured
check
Consistent engagement (gap ≤ 30d)
Max gap: 13 days
Phase 2
bolt
Early exit
12 Aug 2025 → 25 Feb 2026
bolt
Stage 6 triggered: early exit
close
MI Techniques sessions
0 of 2 target
check
CBS Techniques sessions
5 of 2 target
check
Risk trend declining
28.4 → 5.6
close
Self Efficacy survey completed
Not recorded
Phase 3: Maintenance
Active
25 Feb 2026 → ongoing
check
Risk avg (last 5) ≤ 10
5.6 (target ≤ 10)
check
Safety Score YP ≥ 80
96 / 100
check
Stage 6 sessions ≥ 2
4 session(s)
check
Engagement maintained (≤ 60d)
Last contact: 19 days ago
When to use
Use the Phase Scorecard at programme transition points to show whether a young person is ready to move to the next phase. Each criterion row maps to a measurable readiness indicator. The assembled scorecard gives key workers, managers, and panel reviewers a structured evidence base for phase decisions. Never use the scorecard as a standalone report always contextualise it with session notes and key worker assessment.
Data Visualisation
KPI Stat Cards & Sparklines
Headline metrics with inline trend sparklines. Use at the top of any dashboard to give an immediate performance snapshot. Each card shows a single number, trend direction, percentage change, and a 12-point sparkline.
Programme Dashboard: Key Metrics
Four core KPIs with trend sparklines. Green upward arrow = positive, red downward = negative.
Encoding guide
Each card has four elements: an eyebrow label (11px / 700 / uppercase), a headline number (40px / 700 in the card's accent colour), a trend pill (▲ / ▼ + % change), and a 12-point sparkline. The sparkline line and fill use the card's hex colour. The card colour class is derived automatically from the data-color attribute.
Design rules
One KPI per card. Maximum 4 cards per row. The sparkline must use the same time period across all cards in the same row. Arrow direction is always positive/negative relative to the previous period never compare to a different baseline without labelling it.
When to use
Use at the top of any dashboard to give an immediate performance snapshot before the user reads detailed charts. Limit to the 4 most important metrics. Do not use KPI cards for data that requires context to interpret pair with a supporting chart.
JS
// KPI sparkline cards use data-* attributes - rendered by initKPICards()
// initKPICards() maps data-color hex → card colour class, then renders:
// .kpi-label - eyebrow metric name (11px / 700 / uppercase)
// .kpi-val - headline number (40px / 700, colour from card class)
// .kpi-change - trend pill (▲ / ▼ + %) coloured to match card
// data-value - current metric value
// data-prev - previous period value (for % change calculation)
// data-label - metric name shown as eyebrow
// data-color - hex for sparkline line + fill; also drives card colour class
// data-suffix - unit appended to value (%, pts, etc.)
// data-invert - "true" if lower is better (e.g. risk score)
// data-spark - 12 comma-separated sparkline data points
The standard tooltip applied to every Chart.js visualisation in the SOG design system. Provides consistent inline data lookup on hover across all chart types.
Month 6 · Phase 2
Composite Score68.4
Risk Score32.1
Safety (YP)74.0
Phase 2 - Engaging
Anatomy
The tooltip has three layers. The title (white, 12px / 700) identifies the shared x-axis label - typically a date, month, or category. The body items (rgba(255,255,255,.8), 12px) show each dataset value with a coloured point marker aligned left. The optional footer (rgba(255,255,255,.4), 10px / 600) provides contextual metadata such as phase name or cohort label.
Design rules
Always use interaction:{mode:'index',intersect:false} for multi-series charts so all datasets at the hovered x position are shown simultaneously. Never use the raw Chart.js default tooltip styling. Always use the charcoal background (#323E48) for maximum contrast across light and dark page surfaces. The footer is optional - use it only when contextual labels (phase name, cohort, date range) add meaningful information.
Displays a single metric against a scale or target. Use for capacity indicators, goal completion, and performance thresholds where a single number needs immediate context.
Programme Capacity & Performance Gauges
Three independent gauges. The coloured arc shows current value; grey arc shows remaining capacity or headroom.
Encoding guide
The arc sweeps from 7 o'clock to 5 o'clock (240° total). The coloured fill arc represents the current value; the grey arc is the remainder. A centred numeric label shows the value; a sub-label shows the metric name. The background track is charcoal-10.
Design rules
Green (SOG Green) for values in target range. Amber (SOG Orange) for values approaching threshold. Red (SOG Red) for values below acceptable threshold. Never use more than 3 gauges per row. Always pair a gauge with a text label the arc alone is not sufficient for screen readers.
When to use
Use for a single KPI that needs immediate threshold context: capacity (current vs max), goal completion (%), or performance score vs target. Avoid gauges when the exact number matters more than the zone use a KPI card instead.
JS
// Gauges are SVG-based - no Chart.js
// Each .gauge-wrap[data-value] is rendered by initGaugeCharts()
// SVG arc formula - semicircle (180° sweep):
// cx=100, cy=100, r=72, strokeWidth=18
// Value 0% = left (180°), 100% = right (0°)
function gaugeArc(value, cx, cy, r) {
const angle = Math.PI * (1 - value / 100); // radians
const x = (cx + r * Math.cos(angle)).toFixed(2);
const y = (cy - r * Math.sin(angle)).toFixed(2);
const largeArc = value > 50 ? 1 : 0;
// SVG arc path: start at left (cx-r, cy), sweep to computed point
return `M ${cx - r} ${cy} A ${r} ${r} 0 ${largeArc} 1 ${x} ${y}`;
}
// Colour thresholds:
// Green (SOG Green #00AF9A): value >= 70 - on target
// Orange (SOG Orange #FF6C0E): value 40–69 - approaching threshold
// Red (SOG Red #F32735): value < 40 - below threshold
function gaugeColor(value) {
return value >= 70 ? '#00AF9A' : value >= 40 ? '#FF6C0E' : '#F32735';
}
Ultra-compact single-metric display showing actual value, target, and performance range. Perfect for dense reporting tables where space is limited and multiple KPIs need to be compared at a glance.
Three background bands (red / amber / green) set the performance context. A single coloured bar shows actual value. A vertical tick mark shows the target. The bar colour matches the band it falls in. Row height: 32px. Row label sits left-aligned in a fixed 160px column.
Design rules
Always define three performance bands. Target marker must be a different visual element from the bar never use colour alone. Keep rows to 6–8 maximum per chart. Sort by percentage of target achieved (highest first) unless a natural ordering exists.
When to use
Bullet charts replace gauges in dense reporting contexts. They communicate actual performance, a target, and qualitative ranges in a single compact row. Use in reporting tables where multiple KPIs need to be compared at a glance.
/* Bullet chart row layout */
#bullet-container > div { display: flex; align-items: center; gap: 12px; }
#bullet-container .bullet-label { width: 180px; text-align: right; flex-shrink: 0;
font-size: 12px; font-weight: 600; color: var(--text); }
#bullet-container .bullet-track { flex: 1; height: 32px; position: relative;
border-radius: 4px; overflow: hidden; }
/* Bands: red (poor) amber (satisfactory) green (good) - set via JS inline styles */
/* Actual bar: coloured fill, 50% height, centred vertically in the track */
/* Target marker: 3px vertical rule at the target percentage */
HTML
<!-- Bullet chart container - JS renders rows into this div -->
<div id="bullet-container"
role="img"
aria-label="Bullet chart showing programme KPIs against targets with performance bands"
style="padding:8px 0">
</div>
Data Visualisation
Funnel Chart
Shows drop-off through sequential programme stages. Essential for pipeline reporting referral to independent progression. Built in pure HTML/CSS for maximum flexibility.
Young Person Programme Journey
Number of young people at each stage of the programme pipeline. Hover for conversion rate from previous stage.
Encoding guide
Each stage is a horizontal bar. Bar width is proportional to participant count. Stage label sits left-aligned inside the bar when wide enough, otherwise outside. Conversion rate from the previous stage appears as a percentage on the right edge.
Design rules
Use SOG_COLORS in order from the palette. Keep stages to 5–7 maximum. Never skip stages. Always start with the widest bar at the top. Conversion labels use charcoal-40 text at 11px.
When to use
Use funnel charts when you have a defined sequence of stages where participants can drop out. The width of each bar is proportional to the count, making volume and conversion immediately visible. Avoid funnels for non-sequential data or when stages can be revisited.
/* Funnel bars - widths set via JS based on stage count */
#funnel-container > div {
transition: width 600ms ease-out; /* Animate on initial render */
}
#funnel-container > div:hover {
filter: brightness(1.08); /* Subtle hover feedback */
cursor: default;
}
HTML
<!-- Funnel container - JS writes bars into this div -->
<div id="funnel-container"
role="img"
aria-label="Programme funnel showing participant progression through stages"
style="padding:8px 0">
</div>
Data Visualisation
Diverging Bar Chart
Horizontal bars extending left (negative/decline) and right (positive/improvement) from a shared zero baseline. Ideal for before/after comparisons, net change, and showing where progress has and hasn't been made.
Outcome Score: Net Change (Intake to Exit)
Change in outcome domain scores from programme intake to exit. Purple = improvement, Red = decline.
Encoding guide
Bars extend left (negative/decline, SOG Red) and right (positive/improvement, SOG Purple) from a shared zero baseline in the centre. Domain labels sit on the left. Value labels appear at bar tips. The zero line is a 1px charcoal-20 vertical rule.
Design rules
Sort bars by magnitude of change (largest positive at top, largest negative at bottom) unless a natural ordering exists. Always annotate the zero line with a "0" label. Use consistent scale both sides. Minimum bar width: 4px so zero-change domains are still visible.
When to use
Diverging bars work best when values can be meaningfully positive or negative relative to a baseline. Use for net change, improvement vs decline, or agreement vs disagreement scales. Always label the zero line clearly.
<!-- Diverging bar - horizontal, zero baseline in centre -->
<div style="position:relative;height:320px">
<canvas id="chart-diverging-canvas"
role="img"
aria-label="Diverging bar chart showing net change in outcome scores from intake to exit">
</canvas>
</div>
Data Visualisation
Net Flow Bar Chart
A net flow chart collapses joiners and exits into a single bar per period green above the zero line when the cohort grew, red below when it shrank. A running active-total line overlaid on the right axis gives the cumulative picture. Use for dashboards and KPI panels where directional clarity matters more than raw composition.
Monthly Cohort Net Flow: Active Participants
Each bar = joiners minus exits. Green = net gain. Red = net loss. Purple line = running active cohort total.
Net gain
Net exit
Active cohort
Encoding guide
Positive bars (net inflow, SOG Green) rise above the baseline. Negative bars (net outflow, SOG Red) fall below. A running total line (SOG Purple, 2px) tracks cumulative change across the period. Bar width fills the column; gaps between bars are 2px.
Net gain bar
Independence Green (#00AF9A). Extends above the zero baseline. Value = joiners − exits.
Net loss bar
Warning Red (#F32735). Extends below the zero baseline. Value = joiners − exits (negative).
Running total line
Professional Purple (#625D9C). Solid line, weight 2.5, plotted on the right y-axis.
Zero baseline
Always explicitly drawn. It is the visual anchor. Bars grow up or down from this line.
When to use
Use a net flow bar chart when the directional question: "did we grow or shrink this month?" is more important than the raw composition of joiners and exits. It works best in dashboards and live reporting panels where space is limited and the audience needs an immediate answer. If the breakdown between joiners and exits is clinically significant (e.g. comparing exit reasons), use the grouped bar chart instead so both figures remain visible.
<!-- Net flow: mixed bar + line chart -->
<div style="position:relative;height:340px">
<canvas id="chart-waterfall-canvas"
role="img"
aria-label="Net flow chart showing monthly cohort change with running total line">
</canvas>
</div>
Data Visualisation
Scatter Plot
Plots correlation between two continuous variables. Use to explore whether engagement (sessions attended) is associated with outcome score improvement across participants.
Sessions Attended vs. Outcome Score Improvement
Each dot represents one participant. A positive correlation suggests higher attendance is associated with greater improvement.
Encoding guide
Each point is one participant. X axis = sessions attended. Y axis = outcome score improvement. Point radius = 4px. Colour encodes phase (Phase 1 purple, Phase 2 blue, Phase 3 teal). A linear regression line (SOG Purple, 60% opacity) shows the overall trend.
Design rules
Always include axis labels with units. Avoid overplotting by using 60% opacity on points. Add a legend when multiple colours are used. Trend line is optional but recommended when the correlation is the key message. Never use scatter for fewer than 10 data points.
When to use
Use scatter plots to explore whether two variables are correlated, not to prove causation. Especially useful in funder reporting as evidence of dosage-effect relationships. Add a trend line when the pattern is unclear for a general audience.
<!-- Scatter plot - each point is one participant -->
<div style="position:relative;height:340px">
<canvas id="chart-scatter-canvas"
role="img"
aria-label="Scatter plot showing correlation between sessions attended and outcome improvement">
</canvas>
</div>
Data Visualisation
Area Chart
A filled line chart for cumulative or volume trends over time. Ideal for showing growth in total young people reached and actively engaged year-on-year.
Young People Reached: Cumulative Growth (2015–2024)
Total young people ever reached (teal) vs. actively engaged in the programme (green) across each year.
Total Young People Reached
Actively Engaged
Encoding guide
Filled region under each line. Multiple series use semi-transparent fills (40% opacity) so overlapping areas remain legible. Lines are 2px. Points are hidden by default, visible on hover. Legend sits above the chart.
Design rules
Use a maximum of 3 series. Always use semi-transparent fills, never solid solid fills obscure overlapping data. Start the Y axis at zero unless the data range makes zero misleading. Area charts imply volume; if only the trend matters, use a line chart instead.
When to use
Use when the filled region carries meaning: cumulative totals, volume over time, or comparing two related metrics. Avoid when the precise values at each point matter more than the trend a bar chart is clearer for precise comparisons.
<!-- Area chart - filled line, two series max -->
<div style="position:relative;height:320px">
<canvas id="chart-area-canvas"
role="img"
aria-label="Area chart showing cumulative growth in young people reached and engaged">
</canvas>
</div>
Data Visualisation
Radial Charts
Radial charts work well for showing proportions, goal completion, and comparative rankings. Three approved patterns: Doughnut (part-to-whole), Polar Area (comparative magnitude), and Radar (multi-dimension scoring). All use SOG brand colours.
Doughnut Chart: Programme Referral Breakdown
Use doughnut charts to show part-to-whole composition. Keep to 5 segments or fewer for legibility.
Referral Sources
Distribution of young person referrals by source. Hover segments for detail.
School Referral
Youth Offending
Self-Referral
Social Services
Community Partner
Polar Area Chart: Tool Category Usage
Polar area charts compare magnitudes across categories using segment radius rather than arc angle. Good for showing relative scale at a glance.
Radar charts display multi-dimensional scoring on a single chart. Use when comparing a subject across 5-8 dimensions. Limit to 2 datasets for readability.
Outcome Dimension Scores: Intake vs Exit
Comparison of young person outcome scores at intake and programme exit across six dimensions.
<!-- Three charts on one row - doughnut, polar, radar -->
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:24px">
<div>
<canvas id="chart-doughnut" role="img"
aria-label="Doughnut chart showing referral source breakdown"></canvas>
</div>
<div>
<canvas id="chart-polar" role="img"
aria-label="Polar area chart showing tool category usage"></canvas>
</div>
<div>
<canvas id="chart-radar" role="img"
aria-label="Radar chart comparing intake and exit outcome scores"></canvas>
</div>
</div>
Data Visualisation
Calendar Heatmap
GitHub-style daily activity grid showing session or engagement volume across a full year. Each cell is one day; colour intensity reflects activity level.
Daily Session Activity: 2024
Each cell represents one day. Darker purple = higher number of sessions delivered that day.
Encoding guide
Each cell is one day (7px square, 1px gap). Cells are arranged in a week-per-column grid, grouped by month with abbreviated month labels above. Colour scale: white (0 sessions) → purple-10 → purple-40 → SOG Purple (maximum). Empty days (weekends with no activity) use the charcoal-10 background.
Design rules
Always include a colour scale legend. Keep cell size at 7–10px for a full year view. Use a single colour ramp (always SOG Purple). Never use red for low values it implies negative, but low session count may simply be a weekend.
When to use
Best for identifying patterns in daily activity over a full year: busy seasons, gaps in service delivery, or day-of-week trends. Especially compelling for operational reporting and annual reviews. Not suitable for datasets with fewer than 90 days.
JS
// Calendar heatmap - pure HTML/CSS grid, no canvas
function renderCalendarHeatmap(containerId, year, sessionCounts) {
// sessionCounts: object keyed by "YYYY-MM-DD" → count
const container = document.getElementById(containerId);
if (!container) return;
const max = Math.max(...Object.values(sessionCounts), 1);
const start = new Date(`${year}-01-01`);
const end = new Date(`${year}-12-31`);
// Pad to week start
const weeks = [];
let day = new Date(start);
day.setDate(day.getDate() - day.getDay()); // back to Sunday
while (day <= end) {
const week = [];
for (let d = 0; d < 7; d++) {
const iso = day.toISOString().slice(0, 10);
const count = sessionCounts[iso] || 0;
const alpha = count === 0 ? 0 : 0.15 + (count / max) * 0.85;
week.push({ iso, count, alpha });
day = new Date(day); day.setDate(day.getDate() + 1);
}
weeks.push(week);
}
// Render as flex-row of week columns
let html = '
Proportional rectangles for hierarchical or part-to-whole breakdowns. Ideal for showing how intervention types, funding streams, or service categories compare by volume.
Intervention Category Breakdown: by Participant Volume
Rectangle size is proportional to the number of participants engaged in each intervention category.
Encoding guide
Rectangles are sized proportionally to participant volume. Colour encodes category using SOG_COLORS in order. Labels show category name and percentage at 11px bold. Minimum label size: 40px × 30px cells smaller than this show no label. A tooltip shows the exact count on hover.
Design rules
Use SOG_COLORS in sequence. Keep to 8–10 categories maximum. Always show percentage labels inside cells where space allows. Add a count legend below. Avoid treemaps for time-series data use an area or bar chart instead.
When to use
Use treemaps to show proportional part-to-whole relationships across many categories simultaneously. They work best when categories differ significantly in size. If all values are similar, a bar chart is clearer. Avoid for more than 10–12 categories.
JS
// Treemap - pure HTML/CSS, no canvas
// Uses a simple slice-and-dice layout algorithm
function renderTreemap(containerId, data) {
// data: [{ label, value, color }]
const container = document.getElementById(containerId);
if (!container) return;
const total = data.reduce((s, d) => s + d.value, 0);
let html = '
';
data.forEach(d => {
const pct = (d.value / total * 100).toFixed(1);
const w = (d.value / total * 100).toFixed(1);
html += `