How to Fetch Secondary Theme Colors in SPFx with supportsThemeVariants: true

How to Fetch Secondary Theme Colors in SPFx with supportsThemeVariants: true

Fetching Secondary Theme Colors in SharePoint SPFx with supportsThemeVariants: true

When building modern SharePoint Framework (SPFx) web parts, supporting theme variants is essential for compatibility with dark mode and section backgrounds.

However, there is an important limitation developers often face:

When supportsThemeVariants is set to true, tryGetTheme() does not return the secondaryColors palette.

But if you disable it:

"supportsThemeVariants": false

then tryGetTheme() can fetch the secondary colors correctly.

Unfortunately, disabling theme variants causes another major issue:

  • SharePoint injects root background styling

  • The web part becomes incompatible with dark themes

  • Section background adaptation breaks

  • Theme-aware rendering no longer works correctly

So the challenge becomes:

How do we keep supportsThemeVariants: true AND still fetch secondary theme colors?

The solution is to directly read the SharePoint theme XML file (theme.spcolor) from the themed CSS folder.

Why tryGetTheme() Fails with Theme Variants Enabled

Normally in SPFx, developers use:

this.context.serviceScope.consume(ThemeProvider.serviceKey);

or:

tryGetTheme();

to access current theme information.

This works fine for primary palette values such as:

  • themePrimary

  • themeSecondary

  • neutralLight

  • neutralLighter

But once theme variants are enabled:

"supportsThemeVariants": true

the returned theme object no longer includes the secondaryColors collection.

This is because SharePoint applies variant-aware theme processing internally and trims certain palette metadata from the runtime theme object.

The Better Approach

Instead of relying on tryGetTheme(), we can fetch the actual SharePoint theme definition file:

theme.spcolor

This XML file contains:

  • Primary theme colors

  • Neutral colors

  • Accent colors

  • Secondary color palettes

  • Light/Dark theme variations

And it works perfectly even when theme variants are enabled.

SPFx Solution

Step 1 — Fetch theme.spcolor

We first locate the themed CSS folder using:

this.context.pageContext.legacyPageContext.themedCssFolderUrl

Then request the XML file.

const legacyCtx = this.context.pageContext.legacyPageContext;
const themedCssFolderUrl = legacyCtx.themedCssFolderUrl;
const response = await this.context.spHttpClient.get(
  `${themedCssFolderUrl}/theme.spcolor`,
  SPHttpClient.configurations.v1,
  { headers: { Accept: "text/xml, application/xml" } },
);

Step 2 — Parse the XML

We use DOMParser to convert the XML response into a readable document.

const xmlText = await response.text();
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, "application/xml");

Step 3 — Extract Colors

The helper function below reads colors from the XML palette.

const getColor: (name: string) => string | undefined = (name) => {
  const nodes = xmlDoc.querySelectorAll("colorPalette > color");
  for (const node of Array.from(nodes)) {
    if (node.getAttribute("name") === name) {
      const raw = node.getAttribute("value") ?? "";
      return raw.length >= 8 ? `#${raw.slice(2)}` : `#${raw}`;
    }
  }
  return undefined;
};

This automatically converts SharePoint ARGB values into standard HEX colors.

Example:

FF0078D4 → #0078D4

Reading Secondary Colors

The important part is extracting:

<secondaryColors>

from the XML.

const secondaryPaletteNodes = xmlDoc.querySelectorAll(
  "secondaryColors > light > colorPalette",
);

Then we transform the palette into usable JavaScript objects.

const palettesFromXml = Array.from(secondaryPaletteNodes).map((palette) => {
  const entry: Record<string, string> = {};
  palette.querySelectorAll("color").forEach((c) => {
    const n = c.getAttribute("name") ?? "";
    const v = c.getAttribute("value") ?? "";
    entry[n] = v.length >= 8 ? `#${v.slice(2)}` : `#${v}`;
  });
  return entry;
});

Creating a Fallback Palette

Not every SharePoint theme contains secondary palettes.

To avoid runtime issues, we generate fallback combinations using primary theme colors.

const fallback = [
  [white, getColor("themePrimary")],
  [white, getColor("accent")],
  [getColor("themePrimary"), white],
  [white, getColor("themeSecondary")],
]

This guarantees that the UI still has usable color pairs even if secondary colors are missing.

Final Result

const secondaryColors = (
  palettesFromXml.length > 0 ? palettesFromXml : fallback
).map((e) => ({
  ...e,
  backgroundColor: e.backgroundColor ?? white,
}));

Now your SPFx web part can:

  • Fully support dark mode

  • Respect SharePoint section themes

  • Use theme variants properly

  • Access secondary palettes

  • Avoid disabling supportsThemeVariants

Complete Working Method

public async getSiteBrandingColors(): Promise<void> {
  const legacyCtx = this.context.pageContext.legacyPageContext;
  const themedCssFolderUrl = legacyCtx.themedCssFolderUrl;
  const response = await this.context.spHttpClient.get(
    `${themedCssFolderUrl}/theme.spcolor`,
    SPHttpClient.configurations.v1,
    { headers: { Accept: "text/xml, application/xml" } },
  );
  if (!response.ok) {
    console.error("Failed to fetch theme.spcolor:", response.status);
    return;
  }
  const xmlText = await response.text();
  const parser = new DOMParser();
  const xmlDoc = parser.parseFromString(xmlText, "application/xml");
  const getColor: (name: string) => string | undefined = (name) => {
    const nodes = xmlDoc.querySelectorAll("colorPalette > color");
    for (const node of Array.from(nodes)) {
      if (node.getAttribute("name") === name) {
        const raw = node.getAttribute("value") ?? "";
        return raw.length >= 8 ? `#${raw.slice(2)}` : `#${raw}`;
      }
    }
    return undefined;
  };
  const secondaryPaletteNodes = xmlDoc.querySelectorAll(
    "secondaryColors > light > colorPalette",
  );
  const palettesFromXml = Array.from(secondaryPaletteNodes).map((palette) => {
    const entry: Record<string, string> = {};
    palette.querySelectorAll("color").forEach((c) => {
      const n = c.getAttribute("name") ?? "";
      const v = c.getAttribute("value") ?? "";
      entry[n] = v.length >= 8 ? `#${v.slice(2)}` : `#${v}`;
    });
    return entry;
  });
  const white = "#ffffff";
  const fallback = [
    [white, getColor("themePrimary")],
    [white, getColor("accent")],
    [getColor("themePrimary"), white],
    [white, getColor("themeSecondary")],
    [getColor("themePrimary"), getColor("neutralLighter") ?? "#f3f2f1"],
    [white, getColor("themeTertiary")],
    [white, getColor("themeLight")],
    [white, getColor("HeaderBackground")],
  ]
    .filter((pair): pair is [string, string] => !!(pair[0] && pair[1]))
    .map(([themePrimary, backgroundColor]) => ({
      themePrimary,
      backgroundColor,
    }));
  const secondaryColors = (
    palettesFromXml.length > 0 ? palettesFromXml : fallback
  ).map((e) => ({
    ...e,
    backgroundColor: e.backgroundColor ?? white,
  }));
  console.log("secondaryColors:", secondaryColors);
}

Conclusion

If your SPFx solution needs:

  • Theme-aware rendering

  • Dark theme compatibility

  • SharePoint section support

  • Secondary theme palettes

then avoid disabling:

"supportsThemeVariants": true

Instead, fetch and parse theme.spcolor directly.

This approach gives you the best of both worlds:

✅ Full theme variant support
✅ Access to SharePoint secondary color palettes

This technique is especially useful for:

  • Branding web parts

  • Theme pickers

  • Dynamic UI generators

  • Design systems

  • Multi-theme SPFx solutions

  • Tenant branding utilities