Adding Integrity to Google Fonts and CSS

Permalink to "Adding Integrity to Google Fonts and CSS"

Part of Applying SRI to Stylesheets & Web Fonts, this page is the copy-pasteable reference for putting a valid integrity attribute on a <link rel="stylesheet"> — for both self-hosted CSS and Google Fonts — and for the traps that make stylesheet SRI fail in ways script SRI never does.

Quick Reference

Permalink to "Quick Reference"
Property Value
Element <link rel="stylesheet">
Attribute integrity
Algorithm identifier sha384 (default), sha256, sha512
Token format sha384-<base64 digest>
Required companion attribute crossorigin="anonymous" (cross-origin CSS)
CORS response header needed Access-Control-Allow-Origin
Preload support <link rel="preload" as="style" integrity="…" crossorigin="anonymous">
CSP enforcement require-sri-for style
Browser support All major engines (Chromium, Firefox, Safari)
Covers font files? No — @font-face url() is unverified

Use SHA-384 unless a specific constraint forces another algorithm. The stylesheet URL must return identical bytes on every request, or the hash cannot validate.

How Stylesheet Integrity Works

Permalink to "How Stylesheet Integrity Works"

The browser fetches the CSS, decodes any Content-Encoding, computes the digest over the decoded bytes, and compares it to the integrity token before parsing the stylesheet into the CSSOM. A match applies the styles; a mismatch blocks the stylesheet and logs net::ERR_SRI_FAILED. The guarantee is scoped to the CSS bytes only — fonts and images the CSS references through url() are fetched separately and are not covered, a boundary detailed in the parent page and enforced by the same fetch-time gate described in Browser Enforcement & Security Boundaries.

Two conditions must both hold for the check to even run. First, the response body must be readable: for a cross-origin stylesheet that means a CORS-eligible fetch (crossorigin="anonymous") answered with an Access-Control-Allow-Origin header. Second, the bytes must be deterministic — the same URL must return the same payload on every request, or the fixed token in the HTML cannot match what an arbitrary visitor receives. Script SRI usually satisfies both trivially because bundles are content-addressed and static. Stylesheet SRI for fonts breaks precisely because the popular delivery model — the Google Fonts CSS endpoint — violates the second condition, returning per-browser payloads. Everything below is about restoring determinism and keeping the response readable.

Google Fonts CSS Byte-Instability Diagram showing the Google Fonts CSS endpoint returning different bytes to Chrome and Firefox, so their SHA-384 hashes differ and a single pinned integrity token cannot validate for both; self-hosting collapses them to one stable hash. fonts.googleapis /css2 endpoint Chrome bytes sha384-AAA… Firefox bytes sha384-BBB… One pinned token matches one, blocks the rest fix: self-host, one stable hash

Canonical Example

Permalink to "Canonical Example"

The following is a complete, production-ready self-hosted stylesheet reference with a SHA-384 integrity attribute. This is the pattern to reach for by default.

<link
  rel="stylesheet"
  href="https://assets.example.com/fonts/inter.7f3a91.css"
  integrity="sha384-h9d0e5rZ0b0m2Q6xk3sVQe0y8u2pQ3n7Xh4l1oCq6b8sJt3vN0aW9cR2yE7fLpH"
  crossorigin="anonymous"
/>

Generate the sha384-… token from the exact bytes the server delivers:

# Hash the deployed CSS artifact, not the source
openssl dgst -sha384 -binary dist/fonts/inter.css | openssl base64 -A
# → h9d0e5rZ0b0m2Q6xk3sVQe0y8u2pQ3n7Xh4l1oCq6b8sJt3vN0aW9cR2yE7fLpH

The -binary flag emits raw digest bytes; -A suppresses newline wrapping. Omitting either yields a value the browser rejects. The same two-command pipeline works for every artifact you ship, and Generating SRI Hashes with OpenSSL and shasum covers the shasum equivalent and the batch forms.

Read the finished tag as four independent parts: the algorithm prefix that tells the browser which digest function to run, the single hyphen that separates prefix from payload, the base64 digest itself, and the crossorigin attribute that makes the response body readable in the first place. Getting any one of them wrong produces the same visible symptom — an unstyled page — so it is worth knowing which part you are looking at.

Anatomy of a Stylesheet Integrity Token An annotated breakdown of a link rel=stylesheet tag showing the href, then the integrity value split into its sha384 algorithm prefix, the hyphen separator and the base64 digest, alongside the crossorigin anonymous attribute, each with a caption explaining its role. <link rel="stylesheet" href="/fonts/inter.7f3a91.css" integrity="…" and crossorigin="…" follow on the same tag sha384 - h9d0e5rZ0b0m…yE7fLpH crossorigin= "anonymous" picks the digest function base64 of the decoded response body, padding kept, no whitespace required before any hash is run

The base64 payload follows the standard alphabet with padding retained; the encoding rules that make a token acceptable are set out in Base64 Encoding Rules for SRI Hashes.

Variant Examples

Permalink to "Variant Examples"

Pinned Google Fonts CSS version

Permalink to "Pinned Google Fonts CSS version"

If you must reference Google’s CSS directly rather than self-hosting, pin a fixed User-Agent fetch and hash that payload — accepting that the token is only valid for browsers receiving the same bytes. This is fragile; self-hosting is strongly preferred.

# Capture one specific representation and hash it
curl -sL \
  -H "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36" \
  "https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" \
  -o inter-pinned.css
openssl dgst -sha384 -binary inter-pinned.css | openssl base64 -A
<!-- Fragile: only validates for User-Agents that receive identical bytes -->
<link
  rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap"
  integrity="sha384-Kx9pL2mQ7rT4vW1yB6nE8sH0jC5dF3gA9uZ2xR7wN4tM1oP6qS8bV0cX3yG5hJ2"
  crossorigin="anonymous"
/>

Preloading a hashed stylesheet

Permalink to "Preloading a hashed stylesheet"

Preload the stylesheet to start the fetch earlier while keeping the hash check. The crossorigin and integrity values must match on both tags, or the preload is discarded and the file is fetched twice.

<link
  rel="preload"
  as="style"
  href="https://assets.example.com/fonts/inter.7f3a91.css"
  integrity="sha384-h9d0e5rZ0b0m2Q6xk3sVQe0y8u2pQ3n7Xh4l1oCq6b8sJt3vN0aW9cR2yE7fLpH"
  crossorigin="anonymous"
/>
<link
  rel="stylesheet"
  href="https://assets.example.com/fonts/inter.7f3a91.css"
  integrity="sha384-h9d0e5rZ0b0m2Q6xk3sVQe0y8u2pQ3n7Xh4l1oCq6b8sJt3vN0aW9cR2yE7fLpH"
  crossorigin="anonymous"
/>

Multiple stylesheets

Permalink to "Multiple stylesheets"

Each <link> carries its own token; there is no shared attribute. Compute one digest per file.

<link rel="stylesheet" href="/css/base.a1b2c3.css"
  integrity="sha384-Tx0aQ9wE2rY7uI4oP1sD6fG3hJ8kL5mN0bV7cX2zA9qW4eR1tY6uI3oP8sD5fG2"
  crossorigin="anonymous" />
<link rel="stylesheet" href="/css/theme.d4e5f6.css"
  integrity="sha384-Ry1bS0xF3tZ8vJ5pQ2eE7gH4iK9lM6nO1cW8dY3aB0rX5fS2uZ7vJ4qR9tY6uI3"
  crossorigin="anonymous" />

SHA-512 for high-assurance pages

Permalink to "SHA-512 for high-assurance pages"

SHA-384 is the default. Use SHA-512 only when a higher-assurance regime calls for it — the token is longer but the tag is otherwise identical. Show SHA-256 only where a legacy constraint forces the shorter digest.

openssl dgst -sha512 -binary dist/fonts/inter.css | openssl base64 -A

Choosing between delivery models

Permalink to "Choosing between delivery models"

The variants above differ on exactly two questions: does the URL return the same bytes to every visitor, and can one token therefore serve every browser? A CDN copy of your own build answers both the same way self-hosting does, provided you disable any edge rule that rewrites the payload. The live Google endpoint answers neither. No delivery model answers the third question — none of them extend the stylesheet hash to the font binaries the CSS pulls in.

Stylesheet Delivery Models Compared A three-row comparison matrix showing that self-hosted hashed CSS and a CDN copy with transforms disabled both return byte-identical responses and validate with one token, while the live Google Fonts css2 URL varies per browser, and that none of the three cover font files referenced from font-face rules. Delivery model Byte-identical per visitor One token for all browsers Covers font binaries Self-hosted, hashed CSS stable yes no Google Fonts /css2 URL varies no no CDN copy, transforms off stable yes no The third column is a spec boundary, not a misconfiguration
<link
  rel="stylesheet"
  href="https://assets.example.com/fonts/inter.7f3a91.css"
  integrity="sha512-9Ck0m1sX3hV6dQ2yB8nE4tR7uP0jC5dF3gA9uZ2xR7wN4tM1oP6qS8bV0cX3yG5hJ2kL5mN0bV7cX2zA9qW4eR1tY6uI3oP8sD5fG2hJ8kL5mN0bV7cX2zA9qW4eR1tY6"
  crossorigin="anonymous"
/>

Gotchas and Edge Cases

Permalink to "Gotchas and Edge Cases"
  • Omitting crossorigin="anonymous" silently blocks the stylesheet. On a cross-origin <link rel="stylesheet"> with an integrity attribute but no crossorigin, the browser issues a no-CORS opaque fetch whose bytes it cannot read. It cannot hash an unreadable body, so it blocks the resource — not because the hash failed, but because the body was never available. This is the single most common stylesheet-SRI bug; always pair integrity with crossorigin="anonymous" for cross-origin CSS.

  • The Google Fonts CSS is byte-unstable per browser. fonts.googleapis.com/css2 serves different @font-face rules (formats, unicode-range subsets) depending on the requesting User-Agent. A token hashed from Chrome’s response fails for Firefox or Safari visitors. Do not pin SRI to the live endpoint — self-host the CSS so every visitor receives byte-identical content and one hash validates universally.

  • @font-face font files are never integrity-checked. The stylesheet hash covers the CSS bytes only. Font files named in src: url(...) are separate subresources with no integrity descriptor in CSS — the reasons this is a specification boundary rather than a missing feature are set out in Why Font Files Cannot Carry an integrity Attribute. Self-host and content-address the fonts and restrict font-src in your CSP; there is no per-font hash to add.

  • Hash the served artifact, not the source. Minification and autoprefixer change the bytes. Compute the digest after the full build, or by curl-ing the live URL, otherwise every deploy mismatches.

  • crossorigin must match between preload and stylesheet. A mismatch (one omitted, one anonymous) makes the browser discard the preload and fetch the CSS a second time, silently negating the optimization.

  • Compression is transparent, mutation is not. The browser hashes the decoded body, so Content-Encoding: gzip or br from the CDN is fine. What breaks the token is CDN-side CSS minification, URL rewriting, or comment stripping applied at the edge — any transformation that changes the decoded bytes. Disable payload-mutating rules for SRI-protected stylesheet paths.

  • @import inside a hashed stylesheet escapes the check. An @import url(...) pulls a second stylesheet that carries no integrity check of its own. Inline the imported rules into the hashed file, or serve the sub-stylesheet from 'self' under a style-src restriction.

Verification Steps

Permalink to "Verification Steps"

1. Confirm both attributes are present

Permalink to "1. Confirm both attributes are present"
grep -E 'rel="stylesheet"' dist/index.html | grep -E 'integrity=|crossorigin='

Expected — every cross-origin stylesheet line shows both integrity="sha384-…" and crossorigin="anonymous".

2. Re-derive the hash from the live CSS

Permalink to "2. Re-derive the hash from the live CSS"
curl -sL https://assets.example.com/fonts/inter.7f3a91.css \
  | openssl dgst -sha384 -binary | openssl base64 -A

The output must match the token in the HTML character for character.

3. Confirm clean enforcement in the browser

Permalink to "3. Confirm clean enforcement in the browser"

Load the page in Chrome or Firefox with DevTools open. A failure produces:

Failed to find a valid digest in the 'integrity' attribute for resource
'https://assets.example.com/fonts/inter.7f3a91.css' with computed SHA-384
integrity 'sha384-<actual>'. The resource has been blocked.

No such message, and a styled render, means the hash is correct. Flip one character of the token and reload to prove the gate blocks a mismatch, then restore it.

4. Gate CI on stylesheet hash drift

Permalink to "4. Gate CI on stylesheet hash drift"

Add a pipeline step that re-derives every stylesheet token from the built files and diffs it against the emitted HTML, failing the build on any divergence so a stale hash never ships:

# GitHub Actions
- name: Verify stylesheet SRI tokens
  run: |
    grep -oP 'href="\K[^"]+\.css' dist/index.html | while read -r css; do
      declared=$(grep -oP "href=\"${css//\//\\/}\"[^>]*integrity=\"\K[^\"]+" dist/index.html)
      computed="sha384-$(openssl dgst -sha384 -binary "dist/${css}" | openssl base64 -A)"
      [ "$declared" = "$computed" ] || { echo "Mismatch: $css"; exit 1; }
    done
    echo "All stylesheet hashes verified."

The step exits non-zero on drift, blocking promotion before a broken token reaches a CDN edge.

Frequently Asked Questions

Permalink to "Frequently Asked Questions"
Can I put SRI directly on the Google Fonts CSS URL?

Not reliably. The Google Fonts CSS endpoint returns different bytes per browser User-Agent, so a single sha384 token validates for the browser you hashed on and fails for others. Self-host the CSS to get one stable hash that works for every visitor.

Why do I need crossorigin on a stylesheet integrity tag?

For a cross-origin stylesheet, without crossorigin=“anonymous” the browser makes a no-CORS opaque fetch whose bytes it cannot read, so it blocks the resource before the hash is ever compared. It is the most common cause of a stylesheet that mysteriously fails SRI.

Does gzip or Brotli compression change the integrity hash?

No. The browser decodes the transfer encoding first and hashes the decoded representation, so the same stylesheet served identity, gzip and Brotli all produce the same digest. What does break the token is any edge rule that alters the decoded bytes themselves — minification, comment stripping or URL rewriting applied at the CDN.

Do I need SRI on a same-origin stylesheet?

It is optional but cheap. Same-origin CSS is readable without crossorigin, so the hash check runs either way, and it catches a corrupted deploy or a tampered cache entry. The stronger reason is policy: once you enforce require-sri-for style, every stylesheet regardless of origin must carry a token or the browser blocks it.

What happens to the page when a stylesheet fails its hash check?

The stylesheet is discarded entirely; no partial rules are applied. The document renders unstyled, the link element fires an error event, and the console shows net::ERR_SRI_FAILED alongside the computed digest. Because the failure is silent to the user beyond the broken layout, wire the error event to your telemetry rather than relying on someone noticing.

Permalink to "Related"

Related Articles

SRI for Self-Hosted Web Fonts
Why Font Files Cannot Carry an integrity Attribute
Applying SRI to Stylesheets & Web Fonts Core SRI Fundamentals & Browse…