diff --git a/packages/site/assets/focus-example-backgrounds.png b/packages/site/assets/focus-example-backgrounds.png
deleted file mode 100644
index 2c2c70061..000000000
Binary files a/packages/site/assets/focus-example-backgrounds.png and /dev/null differ
diff --git a/packages/site/assets/focus-example-input.png b/packages/site/assets/focus-example-input.png
deleted file mode 100644
index eaf65a7b4..000000000
Binary files a/packages/site/assets/focus-example-input.png and /dev/null differ
diff --git a/packages/site/assets/focus-example-radios.png b/packages/site/assets/focus-example-radios.png
deleted file mode 100644
index 30c1d7290..000000000
Binary files a/packages/site/assets/focus-example-radios.png and /dev/null differ
diff --git a/packages/site/scripts/foundation-styles-data.js b/packages/site/scripts/foundation-styles-data.js
new file mode 100644
index 000000000..f274214d7
--- /dev/null
+++ b/packages/site/scripts/foundation-styles-data.js
@@ -0,0 +1,1015 @@
+const fs = require('node:fs');
+const path = require('node:path');
+
+const settingsDirectory = path.join(__dirname, '../../toolkit/core/settings');
+const toolsDirectory = path.join(__dirname, '../../toolkit/core/tools');
+const objectsDirectory = path.join(__dirname, '../../toolkit/core/objects');
+
+const files = {
+ tokensCore: path.join(settingsDirectory, '_tokens-core.scss'),
+ tokensBreakpoint: path.join(settingsDirectory, '_tokens-breakpoint.scss'),
+ tokensStatic: path.join(settingsDirectory, '_tokens-static.scss'),
+ breakpoints: path.join(settingsDirectory, '_breakpoints.scss'),
+ mixins: path.join(toolsDirectory, '_mixins.scss'),
+ grid: path.join(toolsDirectory, '_grid.scss'),
+ spacingTool: path.join(toolsDirectory, '_spacing.scss'),
+ typographyUtilities: path.join(
+ __dirname,
+ '../../toolkit/core/utilities/_typography.scss',
+ ),
+ mainWrapper: path.join(objectsDirectory, '_main-wrapper.scss'),
+};
+
+const typographyRows = [
+ {
+ id: 'heading-xl',
+ label: 'heading-xl',
+ previewText: 'Page heading',
+ className: 'ofh-heading-xl',
+ scaleKey: 'heading-xl',
+ notes: 'Default h1 style.',
+ },
+ {
+ id: 'heading-lg',
+ label: 'heading-lg',
+ previewText: 'Section heading',
+ className: 'ofh-heading-lg',
+ scaleKey: 'heading-lg',
+ notes: 'Default h2 style.',
+ },
+ {
+ id: 'heading-md',
+ label: 'heading-md',
+ previewText: 'Subsection heading',
+ className: 'ofh-heading-md',
+ scaleKey: 'heading-md',
+ notes: 'Default h3 style.',
+ },
+ {
+ id: 'heading-sm',
+ label: 'heading-sm',
+ previewText: 'Support heading',
+ className: 'ofh-heading-sm',
+ scaleKey: 'heading-sm',
+ notes: 'Default h4 style.',
+ },
+ {
+ id: 'heading-xs',
+ label: 'heading-xs',
+ previewText: 'Small heading',
+ className: 'ofh-heading-xs',
+ scaleKey: 'heading-xs',
+ notes: 'Used for h5, h6, and the smallest heading override.',
+ },
+ {
+ id: 'lead',
+ label: 'lead-md',
+ previewText: 'Use lead text once to introduce a page.',
+ className: 'ofh-body-l',
+ scaleKey: 'lead-md',
+ notes: 'Used by .ofh-body-l and .ofh-lede-text.',
+ },
+ {
+ id: 'paragraph',
+ label: 'paragraph-md',
+ previewText: 'Use body copy for most written content.',
+ className: 'ofh-body-m',
+ scaleKey: 'paragraph-md',
+ notes: 'Default paragraph style.',
+ },
+ {
+ id: 'paragraph-small',
+ label: 'paragraph-sm',
+ previewText: 'Use small body copy sparingly.',
+ className: 'ofh-body-s',
+ scaleKey: 'paragraph-sm',
+ notes: 'Secondary or supporting text.',
+ },
+ {
+ id: 'list',
+ label: 'list-md',
+ previewText: 'List item',
+ previewHtml: '
',
+ className: 'ofh-list',
+ scaleKey: 'list-md',
+ notes: 'Bulleted and numbered lists use this responsive list token.',
+ },
+ {
+ id: 'list-small',
+ label: 'list-sm',
+ previewText: 'List item',
+ previewHtml: '',
+ className: 'ofh-body-s',
+ scaleKey: 'list-sm',
+ notes: 'Smaller list token for more compact supporting content.',
+ },
+];
+
+const semanticColourGroups = [
+ {
+ title: 'Text',
+ items: [
+ '$ofh-color-foreground-primary',
+ '$ofh-color-foreground-secondary',
+ '$ofh-color-foreground-brand-blue-navy',
+ '$ofh-color-foreground-primary-inverted',
+ ],
+ },
+ {
+ title: 'Links',
+ items: [
+ '$ofh-color-foreground-link-default',
+ '$ofh-color-foreground-link-hover',
+ '$ofh-color-foreground-link-visited',
+ '$ofh-color-foreground-link-active',
+ ],
+ },
+ {
+ title: 'Breadcrumb navigation links',
+ items: [
+ '$ofh-color-foreground-breadcrumb-default',
+ '$ofh-color-foreground-breadcrumb-hover',
+ '$ofh-color-foreground-breadcrumb-visited',
+ '$ofh-color-foreground-breadcrumb-active',
+ ],
+ },
+ {
+ title: 'Focus state',
+ items: [
+ '$ofh-color-border-feedback-focus',
+ '$ofh-color-border-feedback-focus-inverted',
+ '$ofh-color-foreground-brand-blue-navy',
+ ],
+ },
+ {
+ title: 'Button',
+ items: [
+ '$ofh-color-background-button-default',
+ '$ofh-color-background-button-hover',
+ '$ofh-color-background-button-active',
+ '$ofh-color-background-button-default-inverted',
+ '$ofh-color-background-button-hover-inverted',
+ '$ofh-color-background-button-active-inverted',
+ ],
+ },
+ {
+ title: 'Contextual colours',
+ items: [
+ '$ofh-color-feedback-error-2-main',
+ '$ofh-color-feedback-success-2-main',
+ '$ofh-color-feedback-warning-2-main',
+ '$ofh-color-feedback-info-2-main',
+ ],
+ },
+ {
+ title: 'Borders',
+ items: [
+ '$ofh-color-border-primary',
+ '$ofh-color-border-secondary',
+ '$ofh-color-border-brand',
+ ],
+ },
+ {
+ title: 'Input fields',
+ items: [
+ '$ofh-color-border-input-default',
+ '$ofh-color-border-input-active',
+ '$ofh-color-background-primary',
+ ],
+ },
+ {
+ title: 'Page background',
+ items: [
+ '$ofh-color-background-primary',
+ '$ofh-color-background-secondary',
+ '$ofh-color-background-secondary-blue',
+ '$ofh-color-background-secondary-yellow',
+ '$ofh-color-background-secondary-grey',
+ ],
+ },
+];
+
+const paletteColourGroups = [
+ {
+ title: 'Greyscale',
+ items: [
+ '$ofh-color-greyscale-black',
+ '$ofh-color-greyscale-1',
+ '$ofh-color-greyscale-2',
+ '$ofh-color-greyscale-3',
+ '$ofh-color-greyscale-4',
+ '$ofh-color-greyscale-5',
+ '$ofh-color-greyscale-6',
+ '$ofh-color-greyscale-7',
+ '$ofh-color-greyscale-white',
+ ],
+ },
+ {
+ title: 'Brand blue navy',
+ items: [
+ '$ofh-color-brand-blue-navy-1',
+ '$ofh-color-brand-blue-navy-2',
+ '$ofh-color-brand-blue-navy-3-main',
+ '$ofh-color-brand-blue-navy-4',
+ '$ofh-color-brand-blue-navy-5',
+ '$ofh-color-brand-blue-navy-6',
+ ],
+ },
+ {
+ title: 'Brand blue royal',
+ items: [
+ '$ofh-color-brand-blue-royal-1',
+ '$ofh-color-brand-blue-royal-2',
+ '$ofh-color-brand-blue-royal-3-main',
+ '$ofh-color-brand-blue-royal-4',
+ '$ofh-color-brand-blue-royal-5',
+ '$ofh-color-brand-blue-royal-6',
+ ],
+ },
+ {
+ title: 'Brand blue aqua',
+ items: [
+ '$ofh-color-brand-blue-aqua-1',
+ '$ofh-color-brand-blue-aqua-2',
+ '$ofh-color-brand-blue-aqua-3-main',
+ '$ofh-color-brand-blue-aqua-4',
+ '$ofh-color-brand-blue-aqua-5',
+ '$ofh-color-brand-blue-aqua-6',
+ ],
+ },
+ {
+ title: 'Brand green teal',
+ items: [
+ '$ofh-color-brand-green-teal-1',
+ '$ofh-color-brand-green-teal-2',
+ '$ofh-color-brand-green-teal-3-main',
+ '$ofh-color-brand-green-teal-4',
+ '$ofh-color-brand-green-teal-5',
+ '$ofh-color-brand-green-teal-6',
+ ],
+ },
+ {
+ title: 'Brand green lime',
+ items: [
+ '$ofh-color-brand-green-lime-1',
+ '$ofh-color-brand-green-lime-2',
+ '$ofh-color-brand-green-lime-3-main',
+ '$ofh-color-brand-green-lime-4',
+ '$ofh-color-brand-green-lime-5',
+ '$ofh-color-brand-green-lime-6',
+ ],
+ },
+ {
+ title: 'Brand yellow',
+ items: [
+ '$ofh-color-brand-yellow-1',
+ '$ofh-color-brand-yellow-2',
+ '$ofh-color-brand-yellow-3-main',
+ '$ofh-color-brand-yellow-4',
+ '$ofh-color-brand-yellow-5',
+ '$ofh-color-brand-yellow-6',
+ ],
+ },
+ {
+ title: 'Brand orange',
+ items: [
+ '$ofh-color-brand-orange-1',
+ '$ofh-color-brand-orange-2',
+ '$ofh-color-brand-orange-3-main',
+ '$ofh-color-brand-orange-4',
+ '$ofh-color-brand-orange-5',
+ '$ofh-color-brand-orange-6',
+ ],
+ },
+ {
+ title: 'Brand red',
+ items: [
+ '$ofh-color-brand-red-1',
+ '$ofh-color-brand-red-2',
+ '$ofh-color-brand-red-3-main',
+ '$ofh-color-brand-red-4',
+ '$ofh-color-brand-red-5',
+ '$ofh-color-brand-red-6',
+ ],
+ },
+ {
+ title: 'Feedback',
+ items: [
+ '$ofh-color-feedback-error-1',
+ '$ofh-color-feedback-error-2-main',
+ '$ofh-color-feedback-error-3',
+ '$ofh-color-feedback-error-4',
+ '$ofh-color-feedback-success-1',
+ '$ofh-color-feedback-success-2-main',
+ '$ofh-color-feedback-success-3',
+ '$ofh-color-feedback-success-4',
+ '$ofh-color-feedback-warning-1',
+ '$ofh-color-feedback-warning-2-main',
+ '$ofh-color-feedback-warning-3',
+ '$ofh-color-feedback-warning-4',
+ '$ofh-color-feedback-info-1',
+ '$ofh-color-feedback-info-2-main',
+ '$ofh-color-feedback-info-3',
+ '$ofh-color-feedback-info-4',
+ '$ofh-color-feedback-interactive-1',
+ '$ofh-color-feedback-interactive-2',
+ '$ofh-color-feedback-interactive-3-main',
+ '$ofh-color-feedback-interactive-4',
+ '$ofh-color-feedback-interactive-5',
+ ],
+ },
+ {
+ title: 'Neutral backgrounds',
+ items: [
+ '$ofh-color-backgrounds-grey',
+ '$ofh-color-backgrounds-blue',
+ '$ofh-color-backgrounds-yellow',
+ ],
+ },
+];
+
+const indexCards = [
+ {
+ title: 'Colour',
+ href: '/design-system/styles/colour',
+ summary: 'Semantic colour tokens for UI work and the core palette they are built from.',
+ },
+ {
+ title: 'Focus state',
+ href: '/design-system/styles/focus-state',
+ summary: 'Focus colour guidance and implementation patterns for accessible interactive states.',
+ },
+ {
+ title: 'Icons',
+ href: '/design-system/styles/icons',
+ summary: 'Material icon inventory plus the fixed and responsive sizing rules used by the toolkit.',
+ },
+ {
+ title: 'Layout',
+ href: '/design-system/styles/layout',
+ summary: 'Breakpoints, containers, content widths, grid widths, and page wrapper spacing.',
+ },
+ {
+ title: 'Page template',
+ href: '/design-system/styles/page-template',
+ summary: 'The shared page shell, template blocks, and default content-page and transactional layouts.',
+ },
+ {
+ title: 'Spacing',
+ href: '/design-system/styles/spacing',
+ summary:
+ 'The horizontal and vertical responsive spacing scales, static size tokens, and spacing utility classes.',
+ },
+ {
+ title: 'Typography',
+ href: '/design-system/styles/typography',
+ summary: 'Responsive type styles, headline hierarchy, body sizes, and override utilities.',
+ },
+];
+
+function readFile(filepath) {
+ return fs.readFileSync(filepath, 'utf8');
+}
+
+function stripBlockComments(text) {
+ return text.replace(/\/\*[\s\S]*?\*\//g, '');
+}
+
+function splitStatements(text) {
+ const statements = [];
+ let current = '';
+ let depth = 0;
+ let quote = null;
+ let inLineComment = false;
+
+ for (let index = 0; index < text.length; index += 1) {
+ const character = text[index];
+ const nextCharacter = text[index + 1];
+
+ if (inLineComment) {
+ if (character === '\n') {
+ inLineComment = false;
+ }
+ continue;
+ }
+
+ if (!quote && character === '/' && nextCharacter === '/') {
+ inLineComment = true;
+ index += 1;
+ continue;
+ }
+
+ if (quote) {
+ current += character;
+ if (character === quote && text[index - 1] !== '\\') {
+ quote = null;
+ }
+ continue;
+ }
+
+ if (character === '\'' || character === '"') {
+ quote = character;
+ current += character;
+ continue;
+ }
+
+ if (character === '(') {
+ depth += 1;
+ } else if (character === ')') {
+ depth -= 1;
+ }
+
+ current += character;
+
+ if (character === ';' && depth === 0) {
+ const statement = current.trim();
+ if (statement) {
+ statements.push(statement.slice(0, -1).trim());
+ }
+ current = '';
+ }
+ }
+
+ const trailingStatement = current.trim();
+ if (trailingStatement) {
+ statements.push(trailingStatement);
+ }
+
+ return statements;
+}
+
+function splitTopLevel(value, delimiter) {
+ const parts = [];
+ let current = '';
+ let depth = 0;
+ let quote = null;
+
+ for (let index = 0; index < value.length; index += 1) {
+ const character = value[index];
+
+ if (quote) {
+ current += character;
+ if (character === quote && value[index - 1] !== '\\') {
+ quote = null;
+ }
+ continue;
+ }
+
+ if (character === '\'' || character === '"') {
+ quote = character;
+ current += character;
+ continue;
+ }
+
+ if (character === '(') {
+ depth += 1;
+ } else if (character === ')') {
+ depth -= 1;
+ }
+
+ if (character === delimiter && depth === 0) {
+ if (current.trim()) {
+ parts.push(current.trim());
+ }
+ current = '';
+ continue;
+ }
+
+ current += character;
+ }
+
+ if (current.trim()) {
+ parts.push(current.trim());
+ }
+
+ return parts;
+}
+
+function findTopLevelCharacter(value, target) {
+ let depth = 0;
+ let quote = null;
+
+ for (let index = 0; index < value.length; index += 1) {
+ const character = value[index];
+
+ if (quote) {
+ if (character === quote && value[index - 1] !== '\\') {
+ quote = null;
+ }
+ continue;
+ }
+
+ if (character === '\'' || character === '"') {
+ quote = character;
+ continue;
+ }
+
+ if (character === '(') {
+ depth += 1;
+ } else if (character === ')') {
+ depth -= 1;
+ } else if (character === target && depth === 0) {
+ return index;
+ }
+ }
+
+ return -1;
+}
+
+function isWrappedMap(value) {
+ if (!value.startsWith('(') || !value.endsWith(')')) {
+ return false;
+ }
+
+ let depth = 0;
+ let quote = null;
+
+ for (let index = 0; index < value.length; index += 1) {
+ const character = value[index];
+
+ if (quote) {
+ if (character === quote && value[index - 1] !== '\\') {
+ quote = null;
+ }
+ continue;
+ }
+
+ if (character === '\'' || character === '"') {
+ quote = character;
+ continue;
+ }
+
+ if (character === '(') {
+ depth += 1;
+ } else if (character === ')') {
+ depth -= 1;
+
+ if (depth === 0 && index !== value.length - 1) {
+ return false;
+ }
+ }
+ }
+
+ return depth === 0;
+}
+
+function normaliseExpression(value) {
+ return value.replace(/\s*!default\s*$/u, '').trim();
+}
+
+function parseKey(value) {
+ const trimmedValue = value.trim();
+
+ if (
+ (trimmedValue.startsWith('\'') && trimmedValue.endsWith('\'')) ||
+ (trimmedValue.startsWith('"') && trimmedValue.endsWith('"'))
+ ) {
+ return trimmedValue.slice(1, -1);
+ }
+
+ return trimmedValue;
+}
+
+function parseExpression(value) {
+ const trimmedValue = normaliseExpression(value);
+
+ if (isWrappedMap(trimmedValue)) {
+ const innerValue = trimmedValue.slice(1, -1);
+ const entries = splitTopLevel(innerValue, ',');
+ const parsedMap = {};
+
+ entries.forEach((entry) => {
+ const separatorIndex = findTopLevelCharacter(entry, ':');
+ if (separatorIndex === -1) {
+ return;
+ }
+
+ const key = parseKey(entry.slice(0, separatorIndex));
+ const entryValue = entry.slice(separatorIndex + 1);
+ parsedMap[key] = parseExpression(entryValue);
+ });
+
+ return {
+ type: 'map',
+ value: parsedMap,
+ };
+ }
+
+ if (/^\$[\w-]+$/u.test(trimmedValue)) {
+ return {
+ type: 'reference',
+ value: trimmedValue,
+ };
+ }
+
+ return {
+ type: 'literal',
+ value: trimmedValue,
+ };
+}
+
+function parseVariables(filepath) {
+ const content = stripBlockComments(readFile(filepath));
+ const statements = splitStatements(content);
+ const variables = {};
+
+ statements.forEach((statement) => {
+ const match = statement.match(/^(\$[\w-]+)\s*:\s*([\s\S]+)$/u);
+
+ if (!match) {
+ return;
+ }
+
+ variables[match[1]] = parseExpression(match[2]);
+ });
+
+ return variables;
+}
+
+function resolveExpression(expression, registry, seen = new Set()) {
+ if (!expression) {
+ return null;
+ }
+
+ if (expression.type === 'literal') {
+ return expression.value;
+ }
+
+ if (expression.type === 'map') {
+ return Object.fromEntries(
+ Object.entries(expression.value).map(([key, value]) => [
+ key,
+ resolveExpression(value, registry, seen),
+ ]),
+ );
+ }
+
+ if (expression.type === 'reference') {
+ if (seen.has(expression.value) || !registry[expression.value]) {
+ return expression.value;
+ }
+
+ const nextSeen = new Set(seen);
+ nextSeen.add(expression.value);
+
+ return resolveExpression(registry[expression.value], registry, nextSeen);
+ }
+
+ return null;
+}
+
+function toDisplayHex(value) {
+ if (!value || !value.startsWith('#')) {
+ return value;
+ }
+
+ const shortHexMatch = value.match(/^#([\da-f])([\da-f])([\da-f])$/iu);
+
+ if (shortHexMatch) {
+ return `#${shortHexMatch[1]}${shortHexMatch[1]}${shortHexMatch[2]}${shortHexMatch[2]}${shortHexMatch[3]}${shortHexMatch[3]}`.toUpperCase();
+ }
+
+ return value.toUpperCase();
+}
+
+function directSourceToken(token, registry) {
+ const entry = registry[token];
+
+ if (!entry) {
+ return token;
+ }
+
+ if (entry.type === 'reference') {
+ return entry.value;
+ }
+
+ return token;
+}
+
+function getTokenValue(token, registry) {
+ return resolveExpression(registry[token], registry, new Set([token]));
+}
+
+function needsSwatchBorder(hexValue) {
+ if (!hexValue || !hexValue.startsWith('#')) {
+ return false;
+ }
+
+ const fullHex = toDisplayHex(hexValue).replace('#', '');
+ const red = Number.parseInt(fullHex.slice(0, 2), 16);
+ const green = Number.parseInt(fullHex.slice(2, 4), 16);
+ const blue = Number.parseInt(fullHex.slice(4, 6), 16);
+ const luminance = (red * 299 + green * 587 + blue * 114) / 1000;
+
+ return luminance >= 220;
+}
+
+function toBreakpointSummary(scaleEntry, property) {
+ const mobileValue = property ? scaleEntry.null[property] : scaleEntry.null;
+ const tabletValue = property
+ ? (scaleEntry.tablet || scaleEntry.null)[property]
+ : scaleEntry.tablet || scaleEntry.null;
+ const desktopValue = property
+ ? (scaleEntry.desktop || scaleEntry.tablet || scaleEntry.null)[property]
+ : scaleEntry.desktop || scaleEntry.tablet || scaleEntry.null;
+
+ return {
+ mobile: mobileValue,
+ tablet: tabletValue,
+ desktop: desktopValue,
+ };
+}
+
+function buildTypographyData(registry) {
+ const typographyScale = getTokenValue('$ofh-typography-responsive-scale', registry);
+ const utilityScale = getTokenValue('$ofh-typography-utility-scale', registry);
+
+ const rows = typographyRows.map((row) => {
+ const scale = typographyScale[row.scaleKey];
+ const fontSize = toBreakpointSummary(scale, 'font-size');
+ const lineHeight = toBreakpointSummary(scale, 'line-height');
+
+ return {
+ ...row,
+ token: row.scaleKey,
+ mobile: fontSize.mobile,
+ tablet: fontSize.tablet,
+ desktop: fontSize.desktop,
+ lineHeight: formatTriplet(lineHeight),
+ };
+ });
+
+ const byId = Object.fromEntries(rows.map((row) => [row.id, row]));
+ const utilityRows = Object.keys(utilityScale)
+ .map((size) => Number.parseInt(size, 10))
+ .sort((first, second) => second - first)
+ .map((size) => {
+ const scale = utilityScale[size];
+ const fontSize = toBreakpointSummary(scale, 'font-size');
+ const lineHeight = toBreakpointSummary(scale, 'line-height');
+
+ return {
+ id: `utility-${size}`,
+ label: `${size}`,
+ previewText: 'Sample text',
+ className: `ofh-u-font-size-${size}`,
+ mobile: fontSize.mobile,
+ tablet: fontSize.tablet,
+ desktop: fontSize.desktop,
+ lineHeight: formatTriplet(lineHeight),
+ notes:
+ fontSize.tablet === fontSize.desktop
+ ? 'Tablet and desktop share the same value.'
+ : 'Utility collapses on smaller screens.',
+ };
+ });
+
+ return {
+ rows,
+ byId,
+ utilityRows,
+ };
+}
+
+function buildSpacingData(registry) {
+ const horizontalScale = getTokenValue('$ofh-space-horizontal-responsive-scale', registry);
+ const verticalScale = getTokenValue('$ofh-space-vertical-responsive-scale', registry);
+ const orderedSteps = Object.keys(horizontalScale)
+ .map((step) => Number.parseInt(step, 10))
+ .sort((first, second) => first - second);
+
+ const toRows = (scale, utilityPrefix) =>
+ orderedSteps.map((step) => {
+ const values = toBreakpointSummary(scale[step]);
+
+ return {
+ step,
+ mobile: values.mobile,
+ tablet: values.tablet,
+ desktop: values.desktop,
+ staticToken: `$ofh-size-${step}`,
+ utilityExample: `${utilityPrefix}${step}`,
+ };
+ });
+
+ const byKey = Object.fromEntries(
+ orderedSteps.map((step) => [
+ step,
+ {
+ step,
+ staticToken: `$ofh-size-${step}`,
+ horizontal: toBreakpointSummary(horizontalScale[step]),
+ vertical: toBreakpointSummary(verticalScale[step]),
+ },
+ ]),
+ );
+
+ return {
+ horizontalScale: toRows(horizontalScale, 'ofh-u-margin-right-'),
+ verticalScale: toRows(verticalScale, 'ofh-u-margin-bottom-'),
+ byKey,
+ };
+}
+
+function buildLayoutData(registry) {
+ const mqBreakpoints = getTokenValue('$mq-breakpoints', registry);
+ const gridWidths = getTokenValue('$_ofh-grid-widths', registry);
+ const contentMaxWidth = getTokenValue('$ofh-width-content-max', registry);
+ const readingWidth = extractReadingWidth();
+ const mainWrapper = buildMainWrapperData(registry);
+
+ const breakpoints = [
+ {
+ label: 'mobile',
+ token: '$mq-breakpoints.mobile',
+ value: mqBreakpoints.mobile,
+ note: 'Baseline viewport width for the smallest supported layouts.',
+ },
+ {
+ label: 'tablet',
+ token: '$mq-breakpoints.tablet',
+ value: mqBreakpoints.tablet,
+ note: 'First responsive breakpoint used by the toolkit.',
+ },
+ {
+ label: 'desktop',
+ token: '$mq-breakpoints.desktop',
+ value: mqBreakpoints.desktop,
+ note: 'Default grid breakpoint for multi-column layouts.',
+ },
+ {
+ label: 'large-desktop',
+ token: '$mq-breakpoints.large-desktop',
+ value: mqBreakpoints['large-desktop'],
+ note: 'Used for wider navigation and layout changes.',
+ },
+ ];
+
+ const widths = [
+ {
+ id: 'fluid',
+ label: 'Fluid container',
+ className: 'ofh-width-container-fluid',
+ value: '100%',
+ visualPercent: 100,
+ note: 'Spans the viewport with responsive gutters.',
+ },
+ {
+ id: 'content',
+ label: 'Content max width',
+ className: 'ofh-width-container',
+ value: contentMaxWidth,
+ visualPercent: 87.3,
+ note: 'Used by the default fixed-width container.',
+ },
+ {
+ id: 'reading',
+ label: 'Reading width',
+ className: 'ofh-u-reading-width',
+ value: readingWidth,
+ visualPercent: 64,
+ note: 'Constrains text to around 70 to 80 characters per line.',
+ },
+ ];
+
+ const gridColumns = Object.entries(gridWidths).map(([key, value]) => ({
+ label: key.replace(/-/gu, ' '),
+ className: `ofh-grid-column-${key}`,
+ value,
+ visualPercent: Number.parseFloat(value),
+ }));
+
+ return {
+ breakpoints,
+ breakpointsById: Object.fromEntries(
+ breakpoints.map((breakpoint) => [breakpoint.label, breakpoint]),
+ ),
+ widths,
+ widthsById: Object.fromEntries(widths.map((width) => [width.id, width])),
+ gridColumns,
+ mainWrapper,
+ };
+}
+
+function extractReadingWidth() {
+ const content = stripBlockComments(readFile(files.mixins));
+ const match = content.match(/@mixin reading-width\(\)\s*\{\s*max-width:\s*([^;]+);/u);
+
+ return match ? match[1].trim() : '44em';
+}
+
+function buildMainWrapperData(registry) {
+ const content = stripBlockComments(readFile(files.mainWrapper));
+ const verticalScale = getTokenValue('$ofh-space-vertical-responsive-scale', registry);
+
+ const mixinNames = [
+ ['govuk-main-wrapper', '.ofh-main-wrapper', 'Base page wrapper for main content.'],
+ ['govuk-main-wrapper--l', '.ofh-main-wrapper--l', 'Add to .ofh-main-wrapper for extra top spacing.'],
+ ['govuk-main-wrapper--s', '.ofh-main-wrapper--s', 'Add to .ofh-main-wrapper for tighter transactional spacing.'],
+ ];
+
+ return mixinNames.map(([mixinName, className, note]) => {
+ const blockMatch = content.match(new RegExp(`@mixin ${mixinName}\\s*\\{([\\s\\S]*?)\\}`, 'u'));
+ const block = blockMatch ? blockMatch[1] : '';
+ const paddingCalls = [...block.matchAll(/@include ofh-responsive-padding\((\d+),\s*'([^']+)'\);/gu)];
+ const topCall = paddingCalls.find((call) => call[2] === 'top');
+ const bottomCall = paddingCalls.find((call) => call[2] === 'bottom');
+
+ return {
+ className,
+ top: formatSpacingTriplet(topCall ? verticalScale[topCall[1]] : null),
+ bottom: formatSpacingTriplet(bottomCall ? verticalScale[bottomCall[1]] : null),
+ note,
+ };
+ });
+}
+
+function formatTriplet(values) {
+ return `D ${values.desktop} / T ${values.tablet} / M ${values.mobile}`;
+}
+
+function formatSpacingTriplet(scaleEntry) {
+ if (!scaleEntry) {
+ return 'Inherits base spacing';
+ }
+
+ return formatTriplet(toBreakpointSummary(scaleEntry));
+}
+
+function buildIconData(registry) {
+ const iconScale = getTokenValue('$ofh-iconography-responsive-scale', registry);
+ const sizeScale = Object.keys(iconScale)
+ .map((size) => Number.parseInt(size, 10))
+ .sort((first, second) => first - second)
+ .map((size) => {
+ const values = toBreakpointSummary(iconScale[size]);
+
+ return {
+ size,
+ iconName: 'Search',
+ mobile: values.mobile,
+ tablet: values.tablet,
+ desktop: values.desktop,
+ fixedClass: `.ofh-icon--${size}`,
+ responsiveMixin: `@include ofh-iconography-responsive(${size})`,
+ notes: size === 32 ? 'The responsive 32 token collapses to 24px on mobile and tablet.' : 'Fixed classes stay at this size at every breakpoint.',
+ };
+ });
+
+ return {
+ sizeScale,
+ };
+}
+
+function buildColourGroups(groups, registry) {
+ return groups.map((group) => ({
+ title: group.title,
+ items: group.items.map((token) => {
+ const value = getTokenValue(token, registry);
+ const hex = toDisplayHex(value);
+
+ return {
+ token,
+ sourceToken: directSourceToken(token, registry),
+ hex,
+ useBorder: needsSwatchBorder(hex),
+ };
+ }),
+ }));
+}
+
+function buildColourData(registry) {
+ return {
+ semanticGroups: buildColourGroups(semanticColourGroups, registry),
+ paletteGroups: buildColourGroups(paletteColourGroups, registry),
+ };
+}
+
+function buildRegistry() {
+ return {
+ ...parseVariables(files.tokensCore),
+ ...parseVariables(files.tokensBreakpoint),
+ ...parseVariables(files.tokensStatic),
+ ...parseVariables(files.breakpoints),
+ ...parseVariables(files.grid),
+ ...parseVariables(files.spacingTool),
+ ...parseVariables(files.typographyUtilities),
+ };
+}
+
+function buildFoundationStyles() {
+ const registry = buildRegistry();
+
+ return {
+ typography: buildTypographyData(registry),
+ spacing: buildSpacingData(registry),
+ layout: buildLayoutData(registry),
+ icons: buildIconData(registry),
+ colour: buildColourData(registry),
+ indexCards,
+ };
+}
+
+module.exports = {
+ buildFoundationStyles,
+};
diff --git a/packages/site/styles/app/_app.scss b/packages/site/styles/app/_app.scss
index 61b8df72d..122b20874 100755
--- a/packages/site/styles/app/_app.scss
+++ b/packages/site/styles/app/_app.scss
@@ -8,6 +8,7 @@
@import 'example-callout';
@import 'featured-list';
@import 'footer';
+@import 'foundation-showcase';
@import 'health-az';
@import 'homepage';
@import 'images';
diff --git a/packages/site/styles/app/_code-highlight.scss b/packages/site/styles/app/_code-highlight.scss
index 7b919b270..2929f6278 100644
--- a/packages/site/styles/app/_code-highlight.scss
+++ b/packages/site/styles/app/_code-highlight.scss
@@ -4,6 +4,7 @@ p code,
td code {
background-color: $ofh-color-greyscale-6;
color: $ofh-color-feedback-error-2-main;
+ font-size: 1rem;
padding: 2px $ofh-size-8;
word-break: break-word;
}
diff --git a/packages/site/styles/app/_colour-swatch.scss b/packages/site/styles/app/_colour-swatch.scss
index ef9640b23..dddc4d48e 100644
--- a/packages/site/styles/app/_colour-swatch.scss
+++ b/packages/site/styles/app/_colour-swatch.scss
@@ -3,23 +3,12 @@
========================================================================== */
/**
- * Custom table for the colour palette
+ * Colour table enhancements
*/
.app-colour-list {
margin-top: 0;
-
- th,
- td {
- @include ofh-typography-responsive('paragraph-sm');
-
- border: 0;
- }
-
- .app-colour-list__column--name code,
- .app-colour-list__column--colour code {
- white-space: nowrap;
- }
+ width: 100%;
}
.app-colour-list__swatch {
@@ -27,44 +16,31 @@
border: 1px solid transparent;
border-radius: 50%;
display: inline-block;
+ flex: 0 0 auto;
height: $ofh-size-32;
- margin-right: $ofh-size-4;
- vertical-align: middle;
width: $ofh-size-32;
+}
- @include mq($until: tablet) {
- height: $ofh-size-24;
- left: 0;
- position: absolute;
- width: $ofh-size-24;
- }
+.app-colour-list__token {
+ align-items: center;
+ display: flex;
+ gap: $ofh-size-8;
}
-.app-colour-list__column {
+.app-colour-list__token-cell,
+.app-colour-list__source,
+.app-colour-list__hex {
vertical-align: middle;
-
- @include mq($until: tablet) {
- display: block;
- position: relative;
- }
}
-.app-colour-list__row {
- .app-colour-list__column {
- @include ofh-responsive-padding(8);
-
- // Use a dedicated row tint so light swatches (for example #e8e8e8 and #ffffff)
- // remain distinguishable against the documentation surface.
- background-color: $ofh-color-background-secondary-grey;
- }
-
- .app-colour-list__column--colour code {
- background-color: transparent;
- }
+.app-colour-list__token-cell code,
+.app-colour-list__source code {
+ overflow-wrap: anywhere;
+ white-space: normal;
}
-.app-colour-list__column--name {
- font-weight: normal;
+.app-colour-list__hex code {
+ white-space: nowrap;
}
.app-colour-list__swatch--border {
diff --git a/packages/site/styles/app/_container.scss b/packages/site/styles/app/_container.scss
index c5870bcea..250a5dfb1 100644
--- a/packages/site/styles/app/_container.scss
+++ b/packages/site/styles/app/_container.scss
@@ -3,7 +3,7 @@
========================================================================== */
// Container width variable
-$app-container-size: 1100px;
+$app-container-size: 1280px;
.app-width-container,
.app-breadcrumb .ofh-width-container {
diff --git a/packages/site/styles/app/_foundation-showcase.scss b/packages/site/styles/app/_foundation-showcase.scss
new file mode 100644
index 000000000..4abc712f9
--- /dev/null
+++ b/packages/site/styles/app/_foundation-showcase.scss
@@ -0,0 +1,145 @@
+/* ==========================================================================
+ #FOUNDATION SHOWCASE
+ ========================================================================== */
+
+.app-foundation-showcase__table-wrap {
+ margin-bottom: $ofh-size-32;
+ overflow-x: auto;
+}
+
+.app-foundation-showcase__caption {
+ margin-bottom: $ofh-size-8;
+}
+
+.app-foundation-showcase__table {
+ max-width: none;
+ min-width: max(100%, 680px);
+ width: max-content;
+
+ code {
+ white-space: nowrap;
+ }
+}
+
+.app-component-reading-width .app-foundation-showcase__table {
+ max-width: none;
+}
+
+.app-foundation-showcase__row-heading {
+ min-width: 140px;
+}
+
+.app-foundation-showcase__preview-cell {
+ min-width: 240px;
+}
+
+.app-foundation-showcase__notes-header,
+.app-foundation-showcase__notes-cell {
+ min-width: 200px;
+}
+
+.app-foundation-showcase__line-height-header,
+.app-foundation-showcase__line-height-cell {
+ min-width: 160px;
+}
+
+.app-foundation-showcase__preview {
+ display: block;
+ margin-bottom: 0 !important; /* stylelint-disable-line declaration-no-important */
+ margin-top: 0 !important; /* stylelint-disable-line declaration-no-important */
+}
+
+.app-foundation-showcase__preview--html > * {
+ margin-bottom: 0 !important; /* stylelint-disable-line declaration-no-important */
+ margin-top: 0 !important; /* stylelint-disable-line declaration-no-important */
+}
+
+.app-foundation-showcase__preview--icon {
+ align-items: center;
+ display: inline-flex;
+}
+
+.app-foundation-showcase__inline-list {
+ display: inline-block;
+ padding-left: 20px;
+}
+
+.app-foundation-showcase__spacing-bars {
+ align-items: flex-end;
+ display: flex;
+ gap: $ofh-size-8;
+ min-height: 72px;
+}
+
+.app-foundation-showcase__spacing-bar {
+ border-radius: 999px;
+ display: inline-block;
+ width: 14px;
+}
+
+.app-foundation-showcase__spacing-bar--mobile {
+ background-color: $ofh-color-brand-blue-royal-3-main;
+}
+
+.app-foundation-showcase__spacing-bar--tablet {
+ background-color: $ofh-color-brand-blue-aqua-2;
+}
+
+.app-foundation-showcase__spacing-bar--desktop {
+ background-color: $ofh-color-brand-green-teal-2;
+}
+
+.app-foundation-showcase__width-track {
+ background-color: $ofh-color-background-secondary-grey;
+ border-radius: $ofh-radius-24;
+ max-width: 320px;
+ min-width: 180px;
+ overflow: hidden;
+}
+
+.app-foundation-showcase__width-fill {
+ background: linear-gradient(
+ 90deg,
+ $ofh-color-brand-blue-royal-2 0%,
+ $ofh-color-brand-blue-aqua-2 100%
+ );
+ display: block;
+ height: $ofh-size-24;
+}
+
+.app-foundation-showcase__colour-group + .app-foundation-showcase__colour-group {
+ margin-top: $ofh-size-48;
+}
+
+.app-foundation-showcase__cards {
+ display: grid;
+ gap: $ofh-size-24;
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.app-foundation-showcase__cards > li {
+ height: 100%;
+}
+
+.app-foundation-showcase__landing-card {
+ height: 100%;
+}
+
+.app-foundation-showcase__example-heading {
+ margin-top: $ofh-size-48;
+}
+
+.app-foundation-showcase__example-section + .app-foundation-showcase__example-section {
+ margin-top: $ofh-size-40;
+}
+
+.app-foundation-showcase__icon-gallery-table {
+ table-layout: fixed;
+}
+
+.app-foundation-showcase__icon-gallery-size-col {
+ width: 96px;
+}
diff --git a/packages/site/styles/design-example-overrides.scss b/packages/site/styles/design-example-overrides.scss
index df2711a48..f62b4455f 100644
--- a/packages/site/styles/design-example-overrides.scss
+++ b/packages/site/styles/design-example-overrides.scss
@@ -55,3 +55,10 @@ body {
padding: 20px;
text-align: center;
}
+
+.app-focus-state__button-row {
+ align-items: flex-start;
+ display: flex;
+ flex-wrap: wrap;
+ gap: $ofh-size-16 $ofh-size-24;
+}
diff --git a/packages/site/views/_data/foundationStyles.js b/packages/site/views/_data/foundationStyles.js
new file mode 100644
index 000000000..90aa50f22
--- /dev/null
+++ b/packages/site/views/_data/foundationStyles.js
@@ -0,0 +1,7 @@
+const {
+ buildFoundationStyles,
+} = require('../../scripts/foundation-styles-data');
+
+module.exports = function foundationStylesData() {
+ return buildFoundationStyles();
+};
diff --git a/packages/site/views/design-system/styles/_partials/colour-group.njk b/packages/site/views/design-system/styles/_partials/colour-group.njk
new file mode 100644
index 000000000..faafec4d1
--- /dev/null
+++ b/packages/site/views/design-system/styles/_partials/colour-group.njk
@@ -0,0 +1,32 @@
+
+ {{ group.title }}
+
+
+
+
+
+
+
+
+
+
+ {% for item in group.items %}
+
+
+
+
+ {{ item.token }}
+
+ |
+
+ {{ item.sourceToken }}
+ |
+
+ {{ item.hex }}
+ |
+
+ {% endfor %}
+
+
+
+
diff --git a/packages/site/views/design-system/styles/_partials/showcase-matrix.njk b/packages/site/views/design-system/styles/_partials/showcase-matrix.njk
new file mode 100644
index 000000000..75fcebd26
--- /dev/null
+++ b/packages/site/views/design-system/styles/_partials/showcase-matrix.njk
@@ -0,0 +1,77 @@
+{% from "icon/macro.njk" import icon %}
+
+
+
+ {% if caption %}
+ {{ caption }}
+ {% endif %}
+
+
+
+
+ {% if showClassName %}
+
+ {% endif %}
+ {% if showToken %}
+
+ {% endif %}
+
+
+
+ {% if showLineHeight %}
+
+ {% endif %}
+ {% if showFixedClass %}
+
+ {% endif %}
+ {% if showResponsiveMixin %}
+
+ {% endif %}
+ {% if showNotes %}
+
+ {% endif %}
+
+
+
+ {% for row in rows %}
+
+
+ |
+ {% if row.previewHtml %}
+ {{ row.previewHtml | safe }}
+ {% elseif row.iconName %}
+
+ {{ icon({ "name": row.iconName, "size": row.size, "title": row.iconName }) }}
+
+ {% else %}
+
+ {{ row.previewText }}
+
+ {% endif %}
+ |
+ {% if showClassName %}
+ {{ row.className }} |
+ {% endif %}
+ {% if showToken %}
+ {{ row.token }} |
+ {% endif %}
+ {{ row.desktop }} |
+ {{ row.tablet }} |
+ {{ row.mobile }} |
+ {% if showLineHeight %}
+ {{ row.lineHeight }} |
+ {% endif %}
+ {% if showFixedClass %}
+ {{ row.fixedClass }} |
+ {% endif %}
+ {% if showResponsiveMixin %}
+ {{ row.responsiveMixin }} |
+ {% endif %}
+ {% if showNotes %}
+ {{ row.notes }} |
+ {% endif %}
+
+ {% endfor %}
+
+
+
diff --git a/packages/site/views/design-system/styles/_partials/showcase-scale.njk b/packages/site/views/design-system/styles/_partials/showcase-scale.njk
new file mode 100644
index 000000000..514ab9de6
--- /dev/null
+++ b/packages/site/views/design-system/styles/_partials/showcase-scale.njk
@@ -0,0 +1,61 @@
+
+
+ {% if caption %}
+ {{ caption }}
+ {% endif %}
+
+
+ {% if variant == "spacing" %}
+
+
+
+
+
+
+
+ {% else %}
+
+
+
+
+ {% if showNotes is not defined or showNotes %}
+
+ {% endif %}
+ {% endif %}
+
+
+
+ {% for item in items %}
+
+ {% if variant == "spacing" %}
+
+ |
+
+
+
+
+
+ |
+ {{ item.desktop }} |
+ {{ item.tablet }} |
+ {{ item.mobile }} |
+ {{ item.staticToken }} |
+ {{ item.utilityExample }} |
+ {% else %}
+
+
+
+
+
+ |
+ {{ item.value }} |
+ {{ item.className }} |
+ {% if showNotes is not defined or showNotes %}
+ {{ item.note }} |
+ {% endif %}
+ {% endif %}
+
+ {% endfor %}
+
+
+
diff --git a/packages/site/views/design-system/styles/colour/index.njk b/packages/site/views/design-system/styles/colour/index.njk
index 35dfa60ab..96bd41eed 100644
--- a/packages/site/views/design-system/styles/colour/index.njk
+++ b/packages/site/views/design-system/styles/colour/index.njk
@@ -4,6 +4,7 @@
{% set subSection = "Foundation styles" %}
{% set dateUpdated = "March 2026" %}
{% set backlog_issue_id = "2" %}
+{% set hideContact = "true" %}
{% extends "app-layout.njk" %}
@@ -30,7 +31,7 @@
@@ -39,9 +40,7 @@
Icons
- Layout
-
-
+ Layout
Page template
@@ -54,916 +53,51 @@
{% endblock %}
-
-
-
-
{% block bodyContent %}
Using colour
Our colours help people to recognise our services.
- We also use colour to help users prioritise and differentiate information - for example we use:
+ We also use colour to help users prioritise and differentiate information. For example we use:
Main colours
- Use the Sass variables rather than the hexadecimal (hex) colour values. For example, use $ofh-color-foreground-primary rather than #1B1B1B. This means that your product will always use the most recent colour palette whenever you update.
-
- Only use the Sass variables in the context they are designed for. If you want to use a colour for something else, use the extended colours. For example, if you wanted to use red in a graph you should use $ofh-color-brand-red-3-main rather than $ofh-color-feedback-error-2-main.
-
-
-
-
-
-
- Text
-
- |
-
-
-
-
- $ofh-color-foreground-primary
- |
-
- #1B1B1B
- |
-
-
-
-
- $ofh-color-foreground-secondary
- |
-
- #494949
- |
-
-
-
-
- $ofh-color-foreground-brand-blue-navy
- |
-
- #011D4B
- |
-
-
-
-
- $ofh-color-foreground-primary-inverted
- |
-
- #FFFFFF
- |
-
-
-
-
-
- Links
-
- |
-
-
-
-
- $ofh-color-foreground-link-default
- |
-
- #0053B3
- |
-
-
-
-
- $ofh-color-foreground-link-hover
- |
-
- #002D61
- |
-
-
-
-
- $ofh-color-foreground-link-visited
- |
-
- #330072
- |
-
-
-
-
- $ofh-color-foreground-link-active
- |
-
- #002D61
- |
-
-
-
-
-
- Breadcrumb navigation links
-
- |
-
-
-
-
- $ofh-color-foreground-breadcrumb-default
- |
-
- #011D4B
- |
-
-
-
-
- $ofh-color-foreground-breadcrumb-hover
- |
-
- #20354B
- |
-
-
-
-
- $ofh-color-foreground-breadcrumb-visited
- |
-
- #330072
- |
-
-
-
-
- $ofh-color-foreground-breadcrumb-active
- |
-
- #0C68A9
- |
-
-
-
-
-
- Focus state
-
- |
-
-
-
-
- $ofh-color-border-feedback-focus
- |
-
- #0053B3
- |
-
-
-
-
- $ofh-color-foreground-brand-blue-navy
- |
-
- #011D4B
- |
-
-
-
-
-
- Button
-
- |
-
-
-
-
- $ofh-color-background-button-default
- |
-
- #0053B3
- |
-
-
-
-
- $ofh-color-background-button-hover
- |
-
- #002D61
- |
-
-
-
-
- $ofh-color-background-button-active
- |
-
- #002D61
- |
-
-
-
-
- $ofh-color-background-button-default-inverted
- |
-
- #FFFFFF
- |
-
-
-
-
- $ofh-color-background-button-hover-inverted
- |
-
- #E8E8E8
- |
-
-
-
-
- $ofh-color-background-button-active-inverted
- |
-
- #D1D1D1
- |
-
-
-
-
-
-
- Contextual colours
-
- |
-
-
-
-
- $ofh-color-feedback-error-2-main
- |
-
- #B60000
- |
-
-
-
-
- $ofh-color-feedback-success-2-main
- |
-
- #00725F
- |
-
-
-
-
-
- Borders
-
- |
-
-
-
-
- $ofh-color-border-primary
- |
-
- #D1D1D1
- |
-
-
-
-
-
- Input fields
-
- |
-
-
-
-
- $ofh-color-border-input-default
- |
-
- #494949
- |
-
-
-
-
- $ofh-color-border-input-active
- |
-
- #1B1B1B
- |
-
-
-
-
- $ofh-color-background-primary
- |
-
- #FFFFFF
- |
-
-
-
-
- Page background colour
- The default page background is $ofh-color-background-primary (#FFFFFF).
- Use background tint tokens such as $ofh-color-background-secondary, $ofh-color-background-secondary-blue, $ofh-color-background-secondary-yellow and $ofh-color-background-secondary-grey to create contrast between sections and content blocks.
+ Use the semantic Sass variables rather than hardcoded hexadecimal values. For example, use $ofh-color-foreground-primary rather than #1B1B1B.
+ Only use semantic variables in the context they are designed for. The source token column shows which core palette token the semantic alias resolves to today, but you should still use the semantic name in your product code.
- Colour palette
- Avoid using the palette colours if there is a Sass variable that is designed for your context. For example, if you are styling the error state of a component you should use the $ofh-color-feedback-error-2-main Sass variable rather than $ofh-color-brand-red-3-main.
+ {% for group in foundationStyles.colour.semanticGroups %}
+ {% include "design-system/styles/_partials/colour-group.njk" %}
+ {% endfor %}
-
-
+ Core colours
+ Use the core palette only when a semantic token is not designed for your context, for example charts, illustrations, or more bespoke editorial surfaces.
+ If you are styling a product UI state like an error, link, focus ring, or button, prefer the semantic token instead of the palette token that happens to resolve to the same hex value.
-
-
-
- $ofh-color-brand-blue-navy-3-main
- |
-
- #011D4B
- |
-
-
-
-
-
- $ofh-color-brand-blue-royal-3-main
- |
-
- #108BE2
- |
-
-
-
-
-
- $ofh-color-brand-blue-aqua-3-main
- |
-
- #A4E7E2
- |
-
-
-
-
-
- $ofh-color-brand-green-teal-3-main
- |
-
- #00A08A
- |
-
-
-
-
-
- $ofh-color-brand-green-lime-3-main
- |
-
- #A2F5AC
- |
-
-
-
-
-
- $ofh-color-brand-yellow-3-main
- |
-
- #FFC62C
- |
-
-
-
-
-
- $ofh-color-brand-orange-3-main
- |
-
- #FF7800
- |
-
-
-
-
-
- $ofh-color-brand-red-3-main
- |
-
- #F34848
- |
-
-
-
-
-
- $ofh-color-greyscale-black
- |
-
- #000000
- |
-
-
-
-
- $ofh-color-greyscale-1
- |
-
- #1B1B1B
- |
-
-
-
-
- $ofh-color-greyscale-2
- |
-
- #494949
- |
-
-
-
-
- $ofh-color-greyscale-3
- |
-
- #767676
- |
-
-
-
-
- $ofh-color-greyscale-4
- |
-
- #A4A4A4
- |
-
-
-
-
- $ofh-color-greyscale-5
- |
-
- #D1D1D1
- |
-
-
-
-
- $ofh-color-greyscale-6
- |
-
- #E8E8E8
- |
-
-
-
-
- $ofh-color-greyscale-7
- |
-
- #F4F4F4
- |
-
-
-
-
- $ofh-color-greyscale-white
- |
-
- #FFFFFF
- |
-
-
-
-
- Extended colours
- Our extended colour palette is derived from our core brand colour palette. You can use these colours for graphical elements like illustrations, graphs and charts. Your elements will then fit with the other Design System components and patterns and will help people to recognise our services.
- Avoid using the extended palette colours for components like text, links and input fields where there is a Sass variable that is designed for your component or context.
-
-
-
-
-
-
- $ofh-color-brand-blue-navy-1
- |
-
- #000E26
- |
-
-
-
-
- $ofh-color-brand-blue-navy-2
- |
-
- #011638
- |
-
-
-
-
- $ofh-color-brand-blue-navy-4
- |
-
- #405578
- |
-
-
-
-
- $ofh-color-brand-blue-navy-5
- |
-
- #808EA5
- |
-
-
-
-
- $ofh-color-brand-blue-navy-6
- |
-
- #C3C9D2
- |
-
-
-
-
- $ofh-color-brand-blue-royal-1
- |
-
- #084571
- |
-
-
-
-
- $ofh-color-brand-blue-royal-2
- |
-
- #0C68A9
- |
-
-
-
-
- $ofh-color-brand-blue-royal-4
- |
-
- #4CA8E9
- |
-
-
-
-
- $ofh-color-brand-blue-royal-5
- |
-
- #87C5F1
- |
-
-
-
-
- $ofh-color-brand-blue-royal-6
- |
-
- #C6DFF0
- |
-
-
-
-
- $ofh-color-brand-blue-aqua-1
- |
-
- #527371
- |
-
-
-
-
- $ofh-color-brand-blue-aqua-2
- |
-
- #7BADA9
- |
-
-
-
-
- $ofh-color-brand-blue-aqua-4
- |
-
- #BBEDE9
- |
-
-
-
-
- $ofh-color-brand-blue-aqua-5
- |
-
- #D1F3F1
- |
-
-
-
-
- $ofh-color-brand-blue-aqua-6
- |
-
- #E4F1F0
- |
-
-
-
-
- $ofh-color-brand-green-teal-1
- |
-
- #005045
- |
-
-
-
-
- $ofh-color-brand-green-teal-2
- |
-
- #007867
- |
-
-
-
-
- $ofh-color-brand-green-teal-4
- |
-
- #40B8A8
- |
-
-
-
-
- $ofh-color-brand-green-teal-5
- |
-
- #80CFC4
- |
-
-
-
-
- $ofh-color-brand-green-teal-6
- |
-
- #C7EAE5
- |
-
-
-
-
- $ofh-color-brand-green-lime-1
- |
-
- #517B56
- |
-
-
-
-
- $ofh-color-brand-green-lime-2
- |
-
- #7AB881
- |
-
-
-
-
- $ofh-color-brand-green-lime-4
- |
-
- #B9F8C1
- |
-
-
-
-
- $ofh-color-brand-green-lime-5
- |
-
- #D0FAD5
- |
-
-
-
-
- $ofh-color-brand-green-lime-6
- |
-
- #E3F4E5
- |
-
-
-
-
- $ofh-color-brand-yellow-1
- |
-
- #806316
- |
-
-
-
-
- $ofh-color-brand-yellow-2
- |
-
- #BF9421
- |
-
-
-
-
- $ofh-color-brand-yellow-4
- |
-
- #FFD461
- |
-
-
-
-
- $ofh-color-brand-yellow-5
- |
-
- #FFE296
- |
-
-
-
-
- $ofh-color-brand-yellow-6
- |
-
- #FFEFC4
- |
-
-
-
-
- $ofh-color-brand-orange-1
- |
-
- #803C00
- |
-
-
-
-
- $ofh-color-brand-orange-2
- |
-
- #BF5A00
- |
-
-
-
-
- $ofh-color-brand-orange-4
- |
-
- #FF9A40
- |
-
-
-
-
- $ofh-color-brand-orange-5
- |
-
- #FFBC80
- |
-
-
-
-
- $ofh-color-brand-orange-6
- |
-
- #F6DBC3
- |
-
-
-
-
- $ofh-color-brand-red-1
- |
-
- #7A2424
- |
-
-
-
-
- $ofh-color-brand-red-2
- |
-
- #B63636
- |
-
-
-
-
- $ofh-color-brand-red-4
- |
-
- #F67676
- |
-
-
-
-
- $ofh-color-brand-red-5
- |
-
- #F9A3A3
- |
-
-
-
-
- $ofh-color-brand-red-6
- |
-
- #F4D1D1
- |
-
-
-
-
- $ofh-color-backgrounds-blue
- |
-
- #F2F5FA
- |
-
-
-
-
- $ofh-color-backgrounds-yellow
- |
-
- #F8F5EF
- |
-
-
-
-
- $ofh-color-backgrounds-grey
- |
-
- #F8F8F8
- |
-
-
-
+ {% for group in foundationStyles.colour.paletteGroups %}
+ {% include "design-system/styles/_partials/colour-group.njk" %}
+ {% endfor %}
Accessibility
- Do not rely on colour to convey meaning, for example, an instruction. To communicate with people who are visually impaired or have difficulty telling the difference between colours, you may need to:
-
- - word things differently
- - use more than one visual cue, for example, text and an icon as well as colour
-
-
+ Do not rely on colour alone to convey meaning, for example in an instruction or status message.
+ To communicate with people who are visually impaired or have difficulty telling the difference between colours, you may need to:
+
+ - word things differently
+ - use more than one visual cue, for example text and an icon as well as colour
+
+
Colour contrast
You must make sure that the contrast ratio of text and interactive elements and components like buttons meet the contrast minimum for AA of the Web Content Accessibility Guidelines (WCAG 2.1).
- This helps people with colour vision deficiency (colour blindness). They find it difficult to tell the difference between certain colours, often shades of red, yellow and green.
+ This helps people with colour vision deficiency, colour blindness. They find it difficult to tell the difference between certain colours, often shades of red, yellow, and green.
AA
The contrast ratio should be at least:
- - 4.5:1 for small text - smaller than 24px, or 19px if bold
- - 3:1 for large text - larger than 24px, or 19px if bold
+ - 4.5:1 for small text, smaller than 24px, or 19px if bold
+ - 3:1 for large text, larger than 24px, or 19px if bold
- 3:1 for graphic elements and components
@@ -972,7 +106,7 @@
Test colour contrast with people of all abilities.
diff --git a/packages/site/views/design-system/styles/focus-state/buttons-inverted/index.njk b/packages/site/views/design-system/styles/focus-state/buttons-inverted/index.njk
new file mode 100644
index 000000000..52c6f3d5c
--- /dev/null
+++ b/packages/site/views/design-system/styles/focus-state/buttons-inverted/index.njk
@@ -0,0 +1,20 @@
+{% from 'button/macro.njk' import button %}
+
+
+
+ {{ button({
+ "text": "Find my location",
+ "type": "button",
+ "classes": "ofh-button--ghost-inverted",
+ "attributes": {
+ "id": "focus-state-button-inverted-target"
+ }
+ }) }}
+
+ {{ button({
+ "text": "Cancel",
+ "type": "button",
+ "classes": "ofh-button--text-inverted"
+ }) }}
+
+
diff --git a/packages/site/views/design-system/styles/focus-state/buttons/index.njk b/packages/site/views/design-system/styles/focus-state/buttons/index.njk
new file mode 100644
index 000000000..52243ae1d
--- /dev/null
+++ b/packages/site/views/design-system/styles/focus-state/buttons/index.njk
@@ -0,0 +1,33 @@
+{% from 'button/macro.njk' import button %}
+
+
+
+
+ {{ button({
+ "text": "Save and continue",
+ "type": "button",
+ "classes": "ofh-button--contained",
+ "attributes": {
+ "id": "focus-state-button-target"
+ }
+ }) }}
+
+ {{ button({
+ "text": "Review answers",
+ "type": "button",
+ "classes": "ofh-button--outlined"
+ }) }}
+ {{ button({
+ "text": "Skip for now",
+ "type": "button",
+ "classes": "ofh-button--ghost"
+ }) }}
+
+ {{ button({
+ "text": "Cancel",
+ "type": "button",
+ "classes": "ofh-button--text"
+ }) }}
+
+
+
diff --git a/packages/site/views/design-system/styles/focus-state/choices/index.njk b/packages/site/views/design-system/styles/focus-state/choices/index.njk
new file mode 100644
index 000000000..31c98d525
--- /dev/null
+++ b/packages/site/views/design-system/styles/focus-state/choices/index.njk
@@ -0,0 +1,50 @@
+{% from 'radios/macro.njk' import radios %}
+{% from 'checkboxes/macro.njk' import checkboxes %}
+
+
+
+
+
+
diff --git a/packages/site/views/design-system/styles/focus-state/index.njk b/packages/site/views/design-system/styles/focus-state/index.njk
index 325c47d4c..bb508bb84 100644
--- a/packages/site/views/design-system/styles/focus-state/index.njk
+++ b/packages/site/views/design-system/styles/focus-state/index.njk
@@ -1,9 +1,10 @@
{% set pageTitle = "Focus state" %}
-{% set pageDescription = "Use these focus state styles to let users know which element they’re on and that they can interact with it." %}
+{% set pageDescription = "Current focus treatments for links, form controls and buttons in the OFH toolkit." %}
{% set pageSection = "Design system" %}
{% set subSection = "Foundation styles" %}
-{% set dateUpdated = "October 2019" %}
+{% set dateUpdated = "March 2026" %}
{% set backlog_issue_id = "193" %}
+{% set hideContact = "true" %}
{% extends "app-layout.njk" %}
@@ -14,53 +15,198 @@
{% block bodyContent %}
What is a focus state?
- Some people use keyboards or other devices to navigate through a page by jumping from 1 interactive element to the next. The focus state lets users know which element they’re currently on and that it's ready for them to interact with.
+ Focus states show people which interactive element is currently active when they move through a page using a keyboard or another assistive technology.
+ The toolkit applies focus styles through shared mixins and tokens so links, form controls and buttons behave consistently.
- Our focus state styles
- We've followed the GOV.UK Design System's approach to focus state styles.
- Like GOV.UK, we use a combination of yellow and black to make sure we meet the Web Content Accessibility Guidelines (WCAG) 2.1 level AA non-text contrast on any background colour on the NHS website.
- The yellow has a high contrast with dark backgrounds and the thick black border has a high contrast against light backgrounds.
+ Current focus tokens
+ Our default focus ring uses $ofh-color-border-feedback-focus, which resolves to the current interactive blue token. On dark surfaces, inverted button variants use $ofh-color-border-feedback-focus-inverted, which resolves to white.
+ This page replaces the older yellow-and-black guidance that no longer matches the current OFH toolkit.
- Link focus state style
- When links are focused, they have a yellow background with a black bottom border. This helps the focused link stand out from the rest of the content on the page.
-
+ Focus treatments in the toolkit
+ {{ table({
+ responsive: true,
+ panel: false,
+ caption: "Focus treatments in the OFH toolkit",
+ head: [
+ {
+ text: "Mixin or token"
+ },
+ {
+ text: "Use for"
+ },
+ {
+ text: "Current treatment"
+ }
+ ],
+ rows: [
+ [
+ {
+ header: "Mixin or token",
+ html: "ofh-focused-text"
+ },
+ {
+ header: "Use for",
+ text: "Links, action links, details summaries and similar text-led controls"
+ },
+ {
+ header: "Current treatment",
+ text: "3px focus outline using the default focus token with a 1px offset"
+ }
+ ],
+ [
+ {
+ header: "Mixin or token",
+ html: "ofh-focused-input"
+ },
+ {
+ header: "Use for",
+ text: "Inputs, selects and textareas"
+ },
+ {
+ header: "Current treatment",
+ text: "Active input border plus a 4px focus outline offset by 4px"
+ }
+ ],
+ [
+ {
+ header: "Mixin or token",
+ html: "ofh-focused-radio"
+ },
+ {
+ header: "Use for",
+ text: "Radio controls"
+ },
+ {
+ header: "Current treatment",
+ text: "Active control border plus a 4px focus outline offset by 4px"
+ }
+ ],
+ [
+ {
+ header: "Mixin or token",
+ html: "ofh-focused-checkbox"
+ },
+ {
+ header: "Use for",
+ text: "Checkbox controls"
+ },
+ {
+ header: "Current treatment",
+ text: "Active control border plus a 4px focus outline offset by 4px"
+ }
+ ],
+ [
+ {
+ header: "Mixin or token",
+ html: "ofh-focused-button"
+ },
+ {
+ header: "Use for",
+ text: "Buttons and button-like interactive controls on light surfaces"
+ },
+ {
+ header: "Current treatment",
+ text: "4px default focus outline offset by 4px"
+ }
+ ],
+ [
+ {
+ header: "Mixin or token",
+ html: "ofh-focused-button-inverted"
+ },
+ {
+ header: "Use for",
+ text: "Inverted button variants on dark surfaces"
+ },
+ {
+ header: "Current treatment",
+ text: "4px inverted focus outline offset by 4px"
+ }
+ ]
+ ]
+ }) }}
-
- When form inputs are focused, they have a yellow outline and a thick black border. If the element already has a border, the border gets thicker.
-
- Radios and checkboxes use the same style.
-
+ Live examples
+ Click into an example, then use Tab inside it to move through the interactive elements and inspect the focus treatment.
- Making focus states accessible for extended and modified components
- If you’ve extended or modified components in the NHS digital service manual, you can use service manual styles to make the focus states of these components accessible.
- How you make focus states accessible depends on whether the component is:
-
- - focusable text without a background colour or border
- - another focusable element with a background colour or border
-
+
+ Text-based interactive elements
+ Use ofh-focused-text for body links, action links, details summaries, pagination links and similar text-led controls.
+ {{ designExample({
+ group: "styles",
+ item: "focus-state",
+ itemTitle: "focus state",
+ type: "text",
+ showCode: false
+ }) }}
+
+
+
+ Inputs, selects and textareas
+ Use ofh-focused-input for input-based controls. The toolkit combines an active input border with the default focus outline so the control remains visible on light surfaces.
+ {{ designExample({
+ group: "styles",
+ item: "focus-state",
+ itemTitle: "focus state",
+ type: "inputs",
+ showCode: false
+ }) }}
+
- Make focusable text accessible
- If you use Sass, you should include the ofh-focused-text mixin in your component's :focus selector if that component is focusable text. For example, if the component is a link in body text, or the details component:
-.app-component:focus {
- @include ofh-focused-text;
-}
+
+ Radios and checkboxes
+ Use ofh-focused-radio and ofh-focused-checkbox for selection controls rather than rebuilding the ring and border treatment manually.
+ {{ designExample({
+ group: "styles",
+ item: "focus-state",
+ itemTitle: "focus state",
+ type: "choices",
+ showCode: false
+ }) }}
+
- Make other focusable elements accessible
- If you use Sass, you can use 3 NHS.UK frontend variables if your component has a background colour or border. For example, a text input or checkbox.
- The 3 Sass variables are:
+
+ Buttons on light surfaces
+ Use ofh-focused-button for the standard button variants. This is also the right treatment for other button-like controls on light surfaces.
+ {{ designExample({
+ group: "styles",
+ item: "focus-state",
+ itemTitle: "focus state",
+ type: "buttons",
+ showCode: false
+ }) }}
+
+
+
+ Inverted buttons on dark surfaces
+ Use ofh-focused-button-inverted for inverted button variants. These controls use the inverted focus token so the ring remains visible on dark backgrounds.
+ {{ designExample({
+ group: "styles",
+ item: "focus-state",
+ itemTitle: "focus state",
+ type: "buttons-inverted",
+ showCode: false
+ }) }}
+
+
+ When building new components
+ Use the mixin that matches the control type instead of rebuilding focus styles from raw values. That keeps the focus treatment aligned with the toolkit and the current design token model.
- $ofh-color-border-feedback-focus - yellow background
- $ofh-focus-text-color - black text
- $ofh-stroke-weight-4 - for consistent width
+ - text-like controls:
ofh-focused-text
+ - inputs, selects and textareas:
ofh-focused-input
+ - radios:
ofh-focused-radio
+ - checkboxes:
ofh-focused-checkbox
+ - buttons and button-like controls on light surfaces:
ofh-focused-button
+ - inverted buttons on dark surfaces:
ofh-focused-button-inverted
- Use these variables in your components instead of numeric values for the background, text and widths.
-
- If you do not use Sass
- To make a component’s focus state accessible without using Sass, you can:
+ If you are not using Sass
+ Use the compiled CSS from the equivalent toolkit component as the source of truth. If you need to reproduce the treatment directly, use the current focus tokens from the toolkit rather than the older values previously documented on this page.
- - see how the
ofh-focused-text mixin works from the NHS.UK frontend source code
- - get the values for
$ofh-color-border-feedback-focus and $ofh-focus-text-color from the colour page
+ $ofh-color-border-feedback-focus for the default focus ring
+ $ofh-color-border-feedback-focus-inverted for inverted button focus on dark surfaces
+ $ofh-color-border-input-active for the active input border on form controls
+ $ofh-stroke-weight-4 for the standard ring width on input and button treatments
{% endblock %}
diff --git a/packages/site/views/design-system/styles/focus-state/inputs/index.njk b/packages/site/views/design-system/styles/focus-state/inputs/index.njk
new file mode 100644
index 000000000..415e33dd8
--- /dev/null
+++ b/packages/site/views/design-system/styles/focus-state/inputs/index.njk
@@ -0,0 +1,49 @@
+{% from 'input/macro.njk' import input %}
+{% from 'select/macro.njk' import select %}
+{% from 'textarea/macro.njk' import textarea %}
+
+
+
+ {{ input({
+ "label": {
+ "text": "Email address"
+ },
+ "id": "focus-state-input-target",
+ "name": "focus-state-input-target"
+ }) }}
+
+ {{ select({
+ "id": "focus-state-select",
+ "name": "focus-state-select",
+ "label": {
+ "text": "Preferred contact method"
+ },
+ "items": [
+ {
+ "value": "email",
+ "text": "Email"
+ },
+ {
+ "value": "phone",
+ "text": "Phone",
+ "selected": true
+ },
+ {
+ "value": "text",
+ "text": "Text message"
+ }
+ ]
+ }) }}
+
+ {{ textarea({
+ "name": "focus-state-textarea",
+ "id": "focus-state-textarea",
+ "label": {
+ "text": "Additional detail"
+ },
+ "hint": {
+ "text": "Use this field to describe anything else we should know."
+ }
+ }) }}
+
+
diff --git a/packages/site/views/design-system/styles/focus-state/text/index.njk b/packages/site/views/design-system/styles/focus-state/text/index.njk
new file mode 100644
index 000000000..2953216f5
--- /dev/null
+++ b/packages/site/views/design-system/styles/focus-state/text/index.njk
@@ -0,0 +1,21 @@
+{% from 'action-link/macro.njk' import actionLink %}
+{% from 'details/macro.njk' import details %}
+
+
+
+ Read our screening guidance before you continue.
+
+ {{ actionLink({
+ "text": "Start your questionnaire",
+ "href": "#"
+ }) }}
+
+ {{ details({
+ "text": "How to find your NHS number",
+ "HTML": "
+ An NHS number is a 10 digit number, like 485 777 3456.
+ You can find your NHS number on prescriptions, test results and appointment letters.
+ "
+ }) }}
+
+
diff --git a/packages/site/views/design-system/styles/icons/index.njk b/packages/site/views/design-system/styles/icons/index.njk
index 3ca952d1e..e79c83295 100644
--- a/packages/site/views/design-system/styles/icons/index.njk
+++ b/packages/site/views/design-system/styles/icons/index.njk
@@ -4,6 +4,7 @@
{% set subSection = "Foundation styles" %}
{% set dateUpdated = "March 2026" %}
{% set backlog_issue_id = "4" %}
+{% set hideContact = "true" %}
{% extends "app-layout.njk" %}
{% from "icon/macro.njk" import icon %}
@@ -24,31 +25,111 @@
Example: {% raw %}{{ icon({ "name": "Search", "size": 24 }) }}{% endraw %}
{{ materialIcons.total }} Material icons are currently available in the toolkit sprite.
+ Responsive size behaviour
+ Icons can be sized in 2 different ways. Use a fixed class when the icon should stay the same size at every breakpoint, or use the responsive mixin when the icon should follow the responsive iconography scale.
+ If you render the shared icon primitive without passing a size or adding a size class, it defaults to 24px. Some components override that base behaviour in their own CSS, so treat 24px as the icon primitive default rather than a guarantee for every component-level icon.
+ The fixed classes .ofh-icon--16, .ofh-icon--24, and .ofh-icon--32 are backwards compatible and always render at the size you pick.
+ Responsive icon sizes are opt-in and come from $ofh-iconography-responsive-scale via @include ofh-iconography-responsive(...).
+
+
+
+ Fixed icon sizes
+
+
+
+
+
+
+
+
+
+ {% for row in foundationStyles.icons.sizeScale %}
+
+
+ |
+
+ {{ icon({ "name": row.iconName, "size": row.size, "title": row.iconName }) }}
+
+ |
+ {{ row.size }}px |
+ Always {{ row.size }}px. |
+
+ {% endfor %}
+
+
+
+
+
+
+ Responsive icon scale
+
+
+
+
+
+
+
+
+
+
+
+ {% for row in foundationStyles.icons.sizeScale %}
+
+
+ |
+
+ {{ icon({ "name": row.iconName, "size": row.size, "title": row.iconName }) }}
+
+ |
+ {{ row.desktop }} |
+ {{ row.tablet }} |
+ {{ row.mobile }} |
+
+ {% if row.mobile == row.tablet and row.tablet == row.desktop %}
+ {{ row.desktop }} at every breakpoint.
+ {% elseif row.mobile == row.tablet %}
+ {{ row.desktop }} desktop, {{ row.tablet }} tablet/mobile.
+ {% else %}
+ {{ row.desktop }} desktop, {{ row.tablet }} tablet, {{ row.mobile }} mobile.
+ {% endif %}
+ |
+
+ {% endfor %}
+
+
+
+
Material icon gallery
{% for group in materialIcons.iconsByCategory %}
{{ group.category }}
-
-
+
+
+
+
+
+
+
+
+
- | Name |
- 16 |
- 24 |
- 32 |
- Default |
+
+
+
+
-
+
{% for item in group.icons %}
-
- {{ item.name }} |
- {{ icon({ "name": item.name, "size": 16 }) }} |
- {{ icon({ "name": item.name, "size": 24 }) }} |
- {{ icon({ "name": item.name, "size": 32 }) }} |
- {{ item.defaultSize }} |
+
+
+ | {{ icon({ "name": item.name, "size": 16 }) }} |
+ {{ icon({ "name": item.name, "size": 24 }) }} |
+ {{ icon({ "name": item.name, "size": 32 }) }} |
{% endfor %}
+
{% endfor %}
Legacy icon files have been removed. Use the Material icon macro and sprite for all icon usage.
@@ -66,10 +147,10 @@
- Search
- ChevronRight
- - ExpandMore and ExpandLess (for expandable sections)
+ - ExpandMore and ExpandLess for expandable sections
- Close
- Other icons needs an explanation. They include:
+ Other icons need an explanation. They include:
- action links - tell the user what the link will help them do, for example: "Find a pharmacy"
- right and left arrows for pagination - add "Previous" or "Next"
@@ -77,8 +158,7 @@
Using SVG classes for icons
- We use scalable vector graphics (SVG) for icons, rather than images such as PNG. SVG are code snippets that you can drop directly into the HTML.
- SVG icons are sharp, flexible, and load quickly - and you can control how they appear, for example their colour, with style sheets (CSS).
+ We use scalable vector graphics, SVG, for icons rather than images such as PNG. SVGs are sharp, flexible, and load quickly. You can also control how they appear with CSS.
If you're using a server side or templating language, include icons with the shared macro, for example {% raw %}{{ icon({ "name": "Search", "size": 24 }) }}{% endraw %}.
The icon macro renders consistent .ofh-icon markup that references the Material sprite symbols.
Legacy PNG fallbacks are not provided as part of this implementation.
@@ -88,7 +168,7 @@
If you have any research or experience to share, please get in touch on the NHS.UK service manual Slack workspace or email us at service-manual@nhs.net.
Research
- These icons seem to be universally recognisable. When we tested them, we found that people understood them. We tested them in context - not on their own, but in components on a full page.
+ These icons seem to be universally recognisable. When we tested them, we found that people understood them. We tested them in context, not on their own, but in components on a full page.
Read more about the benefits of SVG icons:
- CSS Tricks - a pretty good SVG icon system
diff --git a/packages/site/views/design-system/styles/index.njk b/packages/site/views/design-system/styles/index.njk
index 39935462b..f118e7e9b 100644
--- a/packages/site/views/design-system/styles/index.njk
+++ b/packages/site/views/design-system/styles/index.njk
@@ -1,9 +1,11 @@
{% set pageTitle = "Foundation styles" %}
-{% set pageDescription = "Make your service look like other Our Future Health services with these foundation styles" %}
+{% set pageDescription = "Reference the core tokens, scales and layout rules that shape Our Future Health interfaces." %}
{% set pageSection = "Design system" %}
{% set subSection = "Foundation styles" %}
{% set theme = "Design" %}
+{% set dateUpdated = "March 2026" %}
{% set hideContact = "true" %}
+{% from 'card/macro.njk' import card %}
{% extends "app-layout.njk" %}
@@ -13,6 +15,24 @@
{% block bodyContent %}
+ Use these pages to inspect the toolkit foundations that sit underneath components and patterns.
+ Where practical, the values shown here are derived from the toolkit token files so the docs stay aligned with the code.
+
+
+ {% for item in foundationStyles.indexCards %}
+ -
+ {{ card({
+ variant: 'clickable',
+ href: item.href,
+ heading: item.title,
+ headingLevel: 2,
+ description: item.summary,
+ classes: 'app-foundation-showcase__landing-card'
+ }) }}
+
+ {% endfor %}
+
+
{% include "_side-nav.njk" %}
diff --git a/packages/site/views/design-system/styles/layout/index.njk b/packages/site/views/design-system/styles/layout/index.njk
index 5195b687a..19c459392 100644
--- a/packages/site/views/design-system/styles/layout/index.njk
+++ b/packages/site/views/design-system/styles/layout/index.njk
@@ -2,8 +2,7 @@
{% set pageDescription = "Structure your page content and elements." %}
{% set pageSection = "Design system" %}
{% set subSection = "Foundation styles" %}
-{% set dateUpdated = "May 2023" %}
-
+{% set dateUpdated = "March 2026" %}
{% set backlog_issue_id = "3" %}
{% set hideContact = "true" %}
@@ -60,27 +59,51 @@
Screen size
Design for mobile first using a single-column layout and work up to wider layouts.
- The default maximum page width is 960px, but you can make it wider if your content requires it. Lines of text should be no longer than 70 to 80 characters. Any longer will make the page difficult to read.
+ The mq breakpoints define when layout rules change. They are not the same as the content width token or the reading width utility, which control how wide content is allowed to grow once a breakpoint has been reached.
Responsive breakpoints
- The responsive breakpoints are:
-
- - mobile - 320px
- - tablet - 641px
- - desktop - 769px
- - large desktop - 990px
-
+
+
+ Toolkit mq breakpoints
+
+
+
+
+
+
+
+
+
+ {% for breakpoint in foundationStyles.layout.breakpoints %}
+
+
+ {{ breakpoint.token }} |
+ {{ breakpoint.value }} |
+ {{ breakpoint.note }} |
+
+ {% endfor %}
+
+
+
Containers
To set up your layout, you will need to use a container which sets the maximum width of the content.
You can choose from either of these:
- - fixed-width container – up to 960px
- - fluid-width container – spans the entire width of the viewport
+ - fixed-width container, which uses the content max width token
+ - fluid-width container, which spans the viewport and relies on content-width utilities inside it
+ The class column shows the container or utility class to apply to the wrapper that should take on that width behaviour.
+
+ {% set items = foundationStyles.layout.widths %}
+ {% set caption = "Container and width references" %}
+ {% set variant = "width" %}
+ {% set classColumnLabel = "Use this class" %}
+ {% set showNotes = true %}
+ {% include "design-system/styles/_partials/showcase-scale.njk" %}
Fixed-width container
- Use .ofh-width-container for a container with a maximum width of 960px.
+ Use .ofh-width-container for a container with a maximum width of {{ foundationStyles.layout.widthsById.content.value }}.
{{ designExample({
group: "styles",
@@ -105,6 +128,30 @@
To add vertical margin or padding to your layout, you will need a main element with the class .ofh-main-wrapper. This gives responsive padding to the top and bottom of the page and will be the wrapper for the main content of the page.
There should be only one main element and it should have a unique id of maincontent, which allows keyboard-only users to skip to the main content on a page with the skip link component.
+
+
+ Main wrapper spacing
+
+
+
+
+
+
+
+
+
+ {% for wrapper in foundationStyles.layout.mainWrapper %}
+
+
+ | {{ wrapper.top }} |
+ {{ wrapper.bottom }} |
+ {{ wrapper.note }} |
+
+ {% endfor %}
+
+
+
+
{{ designExample({
group: "styles",
item: "layout",
@@ -113,12 +160,20 @@
showExample: false
}) }}
- The vertical padding can be made larger or smaller by using the modifier classes .ofh-main-wrapper--l or .ofh-main-wrapper--s. We recommend using smaller vertical padding on transactional services.
+ The vertical padding can be made larger or smaller by using the modifier classes .ofh-main-wrapper--l or .ofh-main-wrapper--s. We recommend using the smaller modifier on transactional services.
Grid system
- We use a grid to structure our designs and create consistent spacing. It gives us a flexible way to layout components that adapts to different device sizes.
+ We use a grid to structure our designs and create consistent spacing. It gives us a flexible way to lay out components that adapts to different device sizes.
The grid is structured with a .ofh-grid-row wrapper which acts as a row to contain your grid columns.
- You can add columns inside this wrapper to create your layout. To define your columns, add the class beginning with .ofh-grid-column- to a new container followed by the width, for example .ofh-grid-column-one-third, to make it the width you want.
+ You can add columns inside this wrapper to create your layout. To define your columns, add the class beginning with .ofh-grid-column- to a new container followed by the width, for example .ofh-grid-column-one-third.
+ The grid class column shows the class to apply to a child of .ofh-grid-row to set that column width.
+
+ {% set items = foundationStyles.layout.gridColumns %}
+ {% set caption = "Grid column widths" %}
+ {% set variant = "width" %}
+ {% set classColumnLabel = "Grid class" %}
+ {% set showNotes = false %}
+ {% include "design-system/styles/_partials/showcase-scale.njk" %}
Full width
@@ -207,7 +262,7 @@
Reading width
Lines of text should be no longer than 70 to 80 characters.
- When using the fluid-width container or wider grid columns, wrap text content with .ofh-u-reading-width to apply a maximum width and limit the number of characters per line.
+ Use .ofh-u-reading-width to apply a maximum width of {{ foundationStyles.layout.widthsById.reading.value }} to longer blocks of content.
{{ designExample({
group: "styles",
@@ -217,8 +272,8 @@
}) }}
Tablet and mobile-specific grid classes
- By default, grid columns sizes will go to 100% width of the container on screen sizes below the desktop breakpoint, which is 769px. These utility classes will enforce column widths on all screen sizes.
- To define the column sizes, add the utility class .ofh-u- followed by the width to an existing grid column. For example .ofh-u-one-half will set your column width to be one-half on all screen sizes.
+ By default, grid columns go to 100% width of the container on screen sizes below the desktop breakpoint, which is {{ foundationStyles.layout.breakpointsById.desktop.value }}. These utility classes will enforce column widths on all screen sizes.
+ To define the column sizes, add the utility class .ofh-u- followed by the width to an existing grid column. For example .ofh-u-one-half sets your column width to one-half on all screen sizes.
{{ designExample({
group: "styles",
@@ -228,7 +283,7 @@
}) }}
Tablet-specific grid classes
- These utility classes will enforce column widths on screen sizes larger than the mobile breakpoint, which is 320px.
+ These utility classes will enforce column widths from the tablet breakpoint upwards, which is {{ foundationStyles.layout.breakpointsById.tablet.value }}.
To define your column sizes, add the utility class .ofh-u- followed by the width and the suffix -tablet to an existing grid column. For example, .ofh-u-one-third-tablet will set your column width to be one-third on screen sizes for tablet and larger.
{{ designExample({
diff --git a/packages/site/views/design-system/styles/spacing/index.njk b/packages/site/views/design-system/styles/spacing/index.njk
index 388c6ce3d..e6ad183e6 100644
--- a/packages/site/views/design-system/styles/spacing/index.njk
+++ b/packages/site/views/design-system/styles/spacing/index.njk
@@ -39,7 +39,7 @@
- Spacing
@@ -59,81 +59,15 @@
Desktop is the reference size for each spacing key. Values then either stay the same or collapse at smaller breakpoints.
The responsive scales are split by direction. When you use the mixin with all, top and bottom use the vertical scale, while left and right use the horizontal scale.
- Horizontal spacing scale
-
- {{ table({
- panel: false,
- caption: "",
- head: [
- {
- text: "Size key"
- },
- {
- text: "Desktop",
- format: "numeric"
- },
- {
- text: "Tablet",
- format: "numeric"
- },
- {
- text: "Mobile",
- format: "numeric"
- }
- ],
- rows: [
- [{ text: "0" }, { text: "0", format: "numeric" }, { text: "0", format: "numeric" }, { text: "0", format: "numeric" }],
- [{ text: "2" }, { text: "2px", format: "numeric" }, { text: "2px", format: "numeric" }, { text: "2px", format: "numeric" }],
- [{ text: "4" }, { text: "4px", format: "numeric" }, { text: "4px", format: "numeric" }, { text: "4px", format: "numeric" }],
- [{ text: "8" }, { text: "8px", format: "numeric" }, { text: "8px", format: "numeric" }, { text: "8px", format: "numeric" }],
- [{ text: "12" }, { text: "12px", format: "numeric" }, { text: "12px", format: "numeric" }, { text: "12px", format: "numeric" }],
- [{ text: "16" }, { text: "16px", format: "numeric" }, { text: "16px", format: "numeric" }, { text: "16px", format: "numeric" }],
- [{ text: "24" }, { text: "24px", format: "numeric" }, { text: "24px", format: "numeric" }, { text: "16px", format: "numeric" }],
- [{ text: "32" }, { text: "32px", format: "numeric" }, { text: "32px", format: "numeric" }, { text: "16px", format: "numeric" }],
- [{ text: "40" }, { text: "40px", format: "numeric" }, { text: "32px", format: "numeric" }, { text: "16px", format: "numeric" }],
- [{ text: "48" }, { text: "48px", format: "numeric" }, { text: "32px", format: "numeric" }, { text: "16px", format: "numeric" }],
- [{ text: "56" }, { text: "56px", format: "numeric" }, { text: "32px", format: "numeric" }, { text: "16px", format: "numeric" }],
- [{ text: "64" }, { text: "64px", format: "numeric" }, { text: "32px", format: "numeric" }, { text: "16px", format: "numeric" }]
- ]
- }) }}
+ {% set items = foundationStyles.spacing.horizontalScale %}
+ {% set caption = "Horizontal spacing scale" %}
+ {% set variant = "spacing" %}
+ {% include "design-system/styles/_partials/showcase-scale.njk" %}
- Vertical spacing scale
-
- {{ table({
- panel: false,
- caption: "",
- head: [
- {
- text: "Size key"
- },
- {
- text: "Desktop",
- format: "numeric"
- },
- {
- text: "Tablet",
- format: "numeric"
- },
- {
- text: "Mobile",
- format: "numeric"
- }
- ],
- rows: [
- [{ text: "0" }, { text: "0", format: "numeric" }, { text: "0", format: "numeric" }, { text: "0", format: "numeric" }],
- [{ text: "2" }, { text: "2px", format: "numeric" }, { text: "2px", format: "numeric" }, { text: "2px", format: "numeric" }],
- [{ text: "4" }, { text: "4px", format: "numeric" }, { text: "4px", format: "numeric" }, { text: "4px", format: "numeric" }],
- [{ text: "8" }, { text: "8px", format: "numeric" }, { text: "8px", format: "numeric" }, { text: "8px", format: "numeric" }],
- [{ text: "12" }, { text: "12px", format: "numeric" }, { text: "8px", format: "numeric" }, { text: "8px", format: "numeric" }],
- [{ text: "16" }, { text: "16px", format: "numeric" }, { text: "8px", format: "numeric" }, { text: "8px", format: "numeric" }],
- [{ text: "24" }, { text: "24px", format: "numeric" }, { text: "16px", format: "numeric" }, { text: "16px", format: "numeric" }],
- [{ text: "32" }, { text: "32px", format: "numeric" }, { text: "24px", format: "numeric" }, { text: "24px", format: "numeric" }],
- [{ text: "40" }, { text: "40px", format: "numeric" }, { text: "32px", format: "numeric" }, { text: "32px", format: "numeric" }],
- [{ text: "48" }, { text: "48px", format: "numeric" }, { text: "40px", format: "numeric" }, { text: "40px", format: "numeric" }],
- [{ text: "56" }, { text: "56px", format: "numeric" }, { text: "48px", format: "numeric" }, { text: "48px", format: "numeric" }],
- [{ text: "64" }, { text: "64px", format: "numeric" }, { text: "56px", format: "numeric" }, { text: "56px", format: "numeric" }]
- ]
- }) }}
+ {% set items = foundationStyles.spacing.verticalScale %}
+ {% set caption = "Vertical spacing scale" %}
+ {% set variant = "spacing" %}
+ {% include "design-system/styles/_partials/showcase-scale.njk" %}
Spacing override classes
Occasionally, you might need to make minor adjustments like adding spacing or removing it from elements of your design. You can use the responsive spacing override classes for this.
@@ -142,12 +76,12 @@
The spacing override classes are structured to allow you to apply any size of the scale, using margin or padding in any direction.
The class name gives you the spacing size key, not a single fixed pixel value at every breakpoint.
To add padding use -padding instead of -margin.
- If you want to add the margin or padding in a particular direction, add left for left, right for right, top for top, or bottom for bottom. For example, -margin-top will set margin-top, -padding-right will set padding-right. If you do not specify a direction, the margin or padding will be applied to all sides of the element.
+ If you want to add the margin or padding in a particular direction, add left for left, right for right, top for top, or bottom for bottom. For example, -margin-top will set margin-top and -padding-right will set padding-right. If you do not specify a direction, the margin or padding will be applied to all sides of the element.
The last part of the class is the spacing size key. For example, ofh-u-margin-56 uses the 56 responsive spacing key and then applies the matching values from the relevant horizontal or vertical scale.
Examples:
- ofh-u-margin-bottom-56 uses the vertical scale, so it is 56px on desktop and 48px on tablet and mobile
- ofh-u-margin-right-56 uses the horizontal scale, so it is 56px on desktop, 32px on tablet, and 16px on mobile
+ ofh-u-margin-bottom-56 uses the vertical scale, so it is {{ foundationStyles.spacing.byKey['56'].vertical.desktop }} on desktop and {{ foundationStyles.spacing.byKey['56'].vertical.tablet }} on tablet and mobile
+ ofh-u-margin-right-56 uses the horizontal scale, so it is {{ foundationStyles.spacing.byKey['56'].horizontal.desktop }} on desktop, {{ foundationStyles.spacing.byKey['56'].horizontal.tablet }} on tablet, and {{ foundationStyles.spacing.byKey['56'].horizontal.mobile }} on mobile
ofh-u-margin-56 combines both scales, so top and bottom use the vertical values while left and right use the horizontal values
Small spacing keys stay consistent across breakpoints. Larger keys reduce as the layout collapses.
@@ -170,7 +104,8 @@
}) }}
Spacing on custom components
- If you’re building your own components and want to reference spacing directly in SCSS, use the static size tokens for fixed geometry and the responsive spacing mixins for layout spacing.
+ If you are building your own components and want to reference spacing directly in SCSS, use the static size tokens for fixed geometry and the responsive spacing mixins for layout spacing.
+ The old ofh-spacing() helper was removed in toolkit v4.5.0, so fixed spacing values should now come from $ofh-size-* tokens.
Using the static spacing tokens
If you want a fixed value that does not change by breakpoint, use the matching $ofh-size-* token:
@@ -180,8 +115,8 @@
If you want layout spacing that adapts by breakpoint, use the responsive mixin with a size key from the tables above.
For example:
- use @include ofh-responsive-margin(40, "bottom"); for 40px margin-bottom on desktop and 32px on tablet and mobile
- use @include ofh-responsive-padding(40); for 40px top and bottom on desktop, 32px top and bottom on tablet and mobile, and the matching horizontal scale on the left and right
+ use @include ofh-responsive-margin(40, "bottom"); for {{ foundationStyles.spacing.byKey['40'].vertical.desktop }} margin-bottom on desktop and {{ foundationStyles.spacing.byKey['40'].vertical.tablet }} on tablet and mobile
+ use @include ofh-responsive-padding(40); for {{ foundationStyles.spacing.byKey['40'].vertical.desktop }} top and bottom on desktop, {{ foundationStyles.spacing.byKey['40'].vertical.tablet }} top and bottom on tablet and mobile, and the matching horizontal scale on the left and right
Width override classes
diff --git a/packages/site/views/design-system/styles/typography/index.njk b/packages/site/views/design-system/styles/typography/index.njk
index d61e64586..1800df482 100644
--- a/packages/site/views/design-system/styles/typography/index.njk
+++ b/packages/site/views/design-system/styles/typography/index.njk
@@ -6,7 +6,6 @@
{% set backlog_issue_id = "1" %}
{% set hideContact = "true" %}
-
{% extends "app-layout.njk" %}
{% block breadcrumb %}
@@ -65,22 +64,37 @@
Font
Source Sans Pro
- Source Sans Pro is our primary typeface and should be used for all text content across our digital products and services.
+ Source Sans Pro is our primary typeface and should be used for text content across our digital products and services.
Fallback font
You can default to Arial when Source Sans Pro is not available.
- font-family: “Source Sans Pro”, Arial, sans-serif;
+ font-family: 'Source Sans Pro', Arial, sans-serif;
Typescale
- The typography scale depends on the viewport width. Phones and tablets share the same base sizes, and some styles step up at the desktop breakpoint from 769px and above.
+ The responsive type scale is defined in $ofh-typography-responsive-scale. It uses the same named tokens across the toolkit, docs, and published type utilities.
+ Phones and tablets share the same base sizes, and styles step up at the desktop breakpoint where the current token scale defines larger values.
+ The matrix below shows the current desktop, tablet, and mobile values behind the published typography utilities.
+ The class column shows the published class to apply when you want that typography style on an element.
+
+ {% set rows = foundationStyles.typography.rows %}
+ {% set caption = "Responsive typography matrix" %}
+ {% set showClassName = true %}
+ {% set showToken = false %}
+ {% set showLineHeight = true %}
+ {% set showNotes = true %}
+ {% set firstColumnLabel = "Token" %}
+ {% set classColumnLabel = "Apply this class" %}
+ {% include "design-system/styles/_partials/showcase-matrix.njk" %}
Headings
Write all headings in sentence case.
Style headings consistently to create a clear content structure throughout your service and a clear content hierarchy on a page.
Heading levels have meaning, especially for screen reader users and search engines. Using correct heading levels improves SEO and helps people using assistive technology to navigate through a page.
- Heading overrides
- We have 5 different classes that can be used to change the default font-size and spacing of headings. These classes can be used with any heading. Always ensure heading structure is semantically correct.
+ Heading overrides
+ We provide 5 heading utility classes, from ofh-heading-xl to ofh-heading-xs, which can be applied to any heading level.
+ h5 and h6 share the same responsive heading token.
+ Always ensure the heading structure is semantically correct, even when you override the visual size.
{{ designExample({
group: "styles",
@@ -90,8 +104,8 @@
}) }}
Captions
- Sometimes you may need to make it clear that a page is part of a larger section or group. To do this, you can use a heading with a caption
- We have 3 different sizes of caption, by default captions will display above headings. You can use an additional class to display the caption below headings.
+ Sometimes you may need to make it clear that a page is part of a larger section or group. To do this, you can use a heading with a caption.
+ We have 3 caption sizes. By default captions display above headings, and you can use an additional class to display the caption below the heading.
Consider whether the caption should be part of the page heading. This will determine whether the span is nested in the heading or not.
{{ designExample({
@@ -104,7 +118,7 @@
Paragraphs
Body
- The default paragraph font size is 20px on desktop and 18px on phones and tablets.
+ The default paragraph font size is {{ foundationStyles.typography.byId.paragraph.desktop }} on desktop and {{ foundationStyles.typography.byId.paragraph.mobile }} on mobile.
You can also add classes to create a lead paragraph or smaller body copy to convey content hierarchy in your page.
{{ designExample({
@@ -114,10 +128,9 @@
htmlOnly: true
}) }}
-
Lead paragraph
A lead paragraph is an introductory paragraph that you can use at the top of a page to summarise the content.
- Lead paragraphs use 24px type on desktop and 20px on phones and tablets. You should only use them once per page.
+ Lead paragraphs use {{ foundationStyles.typography.byId.lead.desktop }} type on desktop and {{ foundationStyles.typography.byId.lead.mobile }} on mobile. You should only use them once per page.
{{ designExample({
group: "styles",
@@ -128,7 +141,7 @@
Body small
You can use the ofh-body-s class sparingly to make your paragraph font size smaller.
- Small body text uses 16px across all breakpoints.
+ Small body text uses {{ foundationStyles.typography.byId['paragraph-small'].desktop }} on desktop and {{ foundationStyles.typography.byId['paragraph-small'].mobile }} on mobile.
{{ designExample({
group: "styles",
@@ -138,65 +151,23 @@
}) }}
Font override classes
- You might need to set the font size or font weight of an element outside the predefined heading and paragraph classes. To do this you can use the font override classes in your HTML or reference the typography mixins in your own components.
+ You might need to set the font size or font weight of an element outside the predefined heading and paragraph classes. To do this you can use the typography override classes in your HTML or reference the typography mixins in your own components.
Font size
- The font size override classes use the numeric responsive typography utilities, such as ofh-u-font-size-24 and ofh-u-font-size-20. They keep the pre-monorepo responsive values so maintainers can choose an override by the expected size.
+ The font size override classes use the numeric responsive typography utilities, such as ofh-u-font-size-24 and ofh-u-font-size-20. They keep the current toolkit utility scale so maintainers can choose an override by the expected rendered size.
Use these overrides sparingly where you need a utility-based typography adjustment, and visually check the result because they also apply the matching line height.
- The class name reflects the tablet-and-desktop size. On mobile, the utility collapses to the matching smaller value.
-
-
- Responsive font size override scale
-
-
-
-
-
-
-
-
-
- ofh-u-font-size-64 |
- 64px / 72px |
- 48px / 56px |
-
-
- ofh-u-font-size-48 |
- 48px / 56px |
- 32px / 40px |
-
-
- ofh-u-font-size-32 |
- 32px / 40px |
- 24px / 32px |
-
-
- ofh-u-font-size-24 |
- 24px / 32px |
- 20px / 28px |
-
-
- ofh-u-font-size-22 |
- 22px / 32px |
- 18px / 28px |
-
-
- ofh-u-font-size-20 |
- 20px / 28px |
- 18px / 24px |
-
-
- ofh-u-font-size-16 |
- 16px / 24px |
- 14px / 24px |
-
-
- ofh-u-font-size-14 |
- 14px / 24px |
- 12px / 20px |
-
-
-
+ The class name reflects the tablet-and-desktop size. Smaller utilities stay the same across breakpoints, while larger utilities step down on mobile.
+ The override utility class column shows the utility to add to an existing element when you need a one-off font-size adjustment.
+
+ {% set rows = foundationStyles.typography.utilityRows %}
+ {% set caption = "Responsive font size override scale" %}
+ {% set showClassName = true %}
+ {% set showToken = false %}
+ {% set showLineHeight = true %}
+ {% set showNotes = false %}
+ {% set firstColumnLabel = "Style" %}
+ {% set classColumnLabel = "Override utility class" %}
+ {% include "design-system/styles/_partials/showcase-matrix.njk" %}
{{ designExample({
group: "styles",
@@ -206,8 +177,8 @@
}) }}
Font weight
- You can add ofh-u-font-weight-normal or ofh-u-font-weight-bold to any other typographic class or element to change the font weight.
- Use bold in body content sparingly. It can be used to highlight critical information that users need to refer to, such as a reference number. But overuse of bold will make it difficult for users to know which parts of your content they need to pay the most attention to.
+ You can add ofh-u-font-weight-normal for regular text or ofh-u-font-weight-bold for semi-bold text (600) to any other typographic class or element.
+ Use semi-bold in body content sparingly. It can be used to highlight critical information that users need to refer to, such as a reference number. But overuse will make it difficult for users to know which parts of your content they need to pay the most attention to.
{{ designExample({
group: "styles",
@@ -248,6 +219,7 @@
Lists
Use lists to make blocks of text easier to read, and to break information into manageable chunks.
+ The list styles use their own responsive token shown in the matrix above, and in the current scale those values align with the default body copy.
{{ designExample({
group: "styles",
@@ -281,7 +253,7 @@
You can use the ofh-section-break classes on an <hr> element to create a thematic break between sections of content. ofh-section-break has class-based modifiers for different size margins.
By default ofh-section-break is only visible by its margin. You can add the ofh-section-break--visible class to make it visible with a separator line.
- Colour Variants
+ Colour variants
@@ -296,7 +268,7 @@
Text alignment
Text is left-aligned by default.
The straight edge of left-aligned text helps people who magnify their screen. It saves them having to search around the screen for the next line or item. They may miss things that are not left-aligned.
- Some people with cognitive differences have difficulty with blocks of text that are ‘justified’ – so that they are aligned to both the left and right margins. Justifying text creates wider spaces between words, which can make the text harder to read.
+ Some people with cognitive differences have difficulty with blocks of text that are justified, so that they are aligned to both the left and right margins. Justifying text creates wider spaces between words, which can make the text harder to read.
Only centre-align or fully justify text if you can show a clear user need.
Text alignment override classes
@@ -310,5 +282,4 @@
htmlOnly: true
}) }}
-
{% endblock %}
diff --git a/packages/site/views/design-system/styles/typography/weight/index.njk b/packages/site/views/design-system/styles/typography/weight/index.njk
index 048db3b40..e6919367b 100644
--- a/packages/site/views/design-system/styles/typography/weight/index.njk
+++ b/packages/site/views/design-system/styles/typography/weight/index.njk
@@ -1,2 +1,2 @@
-ofh-u-font-weight-normal
-ofh-u-font-weight-bold
+Regular (400) using ofh-u-font-weight-normal
+Semi-bold (600) using ofh-u-font-weight-bold