Skip to content

Use StrataKit icons - #1587

Open
GerardasB wants to merge 44 commits into
masterfrom
gerardas/stratakit-icons
Open

GerardasB wants to merge 44 commits into
masterfrom
gerardas/stratakit-icons

Conversation

@GerardasB

@GerardasB GerardasB commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Changes

This PR adds useStrataKit preview feature, which when enabled renders StrataKit icons instead of iTwinUI icons. This can only be used after enabling the StrataKit theme bridge.

To enable the preview feature, a separate subpath is exposed to make sure that imports are not considered when the StrataKit packages are not installed, see #1587 (comment)

Notable changes:

  • StrataKit dependencies are optional and only required when enabling useStrataKit preview feature
  • iTwinUI SVG icons used in AppUI packages are replaced with StrataKit icons, see icon mapping in Add migration from iTwinUI guide stratakit#1643
  • Web font icons from @bentley/icons-generic-webfont are replaced with StrataKit icons using an internal mapping
  • Storybook Theme bridge toolbar item can be used to enable useStrataKit preview feature (this will need additional work, since different decorators are used to setup different stories)
  • Preview feature is enabled via @itwin/appui-react. The StrataKitIcon component is injected via context to lower level packages via StrataKitIconContext of @itwin/core-react

Used scripts

Script to update webFontToStrataKitIcon.ts
// Data from https://raw.githubusercontent.com/iTwin/stratakit/f6b76f3b40692939e0a3aa63077f4f2b3db9b5f2/apps/website/src/content/docs/getting-started/migration-from-itwinui.mdx
const data = {
  "2d": "2d.svg",
  // ...
};

import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

const targetPath = fileURLToPath(
  new URL("./webFontToStrataKitIcon.ts", import.meta.url)
);

const source = readFileSync(targetPath, "utf8");
const commentedPrefix = `  // "icon-`;

const updated = source
  .split("\n")
  .map((line) => {
    if (!line.startsWith(commentedPrefix)) return line;

    const iconName = line.slice(commentedPrefix.length, line.indexOf('":'));
    const skIcon = data[iconName];
    if (!skIcon) return line;

    return `  "icon-${iconName}": "@stratakit/icons/${skIcon}",`;
  })
  .join("\n");

writeFileSync(targetPath, updated);
Extract imports for useStrataKit.ts
import { webFontToStrataKitIcon } from "./appui-react/preview/use-stratakit/webFontToStrataKitIcon.js";

import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

const targetPath = fileURLToPath(new URL("./out.ts", import.meta.url));

let imports = "";
let modules = "";

for (const [iconName, skIcon] of Object.entries(webFontToStrataKitIcon)) {
  const skFileName = skIcon.slice(skIcon.lastIndexOf("/") + 1, skIcon.length);
  const skIconName = toCamelCase(skFileName).slice(0, -4);

  imports += `import svg${skIconName} from "${skIcon}";\n`;
  modules += `"${skIcon}": svg${skIconName},\n`;
}

function toCamelCase(str) {
  return str.replace(/(^\w|-\w)/g, clearAndUpper);
}

function clearAndUpper(str) {
  return str.replace(/-/, "").toUpperCase();
}

writeFileSync(targetPath, `${imports}\n\n${modules}`);

Screenshots

Screenshot Theme bridge useStrataKit preview feature
Screenshot 2026-09-11 at 17 30 51
Screenshot 2026-09-11 at 17 31 01
Screenshot 2026-09-11 at 17 31 17

Testing

/** Dynamically imports a module. Useful for optional dependencies that may not be installed in the project.
* @internal
*/
export function useOptionalModule<T>(importFunc?: () => Promise<T>) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@saskliutas, @iTwin/stratakit-developers Currently I am using dynamic imports to load the optional StrataKit modules (@stratakit/foundations and individual StrataKit icons). This adds some complexity and async behavior to the code e.g. the icons only start loading once rendered. See:

const [hrefModule, isLoading] = useOptionalModule(

const svgMeasure = async () => import("@stratakit/icons/measure.svg");

This was done to facilitate consumers that do no have StrataKit installed: using static imports would cause bundlers to fail when StrataKit is not installed (even if the code is never executed, unless the useStrataKit preview feature is enabled).

While this is the "correct" way to avoid having to release a new major version of AppUI, maybe it would be fine to either:

  • Define @stratakit/icons and @stratakit/foundations as non-optional peer dependencies (i.e. all consumers updating to a new minor version of AppUI would need to install these packages, however they would not be "used" unless the useStrataKit preview feature is enabled)
  • Define @stratakit/icons and @stratakit/foundations as dependencies (i.e. all consumers updating to a new minor version of AppUI would automatically get these packages installed). This might cause issues with multiple versions of StrataKit being installed in the same project.

@mayank99 mayank99 Jul 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that this will work in practice. Dynamic imports still get processed by build tools so that they can output the correct files (ref: Vite docs, Rsbuild docs). If the package is not installed, this could lead to a build error, making this no different from static imports. It's worth testing this claim in a standalone starter app to be sure.

@stratakit/foundations should not be a direct dependency. It will throw an error if multiple versions are detected. We've been asked to make it a peer dep even in our own packages (@stratakit/mui, @stratakit/structures).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The peerDependency approach sounds better. Then the consumer's bundler can decide when and how to bundle the icons.

@GerardasB GerardasB Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that this will work in practice.

Tried it in an empty vite project. Seems like the dev server doesn't like the dynamic import of SVG assets (import of @stratakit/foundations is working as expected): [plugin:vite:import-analysis] Failed to resolve import "__vite-optional-peer-dep:@stratakit/icons/placeholder.svg:@itwin/appui-react" from "node_modules/.vite/deps/@itwin_appui-react_tmp.js?v=6eca5e37". Does the file exist?. When built pnpm build -> pnpm preview seems to work fine for icons as well. Vite config modifications are no better than having to install the package.


Ideally, if the StrataKit imports would be done from a separate subpath that would solve all the issues (and the need for dynamic imports), not sure how feasible is that given the requirement of doing it once at an application level (not in each AppUI import). EDIT: e.g.: WIP: useStrataKit subpath export


For now, if there are no objections, to avoid complexity - let's just go with static imports and assume that consumers will install the required "optional" StrataKit dependencies.

@mayank99 mayank99 Jul 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WIP: useStrataKit subpath export

This seems good as the general approach (assuming it works with vite and rsbuild projects). I like that it hides all the complexity from the consumer.

My main feedback would be to preserve the entire import path at the call site for easier migration in the future. For example:

const { Icon } = modules?.["@stratakit/mui"];
const svgDockLeft = modules?.["@stratakit/icons/dock-left.svg"];

1:1 equivalent to:

import { Icon } from "@stratakit/mui";
import svgDockLeft from "@stratakit/icons/dock-left.svg";

@GerardasB GerardasB linked an issue Aug 7, 2026 that may be closed by this pull request
@GerardasB
GerardasB force-pushed the gerardas/stratakit-icons branch 4 times, most recently from a8eea6a to c1442c8 Compare August 31, 2026 11:52
@GerardasB
GerardasB force-pushed the gerardas/stratakit-icons branch from 443865a to 8bf5909 Compare September 9, 2026 12:51
@GerardasB
GerardasB force-pushed the gerardas/stratakit-icons branch from d43d8da to a03fe85 Compare September 11, 2026 14:30
@GerardasB GerardasB changed the title WIP: Use StrataKit icons Use StrataKit icons Sep 11, 2026
@GerardasB
GerardasB marked this pull request as ready for review September 11, 2026 14:41
@GerardasB
GerardasB requested review from a team as code owners September 11, 2026 14:41
{isLocked && <StrataKitIcon href={svgLock} iconNode={<SvgLock />} />}
{isLocked && (
<StrataKitIcon
module="@stratakit/icons/lock.svg"

@mayank99 mayank99 Sep 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the module prop added in 089997a disconnects the imports from the specifiers. In #1587 (comment), I was suggesting keeping the two close to each other to make future changes easier.

If you wanted to use the new named icon exports (e.g. svgLock & svgLockLarge), that wouldn't possible with this approach, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use StrataKit icons in AppUI

3 participants