Document Head - National Design System

The stylesheets and scripts every NDS page loads so it paints fast, with no flash of unstyled content and no layout shift.

Page Setup

Put the assets and inline scripts in <head>, and the main bundle just before </body>. The ?ver= query is a cache-busting stamp: change it whenever a bundle changes.

<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Page Title</title> <style> /* -- Colors: the first-paint fills. Edit here to re-skin the shell. -- */ html[data-theme~=dark] :where(.nds-main-nav .nds-brand.nds-oncolor :is(img,svg)){filter:brightness(0) invert(1)} html{background-color:var(--background-body, #f9fafb)} html[data-theme~=dark]{background-color:var(--background-body, #111927)} :where(.nds-topbar){background-color:var(--background-topbar, #f3f4f6)} html[data-theme~=dark] :where(.nds-topbar){background-color:var(--background-topbar, #111927)} :where(.nds-main-nav){background-color:var(--background-nav, #fff)} html[data-theme~=dark] :where(.nds-main-nav){background-color:var(--background-nav, #1f2a37)} :where(.nds-hero-image-wrapper)::before{content:"";position:absolute;inset:0;background:color-mix(in srgb, var(--img-overlay-color, #092a1e) calc(var(--overlay, 0.7) * 100%), transparent);pointer-events:none} *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} /* -- Layout reservations and gates (structure) -- */ html{overflow-anchor:none} html :where(header){display:contents} html :where(.nds-topbar){height:40px} html :where(.nds-main-nav){height:var(--nds-nav-height, 72px)} html .nds-swiper.nds-hero:not([data-nds-swiper-initialized],[data-swiper-preset]) .nds-swiper-slide:not(:first-child){display:none} :where(.nds-topbar>*,.nds-main-nav>*,.nds-hero-section .nds-section-action,.nds-content-layout,.nds-user-feedback-section,.nds-footer){visibility:hidden} html:not([data-nds-loaded]) main{overflow-x:clip} :root{--nds-icons-opacity: 0} i.hgi-stroke{opacity:0} :is(.nds-hidden,[hidden],[data-state~=hidden],[data-filtered]){display:none !important} :where(.nds-hero-section){position:relative;height:550px} :where(.nds-hero-section.nds-sub){height:auto;min-height:220px} :where(.nds-hero-image-wrapper){position:absolute;inset:0} :where(.nds-hero-image){width:100%;height:100%;object-fit:cover;display:block} :where(.nds-hero-section :is(.nds-section-body,.nds-section-wrapper,.nds-breadcrumb-nav)){visibility:hidden} </style> <!-- Hero image — page-specific. Only on a page whose hero carries a photograph. The hero image is the LCP element, and the browser finds it late because it sits in a <picture> deep in the body. Preload the FIRST slide only, and repeat the <source> breakpoints exactly so the browser preloads the same file it will use. Drop these three lines on a page with no hero photo. --> <link rel="preload" as="image" href="assets/img/hero-sm.webp" media="(max-width: 768px)" fetchpriority="high"> <link rel="preload" as="image" href="assets/img/hero-md.webp" media="(min-width: 769px) and (max-width: 1646px)" fetchpriority="high"> <link rel="preload" as="image" href="assets/img/hero.webp" media="(min-width: 1647px)" fetchpriority="high"> <!-- Critical CSS — non-blocking; the gate above holds the layout until it lands. --> <link rel="preload" href="assets/css/nds.critical.min.css?ver=1.12.0" as="style" fetchpriority="high" data-nds-defer> <!-- Main CSS — deferred; the inline script applies it, the loader adds the icon sheets after it. --> <link rel="preload" href="assets/css/nds-main.min.css?ver=1.12.0" as="style" fetchpriority="low" data-nds-defer="main"> <!-- Placeholder icon — replace with your own. --> <link rel="icon" type="image/svg+xml" href="assets/img/favicon.svg"> <!-- Inline script — theme guard + applies the deferred styles. Copy from the JavaScript tab. Keep it last in the head. --> <script>/* see the JavaScript tab */</script> </head> <!-- ...page content... then just before </body>: --> <script defer src="assets/js/nds-main.min.js?ver=1.12.0"></script> <!-- No tag for the accessibility panel. nds-main.min.js fetches it on demand. -->

The version stamp

Change the ?ver= value every time you upgrade NDS. A stale stamp serves the old bundles from the browser cache.

Switching to a blocking critical stylesheet

The setup above is the gated one, and it is what this site runs. The inline style block draws the page shell so the page paints before critical CSS arrives, and the shell holds the layout until the real styles land. That block is the same shell this site serves, one rule per line so you can edit the colors.

Use a blocking stylesheet instead when a strict Content Security Policy cannot grant a nonce or a hash for that inline block. Delete the <style> block, then replace the preload link under it with a plain stylesheet link:

<link rel="stylesheet" href="assets/css/nds.critical.min.css?ver=1.12.0">

It blocks the first paint until critical CSS arrives, so the first thing on screen already has the real tokens and nothing flashes. Never remove the style block on its own: without the gate the page paints raw HTML first, then jumps when critical CSS lands.

Asset Files

What each bundle holds and how it loads.

FileContentsLoading
nds.critical.min.cssTokens, reset, fonts, hero, gateRender-blocking (or async behind the gate)
nds-main.min.cssAll component and layout stylesDeferred; gates the page reveal
nds-icons.min.cssUI icons (nds-icon)Added by the loader once main CSS applies
hgi-rounded-stroke-min.cssContent icon glyph map (hgi hgi-stroke). Its @font-face ships in the critical file, so this sheet can land late without a full relayoutAdded by the loader at the reveal, after main CSS and the critical pass
nds-accessibility.min.cssAccessibility panel and its mode overridesFetched by nds-accessibility.js itself once the panel arms — not linked in <head>. Optional; see Accessibility.
nds-main.min.jsLoader and all component behavior<script defer> before </body>
nds-accessibility.min.jsAccessibility panel behaviorFetched by nds-main.min.js on the first press of the accessibility button, or at load for a visitor with saved settings. No tag of its own. Optional

Content Security Policy

NDS runs under a strict CSP. One inline script needs your permission. Everything else loads from your own origin and needs nothing.

A strict policy blocks inline code. NDS keeps its inline code to one small script, so you have one thing to allow. You allow it with a nonce or with a hash. Prefer the nonce whenever your server renders responses. Use the hash only on a static host: a hash must match the script's bytes exactly, and that match breaks easily.

PartWhat it needs
Inline script in <head> (theme guard + deferred stylesheets)A nonce or a hash
Inline critical gate (<style>), unless you switched to the blocking stylesheetA nonce or a hash in style-src
Inline knobs on copied markup (style="--gap: …")Move the knob to your own class. A style attribute needs 'unsafe-inline', and no nonce or hash can cover one
All stylesheets and script bundles'self'
Icon sheets the loader addsNothing — same origin as your other files
UI icons (nds-icon)img-src data: — each icon is an inline SVG mask

A policy that covers all of it:

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-YOUR_RANDOM_VALUE'; style-src 'self' 'nonce-YOUR_RANDOM_VALUE'; img-src 'self' data:; font-src 'self';

Then put the same value on the tag:

<script nonce="YOUR_RANDOM_VALUE">/* the head script */</script> <script nonce="YOUR_RANDOM_VALUE" defer src="assets/js/nds-main.min.js"></script>

Your server must make a new random value for every response. A fixed value is not a nonce. It gives an attacker the same permission your own code has.

The main bundle needs the value too. The loader adds more script files at runtime, and it copies the nonce from the main bundle's tag onto each one. Without it, a nonce-only policy — one with no 'self' in script-src — blocks them, and the components they carry never start.

No server? Use a hash instead. A hash covers the script's contents between the tags, byte for byte — indentation, line endings, everything. A file saved with Windows CRLF line endings hashes differently from the same script saved with LF, and a formatter or minifier that touches the script kills the match too. Hash the contents only, never the tags. Take the SHA-256 of the script's contents, base64 it, and add 'sha256-…' to script-src. The browser tells you the right value for the bytes it actually served: load the page with the policy on, and the console error prints the hash it expected. Re-do this whenever anything edits the script.

Inline knobs under a strict CSP

A style attribute needs 'unsafe-inline'. No nonce and no hash can cover one. So under a strict policy every inline knob is dead: the value never applies, and the only warning is the browser's own CSP violation.

Find them all. Search your pages for style="--. Then move each knob to a class in your own stylesheet, which 'self' already allows. The markup keeps its NDS classes; you add one of your own.

<!-- Before — dead under a strict CSP. The knob never applies. --> <div class="nds-block nds-flex nds-col" style="--align: center;"> <!-- After — same NDS classes, plus one of yours. --> <div class="nds-block nds-flex nds-col signin-stack">

Then set the knob in your stylesheet:

.signin-stack { --align: center; }

NDS components are not affected. Their JavaScript sets styles through the CSSOM, which no policy blocks. Only knobs you wrote into a style attribute need this treatment.

Frameworks that re-render the head

The head script runs once, at page load. It adds the stylesheet links, and NDS later stamps data-nds-loaded and data-nds-fonts-loaded on <html>. None of that is in the server HTML. A framework that diffs the <head> against the server HTML on navigation removes all of it, while the inline gate stays. The page then hides itself until a full reload. Turbo, htmx boost and Blazor enhanced navigation all work this way.

Keep those nodes out of the diff: mark the injected links and the two <html> attributes as permanent, in the way your framework offers. If it removes them anyway, run the deferred-styles loop again after each navigation and put the two stamps back. The component side of a route change is covered in Refresh.

Why the stylesheets look the way they do

Each deferred stylesheet ships as a rel="preload" link with a data-nds-defer mark. The preload downloads the file at the right priority without blocking render. The head script then adds a normal stylesheet link for it, which reuses that download. A more common way to defer CSS is an onload attribute on the link. NDS does not use one, because a nonce and a hash both cover a script element, and neither can ever cover an inline event handler. An onload attribute needs 'unsafe-inline', which defeats the policy. Moving the same work into a script element is what makes a strict CSP possible.

NDS needs JavaScript. The head script is what applies the deferred sheets, so a browser with JavaScript turned off loads no styles and shows a blank page. There is no fallback for this, and it is deliberate: the components need JavaScript to work at all.

The icon sheets load from nds-main.min.js for the same reason. That file is already allowed by 'self', so icons need no grant from you at all.

Last Modified Date: 13/09/2026 - 08:56 AM
Was this page useful?
60% of users said Yes from 2843 Feedbacks