Why Do Perspective Themes Fail After Ignition 8.3 Migration?

Claire Rousseau13 min read
HMI / SCADAOther ManufacturerTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

After an 8.1-to-8.3 upgrade, Perspective custom themes stop loading because they now live in the new config resource collection. Themes that depended on root-level CSS files, bare common folders, or sibling-file imports arrive incomplete or unresolvable. Renaming the .migrated folders back to their original names changes nothing, because the 8.3 gateway never reads that location. Run the checks below in order. Each one names the reading to take, what each outcome means, and which check comes next.

Check 1: Which Themes Directory the 8.3 Gateway Actually Reads

Prerequisite: file-system access to the gateway install directory, and the 8.1 gateway backup or a copy of the original 8.1 themes folder.

Location Version Role in 8.3
data/modules/com.inductiveautomation.perspective/themes 8.0 / 8.1 Legacy location. Not served in 8.3. The gateway reads it only as migration input on restart.
data/config/resources/core/com.inductiveautomation.perspective/themes/ 8.3 The only path the 8.3 theme system reads.
Folders suffixed .migrated 8.1 to 8.3 upgrade Automatic backup of the originals, created because the migration is not 1:1. Present only if legacy themes existed. Never read.
light, dark 8.3 System themes held in memory. They do not appear in core unless you copy them there read-only for viewing.
  1. Open data/config/resources/core/com.inductiveautomation.perspective/themes/. Reading: one sub-folder per custom theme.
  2. If the custom theme folders are present, go to Check 2.
  3. If the folder is empty or missing, the migration had nothing to work from, or it failed. Go directly to the re-migration procedure in the last technical section.
  4. Missing light and dark folders are normal. Do not treat their absence as a fault.

Leave the .migrated copies alone. They are your fallback source if the 8.1 backup is not at hand.

Check 2: Stray config.json and resource.json at the Themes Root

A known migration defect creates a resource.json and a config.json directly in the root themes folder. It is triggered by a legacy .backup directory sitting under the 8.1 themes path. With those two files at the root, the resource system treats the whole themes directory as a resource, and theme resolution breaks. Themes that were perfectly valid in 8.1 then fail. Imported animation libraries built on class selectors are a typical casualty: they simply stop applying.

  1. List the files directly inside .../core/com.inductiveautomation.perspective/themes/. Reading: only theme folders should be present, with no loose .json files.
  2. If resource.json or config.json exists at that level, delete both.
  3. Trigger a rescan from the Gateway Web UI projects page (.../system/projects). Reload a Perspective session and retest the theme.
  4. If the theme now renders, you are done. Continue to Check 3 only if some styling is still missing.
  5. If you have already hand-edited the migrated theme heavily, stop editing it. Re-migrate the untouched 8.1 original instead, using the procedure in the last technical section. A theme reworked after a bad migration is harder to recover than a clean re-migration.

A themes.digest.json that still lists paths under data/modules/com.inductiveautomation.perspective/ also points to stale migration output. Re-run the migration rather than hand-editing every path in the digest.

Check 3: Diff the Migrated Themes Against the 8.1 Originals

The 8.3 migration and import-resolution logic assumes the 8.1 layout: every theme is a root-level CSS file (themes/foo.css) plus a matching folder (themes/foo/). Anything that breaks that pairing is an edge case. Shared stylesheet folders are the main example: they have no root .css file because they were never meant to be selectable themes. The migration either skips them or cannot resolve imports into them. Any theme that imports from them then loads partially.

8.1 layout item Typical migration result Next check
themes/foo.css + themes/foo/ Migrated to core/.../themes/foo/ Check 4, then Check 5
Shared folder with no root .css (for example themes/foo-common/) Missing, or present but not resolvable Check 6
Loose .css file in the themes root with no folder Missing Move it into a theme folder (Check 4) and repoint imports (Check 5)
Theme importing a sibling root file, such as @import "./light.css" Migrated, but the import resolves to nothing Check 5
  1. Walk the 8.1 themes tree and the 8.3 core/.../themes tree side by side. Reading: every 8.1 folder and root CSS file accounted for.
  2. Confirm the gateway version. The theme-migration improvements shipped in the 8.3.0 final release, not in the early-access or RC builds. On a pre-release build, upgrade before debugging further, then re-migrate.
  3. If you still have an 8.1 gateway, the cleaner fix is to repair the layout there: give each shared folder a matching root .css file, correct the imports, and then migrate.

Check 4: Resource Structure Inside Each Theme Folder

In 8.3 the gateway no longer exposes a file system to the browser. It exposes resources. A folder without a resource.json is not detected as a resource at all, which differs from 8.1 behavior. A CSS file that is not listed in the resource's files array is never served. The browser follows each @import and requests the file. If the gateway has not whitelisted it, the request fails and those rules silently disappear. The gateway does not parse your CSS to infer dependencies, so you list every file explicitly.

custom-theme/
  index.css      // theme entry, configurable in config.json
  config.json    // theme configuration
  resource.json  // internal resource manifest
config.json property Effect Setting
entrypoint File where the gateway's theme processing starts. It then follows the imports from there. Defaults to index.css
isPrivate Controls whether the theme appears in the available-themes list. true hides it. Set explicitly: false for selectable themes, true for shared or override resources

A working resource.json for a theme whose index.css imports two local files:

{
  "scope": "A",
  "version": 1,
  "restricted": false,
  "overridable": true,
  "files": [
    "config.json",
    "index.css",
    "variables.css",
    "fonts.css"
  ],
  "attributes": {}
}
  1. For each theme folder, confirm that all three files exist. Reading: index.css (or the declared entry point), config.json, and resource.json.
  2. Trace every @import reachable from the entry point that stays inside this folder. Confirm each target appears in files.
  3. After any direct file-system edit, rescan from .../system/projects. Without a rescan, the gateway keeps serving its previous view of the resource.
  4. Open a Perspective session with the browser developer tools on the network tab. Reading: no failed CSS requests. A failed request for a local file means it is missing from files. A failed request for a file in another theme means the import path is wrong, so go to Check 5.

Check 5: @import Paths That Point at Theme Directories, Not Sibling Files

In 8.1, a theme could import another theme's root file, because both lived side by side in themes/. In 8.3 each theme is a self-contained folder, and the old root files no longer exist. An import has to reach into the other theme's directory and name the actual file. Relative paths resolve from the importing file's location. ./ is the current theme folder, and ../ is the themes collection root.

8.1 import 8.3 import
@import "./light.css"; @import "../light/index.css";
@import "./foo-common.css"; @import "../foo-common/index.css";
@import "./my-theme/variables.css"; (from a root file) @import "./variables.css"; (from my-theme/index.css)

The bundled derived theme light-cool shows the reference pattern. Its index.css reads:

@import "./variables.css";
@import "../light/fonts.css";
@import "../light/globals.css";
@import "../light/app/index.css";
@import "../light/common/index.css";
@import "../light/designer/index.css";
@import "../light/palette/index.css";

The simplest correct form for a derived theme imports the base entry point first, then its own files:

@import "../light/index.css";
@import "./variables.css";
  1. Rewrite every cross-theme import to the ../<theme>/<file> form. This is easier to do on the 8.1 copy before migrating, but it works just as well afterwards.
  2. Keep the base theme imported. light is the base that every theme extends. Any ruleset from it that your theme neither imports nor redeclares shows up as a visible defect, such as a button missing its border.
  3. Rescan and reload. Reading: base component styling is present, and your variable values win where they are redeclared.
  4. If @import "../light/index.css" leaves the theme mostly unstyled, check the gateway build. On the RC, light-cool/index.css returned a 404 because the restructured base themes shipped only in 8.3.0 final. Upgrade to 8.3.0 or later.

Check 6: Shared Stylesheet Folders Rebuilt as Private Themes

A shared folder used by several themes (a *-common directory) has to become a real theme resource in 8.3, or its files are never served. Make it a full resource with its own entry point, and hide it from the theme list.

  1. Create themes/foo-common/ in the core collection, containing index.css, config.json, resource.json, and the shared CSS files.
  2. In config.json, set "entrypoint": "index.css" and "isPrivate": true.
  3. In the shared folder's index.css, import each shared file with ./ paths. List every one of them in resource.json files.
  4. In each consuming theme, replace the old import with @import "../foo-common/index.css";. If a consumer only needs part of the shared set, import the specific file instead.
  5. Rescan. Reading: foo-common does not appear in the session theme dropdown, and every consuming theme renders its shared rules.

Check 7: Customizing Base Light and Dark Through overrides-light and overrides-dark

Inductive Automation owns light and dark, which live in memory and cannot be edited. The derived themes (light-cool, light-warm, dark-cool, dark-warm) can be modified freely. From 8.3.0, you can also override the base themes the way you could in 8.1. Create a theme resource named overrides-light or overrides-dark. When the gateway detects one, it redirects the base theme's entry point to the override's entry point. Every session using light then loads your file instead.

Example POST payload for the resource:

[
  {
    "name": "overrides-light",
    "collection": "core",
    "enabled": true,
    "description": "Overrides light theme.",
    "config": {
      "entrypoint": "index.css",
      "isPrivate": true
    }
  }
]

Minimal override entry point:

@import "../light/index.css";
:root {
  --customVariable: "whatever";
}

Because the override replaces the base entry point, you are responsible for importing the base theme. Leave out ../light/index.css (or its individual sub-indexes) and every session on light loses its base styling.

Requirement Approach Trade-off
Add variables or rules to light/dark for every project overrides-light / overrides-dark One override resource per base theme. A later write replaces the earlier one.
Selectable corporate theme Derived theme that imports ../light/index.css plus local variables.css Projects must select it in session properties.
Per-project class tweaks Project stylesheet (Advanced Stylesheet) Scoped to the project. Supplements base themes without touching them.
Copy base theme files into a new folder and edit Avoid New components' rules and Inductive Automation styling fixes never reach your copy.

Several modules or vendors extending light at the same time must not each ship their own overrides-light. Whichever write lands last wins, and a module that rewrites the override on install or restart wipes out client customizations. Instead, each module ships its styles in its own theme folder, and the site owner maintains one aggregating override:

@import "../light/index.css";
@import "../vendor-a-light/index.css";
@import "../vendor-b-light/index.css";
@import "../site-light/index.css";

Import order is cascade order: later files win on equal specificity. A migrated corporate theme that used to be a full copy of light collapses to three parts. Use the light-cool index as the template, put the changed values in a local variables.css, and move the class-level tweaks into the project's Advanced Stylesheet. Theme variables also work in the Designer property editor. Set a style property to the variable name, for example backgroundColor: --corporateColor.

Check 8: Creating and Inspecting Theme Resources Through the Gateway API

The OpenAPI HTTP interface is the recommended way to create and modify themes. The file system works, but every change needs a rescan.

Purpose Endpoint
Confirm a theme resource exists and read its config {{gateway_address}}/data/api/v1/resources/find/com.inductiveautomation.perspective/themes/:name
Read or write a single theme file /data/api/v1/resources/datafile/com.inductiveautomation.perspective/themes/{name}/{filename}
Multipart upload of a theme's files .../themes/{name}
  1. Query find for each theme. A missing resource means the folder lacks resource.json or no rescan has run yet.
  2. Read the entry point back through datafile. Reading: file contents returned. A 404 on a file that exists on disk means it is not listed in files.
  3. Plan for flat file names. On 8.3.0 and 8.3.3 gateways, the single-file route rejects a {filename} containing /, even when URL-encoded. The multipart route can return 400 bad multipart or throw a Jetty NPE when part names include /.
  4. To deploy a nested tree (for example common/buttons.css) via the API, flatten it to root-level names such as common-buttons.css, update the imports and the files list to match, and then upload. Otherwise, place the nested tree on disk under the core collection and rescan.

Check 9: Custom Icon Libraries Under the 8.3 Resource Collection

Custom icon repositories move to the same resource model, and they fail the same way. An 8.1-style SVG dropped into an icons folder is invisible to the Designer until it is wrapped as a resource.

  1. Under data/config/resources/core/, create com.inductiveautomation.perspective/ if it is not already present, then icons/ inside it.
  2. Inside icons/, create a folder named after the repository: <repository name>/.
  3. Place the SVG spritesheet in it. Any file name works, for example icons.svg.
  4. Add a config.json with a single svgFileName entry that names the spritesheet.
  5. Add a resource.json whose files array lists config.json and the spritesheet.
  6. Rescan from .../system/projects. Reading: the repository appears in the Designer's icon browser.

Re-Migrating a Theme From the 8.1 Backup and Confirming It Renders

Use this branch when Checks 2 through 6 show missing folders, a hand-damaged migrated theme, or a pre-release migration. You do not need a full gateway restore. The 8.3 gateway re-runs the theme migration on restart for anything it finds in the legacy location.

Prerequisites: gateway on 8.3.0 final or later; the untouched 8.1 theme folder and its root .css file, taken from the 8.1 backup or the .migrated copy; a maintenance window for a gateway restart.

  1. On the 8.1 copy, rewrite cross-theme imports to the ../<theme>/index.css form (Check 5). Give every shared folder a matching root .css file so the migration resolves it (Check 3). Confirm: no import references a sibling root file.
  2. Remove any legacy .backup directory from the copy. Confirm: only theme folders and their root .css files remain.
  3. Delete the existing migrated version of the theme from data/config/resources/core/com.inductiveautomation.perspective/themes/. Leaving it in place creates conflicts. Confirm: the folder is gone.
  4. Delete any resource.json or config.json sitting directly in that themes root (Check 2). Confirm: no loose files at the root.
  5. Copy the theme folder and its root .css into data/modules/com.inductiveautomation.perspective/themes on the 8.3 gateway.
  6. Restart the gateway. Confirm: the theme folder reappears under the core collection with index.css, config.json, and resource.json.
  7. Open resource.json and compare files against every local import. Add any missing entries, then rescan from .../system/projects. Confirm: the find endpoint returns the theme resource.
  8. Check isPrivate in config.json: false for the selectable theme, true for shared folders. Confirm: the Designer's session theme property lists the theme, and shared folders are hidden.
  9. Set the project's session theme to the migrated theme and open a Perspective session with browser developer tools on the network tab. Confirm: every CSS request returns successfully, with no 404 on imported files.
  10. Exercise the elements that depend on imported rules: buttons, borders, custom variables bound in component styles, and any animation classes applied to labels or containers. Confirm: each one renders exactly as it did on the 8.1 gateway, including animation classes that previously failed to trigger.

FAQ

Why does renaming the .migrated theme folders not fix Perspective themes in Ignition 8.3?

The .migrated folders are an automatic backup of the 8.1 originals, and the 8.3 gateway never reads them. The only theme path 8.3 reads is data/config/resources/core/com.inductiveautomation.perspective/themes/, and each theme there needs index.css, config.json, and resource.json.

Why does my custom theme look unstyled after importing ../light/index.css in Ignition 8.3?

Check three things. First, the gateway must be on 8.3.0 final or later; the RC did not include the restructured base themes, and light-cool/index.css returned a 404 there. Second, every local file your index.css imports must be listed in resource.json files. Third, no stray resource.json or config.json can sit at the themes root; after fixing any of these, rescan from .../system/projects.

Why does the Ignition 8.3 gateway API reject theme files in subfolders?

On 8.3.0 and 8.3.3, the single-file datafile route fails when the filename contains /, even URL-encoded. The multipart route can return 400 bad multipart or a Jetty NPE for part names containing /. Flatten the file names and update the imports and the files list, or place the nested tree on disk and rescan.

Why does adding CSS variables to the default light theme require an overrides-light resource in 8.3?

light and dark are in-memory system themes that cannot be edited. Create a core theme resource named overrides-light (or overrides-dark) with "entrypoint": "index.css" and "isPrivate": true. Its index.css must start with @import "../light/index.css"; before your :root variables, because the gateway redirects the base entry point to the override.

Back to blog