I’ve run into this exact problem a few times: a team tightens Content Security Policy, ships it to production, and suddenly half the icons vanish.

Not SVG logos. Not a giant component library. Tiny CSS.gg icons.

They look harmless because CSS.gg is “just CSS”. That’s exactly why they get people into trouble. CSS-based icon sets often depend on inline styles, pseudo-elements, CSS variables, or external stylesheets loaded from a CDN. All of those can collide with a strict style-src.

Here’s a real-world case study for a developer audience: what breaks, why it breaks, and what I’d actually ship.

The setup

A site starts with a fairly standard CSP. The real header below comes from headertest.com:

content-security-policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-ZmE0YWM1N2UtYWYwZS00ZDM2LWJiOTAtZDM4NDZiZTRlNGNl' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
  frame-src 'self' https://consentcdn.cookiebot.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none'

That policy is permissive enough for lots of frontend code because style-src includes 'unsafe-inline'.

Then somebody does the right thing and tries to harden it.

Before: CSS.gg works, but the policy is soft

A common integration looks like this:

<link rel="stylesheet" href="/css/gg.css">

<button class="icon-btn">
  <i class="gg-search"></i>
  Search
</button>

Or sometimes this:

<link rel="stylesheet" href="https://cdn.example.com/css.gg.min.css">

And then a little bit of sizing or color inline:

<i class="gg-heart" style="color:#e11d48"></i>

This works under a policy like:

Content-Security-Policy:
  default-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data:;
  object-src 'none';
  base-uri 'self';

Why? Because 'unsafe-inline' allows inline style="" attributes and inline <style> blocks.

For a lot of teams, that’s the hidden dependency. The icons don’t just need the CSS.gg stylesheet. They also rely on developers sprinkling inline style attributes all over templates to tweak size, rotation, spacing, or color.

That’s convenient. It’s also exactly the kind of thing CSP is supposed to constrain.

The change that broke production

The team removes 'unsafe-inline' from style-src:

Content-Security-Policy:
  default-src 'self';
  style-src 'self';
  img-src 'self' data:;
  object-src 'none';
  base-uri 'self';

Immediately, a few things start failing:

  • icon color disappears
  • icon sizing falls back to defaults
  • some icons look misaligned because custom CSS variables were set inline
  • if CSS.gg was loaded from a CDN, the whole icon set may stop rendering

Here’s the kind of browser error you’ll see:

Refused to apply inline style because it violates the following Content Security Policy directive:
"style-src 'self'".
Either the 'unsafe-inline' keyword, a hash, or a nonce is required to enable inline execution.

And if the stylesheet came from a third-party host not listed in style-src, you’ll get:

Refused to load the stylesheet 'https://cdn.example.com/css.gg.min.css'
because it violates the following Content Security Policy directive:
"style-src 'self'".

What was actually happening

CSS.gg icons are CSS classes that draw shapes using borders and pseudo-elements. A typical icon might rely on markup like this:

<i class="gg-menu-right"></i>

That’s fine. The trouble starts when developers customize presentation inline:

<i class="gg-menu-right" style="color: white; transform: scale(1.2)"></i>

Or through per-component <style> blocks generated by a framework.

Strict CSP doesn’t care that it’s “just styling”. Inline style is still inline style.

If you’re trying to reduce XSS impact, keeping 'unsafe-inline' in style-src is a compromise. Sometimes necessary, often lazy, usually worth revisiting.

For a deeper breakdown of the directive behavior, the style-src reference on https://csp-guide.com is a good quick lookup.

After: same icons, tighter CSP

The fix I prefer is boring and reliable:

  1. self-host the CSS.gg stylesheet
  2. remove inline style attributes from icon markup
  3. move icon customization into approved stylesheets
  4. keep style-src 'self' and avoid 'unsafe-inline'

After markup

Instead of this:

<i class="gg-heart" style="color:#e11d48"></i>

Ship this:

<i class="gg-heart icon-heart"></i>

After stylesheet

.icon-heart {
  color: #e11d48;
}

.icon-btn .gg-search {
  transform: scale(1.1);
  margin-right: 0.5rem;
}

After CSP

Content-Security-Policy:
  default-src 'self';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

That’s enough for CSS.gg if you host the stylesheet locally and stop relying on inline style attributes.

A more realistic migration example

Here’s a before-and-after from a navbar component.

Before

<link rel="stylesheet" href="https://cdn.example.com/css.gg.min.css">

<nav class="topbar">
  <a href="/" class="brand">
    <i class="gg-home" style="color:#0f172a; transform:scale(1.15)"></i>
    Dashboard
  </a>

  <button class="menu-toggle">
    <i class="gg-menu" style="color:#334155"></i>
  </button>
</nav>

CSP:

Content-Security-Policy:
  default-src 'self';
  style-src 'self' 'unsafe-inline' https://cdn.example.com;

After

<link rel="stylesheet" href="/assets/css/cssgg.css">
<link rel="stylesheet" href="/assets/css/components/topbar.css">

<nav class="topbar">
  <a href="/" class="brand">
    <i class="gg-home brand-icon"></i>
    Dashboard
  </a>

  <button class="menu-toggle">
    <i class="gg-menu toggle-icon"></i>
  </button>
</nav>
.brand-icon {
  color: #0f172a;
  transform: scale(1.15);
}

.toggle-icon {
  color: #334155;
}

CSP:

Content-Security-Policy:
  default-src 'self';
  style-src 'self';
  img-src 'self' data:;
  object-src 'none';
  base-uri 'self';

Nothing fancy. Just less fragile.

What if your framework injects styles?

This is where teams get stuck.

If you’re using a framework or CSS-in-JS setup that injects <style> tags at runtime, style-src 'self' may still break the page even after you fix the icon markup. In that case you have a few options:

Option 1: add a nonce for style blocks

Content-Security-Policy:
  default-src 'self';
  style-src 'self' 'nonce-rAnd0m123';

Then on the server:

<style nonce="rAnd0m123">
  .brand-icon { color: #0f172a; }
</style>

This works for inline <style> blocks, not style="" attributes.

Option 2: precompile styles

This is my preference when possible. Static CSS is easier to reason about, cache, and secure.

Option 3: keep 'unsafe-inline' temporarily

I’d treat this as a migration step, not the final state.

The headertest.com policy does exactly that:

style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;

That’s understandable for a production site with consent tooling and third-party integrations. But if your goal is a tighter CSP for your own app UI, CSS.gg icons are not a good reason to keep 'unsafe-inline' forever.

The part people forget: third-party stylesheets

If you load CSS.gg from anywhere except your own origin, style-src must explicitly allow that host.

Example:

style-src 'self' https://static.example-cdn.com;

I usually recommend self-hosting icon CSS instead. Fewer dependencies, cleaner CSP, less surprise during an outage.

What I’d ship on csp-examples

For a site like csp-examples, I’d keep the icon path simple:

  • self-host CSS.gg
  • no inline style="" on icon elements
  • no CSS CDN dependency
  • no 'unsafe-inline' in style-src unless another tool absolutely requires it

Example final policy:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic';
  style-src 'self';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';

And the icon usage:

<link rel="stylesheet" href="/assets/css/cssgg.css">
<link rel="stylesheet" href="/assets/css/site.css">

<button class="search-btn">
  <i class="gg-search search-btn__icon" aria-hidden="true"></i>
  <span>Search</span>
</button>
.search-btn__icon {
  color: currentColor;
  margin-right: 0.5rem;
}

That gives you the same UI without punching a hole in style-src.

My take

CSS.gg itself isn’t the security problem. The problem is how people tend to use CSS-based icon libraries: CDN stylesheets, inline tweaks, and framework-generated style blocks that quietly depend on a permissive CSP.

If your icons disappeared after tightening CSP, don’t immediately add 'unsafe-inline' back and call it fixed. Check where the stylesheet comes from, remove inline style attributes, and move customization into static CSS first.

That usually solves it.

For directive-specific details, the official CSP spec and MDN documentation are worth keeping handy, and https://csp-guide.com is useful when you just need the practical version fast.