I hit this problem on a React app using Grommet and grommet-icons: the app looked fine locally, then icons mysteriously disappeared once I tightened the Content Security Policy.

No console errors about JavaScript failures. No broken imports. Just empty spaces where icons should be.

That kind of bug is annoying because it looks like a UI issue, but the root cause is security policy.

Here’s the real-world version of what happened, what broke, and the CSP changes that fixed it without throwing the policy in the trash.

The setup

The app used:

  • React
  • Grommet
  • grommet-icons
  • a production CSP header
  • analytics and consent tooling

A realistic CSP often looks more like this than the tiny examples in docs. This is a real header shape from headertest.com:

content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-YTk2NTI2YmMtMWI0MC00NTYzLWE5ZTQtM2E2MTVmY2QzMWQx' '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 not unusual for a production app. The catch is that component libraries can rely on browser features you don’t think about until CSP blocks them.

What Grommet icons are actually doing

grommet-icons are SVG-based React components. Most of the time that’s good news, because CSP usually doesn’t care about inline SVG elements rendered as part of the DOM the same way it cares about inline scripts.

A typical icon usage looks like this:

import { Add, Trash } from 'grommet-icons';

export function Toolbar() {
  return (
    <>
      <Add />
      <Trash />
    </>
  );
}

That feels self-contained. No remote fetch, no <img src="...">, no webfont dependency.

But there are two places CSP can still bite you:

  1. Inline styles or CSS-in-JS used by your app or theme
  2. Data URLs or external references inside SVG usage patterns elsewhere in the UI

With Grommet specifically, teams often assume “the icons broke,” but the real issue is usually broader: a CSP that clashes with how the component styling pipeline works.

The before state

Here was the first locked-down policy we tried:

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

Looks clean. Looks strict. Also broke parts of the UI.

Symptoms:

  • Grommet icons rendered inconsistently
  • some icon-adjacent components lost sizing or color
  • theme-driven visual styles silently failed
  • devtools showed CSP violations for styles and sometimes image-like resources depending on usage

The key mistake was treating this as “just an icons issue.” It wasn’t. The icons were the obvious thing users noticed first.

How I debugged it

First, I checked the browser console for CSP violations. That gave the real clues.

Typical messages looked like:

Refused to apply inline style because it violates the following Content Security Policy directive: "style-src 'self'".

And in some setups, especially when apps use SVGs through data: URLs in other places, you may also see:

Refused to load the image 'data:image/svg+xml,...' because it violates the following Content Security Policy directive: "img-src 'self'".

That second one is not always caused by grommet-icons directly, but it often shows up in the same app because teams mix icon components with SVG data URIs for avatars, backgrounds, placeholders, or custom icon wrappers.

So the debugging rule was simple:

  • if rendered SVG React components look wrong, inspect style-src
  • if SVG via data URL is blocked, inspect img-src
  • if the app uses CSS-in-JS or runtime style injection, expect CSP friction

Why the policy failed

The strict version failed for two practical reasons.

1. style-src 'self' was too strict for the actual frontend stack

A lot of React UI stacks inject styles at runtime or rely on inline style attributes. If your app, your design system, or your theming layer does that, style-src 'self' by itself is often not enough.

That’s why so many production policies, including the real headertest.com example, contain:

style-src 'self' 'unsafe-inline' ...

I don’t love 'unsafe-inline', but I’ve seen too many teams break their UI by pretending they can just remove it overnight without changing their rendering approach.

2. img-src 'self' blocked valid data: image use

Even if Grommet icons themselves are React SVGs, modern frontend apps frequently use data: for tiny SVG assets. Blocking that can break icons, badges, placeholders, and generated UI elements in surprising ways.

The headertest.com header allows this:

img-src 'self' data: https:;

That data: matters.

The after state

We changed the policy to match the app we actually had, not the app we wished we had.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-rAnd0m123' 'strict-dynamic';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  form-action 'self';

After that:

  • Grommet icons rendered correctly
  • theme styling came back
  • SVG data URI assets loaded again
  • we kept a strong baseline in the rest of the policy

Was it perfect? No. Was it production-usable? Yes.

That’s the tradeoff I’d make every time over a “pure” CSP that breaks the interface.

Before and after in app code

Here’s a simplified component that looked broken under the stricter CSP:

import { Box, Button } from 'grommet';
import { Add } from 'grommet-icons';

export function AddButton() {
  return (
    <Box pad="small">
      <Button icon={<Add color="brand" />} label="Create item" />
    </Box>
  );
}

The component code wasn’t the problem. Changing imports, replacing icons, or rewriting JSX did nothing.

After the CSP fix, the exact same component worked.

That’s a useful lesson: when a mature icon library suddenly “fails,” don’t start by rewriting components. Check the policy first.

A production-ready Express example

If you’re serving a React app with Express and setting headers manually, here’s the practical version:

app.use((req, res, next) => {
  const nonce = crypto.randomUUID();

  res.setHeader(
    'Content-Security-Policy',
    [
      "default-src 'self'",
      `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' data: https:",
      "font-src 'self'",
      "connect-src 'self'",
      "object-src 'none'",
      "base-uri 'self'",
      "frame-ancestors 'none'",
      "form-action 'self'",
    ].join('; ')
  );

  res.locals.nonce = nonce;
  next();
});

If you also have analytics, consent, or API endpoints, extend the directives the way the headertest.com policy does instead of dumping everything into default-src.

What I would not do

I would not solve this by loosening everything:

default-src * 'unsafe-inline' 'unsafe-eval' data: blob:;

That’s not a fix. That’s giving up.

I’d also avoid blindly copying a CSP from another app. The headertest.com header is a good real-world reference because it shows how production policies grow around actual dependencies, but you still need to tailor yours to your stack.

The practical rule for Grommet icons

If you’re using Grommet icons and CSP together, check these first:

  • style-src: if your UI stack injects styles or relies on inline styles, this is usually where the breakage starts
  • img-src: allow data: if your app uses SVG data URIs anywhere
  • script-src: use nonces and strict-dynamic if you need dynamic script loading
  • object-src 'none', base-uri 'self', frame-ancestors 'none': keep these locked down

A decent baseline looks like this:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-<generated-nonce>' 'strict-dynamic';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://your-api.example;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  form-action 'self';

What I’d do next if I wanted to tighten it further

If you want to reduce reliance on 'unsafe-inline', that’s a frontend architecture project, not a header tweak.

That usually means:

  • reducing runtime style injection
  • moving toward nonce-compatible style handling where possible
  • auditing component libraries and theme systems
  • testing every CSP change in Report-Only mode first

For directive behavior and edge cases, the official CSP documentation is still the source of truth, and https://csp-guide.com is useful when you want quick, directive-specific explanations.

The official reference is here:

My takeaway: Grommet icons were never really the enemy. The CSP was out of sync with the frontend stack. Once the policy matched reality, the icons came back and the app stayed locked down where it actually mattered.