Installation
Install Chameleon for the framework you build with, or for several frameworks in one app. Each option below covers the package, the setup, and licensing your build.
Installation options
Install the web components to use Chameleon's cw-* elements in any framework, or with none. For React or Blazor, use their tabs; for more than one framework in one app, use the Multiple frameworks tab.
Install the package
Add the package with your package manager of choice.
npm install @materia-technologies-inc/chameleon-components-web
Register the components
Import the package once, in your app's entry point. The import registers every cw-* element, so nothing else needs importing.
import "@materia-technologies-inc/chameleon-components-web";
Load the stylesheet
Import the package's stylesheet once, next to the package import in your entry point, so the components don't flash unstyled while they load:
import "@materia-technologies-inc/chameleon-components-web/chameleon.css";
If you don't bundle CSS, link the same file from your page with a <link rel="stylesheet"> instead.
Load the fonts
Chameleon doesn't ship fonts. Add these two stylesheets once, in the <head> of your page: Material Symbols, without which every icon shows its name instead of its symbol, and Noto Sans, the text face the components use. If you prefer, you can download the fonts and add them to your application's CSS.
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200">
Ask for all four axes, as above. Without them Google Fonts serves a fixed version, and the icons' size, weight, fill, and grade settings have no effect.
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap">
Noto Sans is the default, in the only weights the components use: 400, 500 and 700, upright and italic. To use another font, load it and set the --ct-ref-typeface-plain and --ct-ref-typeface-brand tokens.
If your site sets a Content Security Policy, add https://fonts.googleapis.com to style-src-elem (or to style-src, if you don't set style-src-elem) and https://fonts.gstatic.com to font-src. If you self-host the fonts, font-src 'self' is enough.
License your build
Every build of your app needs a license grant: a small signed script that records which license the build is under. The grant tooling fetches it during the build. Without one, a build stops, because an app built without a grant would show every component watermarked and disabled.
The steps below are the same for both Community and Pro Chameleon.
Create a build token
- Sign in to chameleonui.io.
- Open your account page, press License management, then press Create build token.
- Copy the token straight away. You see it only once. If you lose it, Replace build token issues a new one, and the old one stops working at once.
A build token lasts 90 days. It's a credential, so keep it out of source control, build logs, and issue reports.
Set the token where you build
The build reads the token from the CHAMELEON_LICENSE_TOKEN environment variable. Set it so that it survives a restart. A variable set for one terminal is gone when that terminal closes.
Windows
In PowerShell:
[Environment]::SetEnvironmentVariable("CHAMELEON_LICENSE_TOKEN", "<your token>", "User")
Close and reopen every terminal and editor so that they pick it up.
macOS
The default shell is zsh. Add this line to ~/.zprofile with a text editor, which keeps the token out of your shell history:
export CHAMELEON_LICENSE_TOKEN="<your token>"
Open a new terminal. Visual Studio Code and JetBrains IDEs read your login shell's environment when they start, so restart them too. Start any other tool from a terminal rather than the Dock.
Linux
Add the same line to ~/.profile with a text editor:
export CHAMELEON_LICENSE_TOKEN="<your token>"
Sign out and back in, so that your desktop session and everything started from it has the variable. If your terminal does not read ~/.profile, add the line to ~/.bashrc as well.
Continuous integration
Store the token as a secret in your CI system, and pass it to the build step as CHAMELEON_LICENSE_TOKEN. In GitHub Actions:
- run: npm run build
env:
CHAMELEON_LICENSE_TOKEN: ${{ secrets.CHAMELEON_LICENSE_TOKEN }}
Add the grant to your build
Install the grant tooling as a development dependency. It needs Node 20 or newer.
npm install --save-dev @materia-technologies-inc/chameleon-grant
Vite
// vite.config.js
import { defineConfig } from "vite";
import { chameleonGrant } from "@materia-technologies-inc/chameleon-grant/vite";
export default defineConfig({
plugins: [chameleonGrant()],
});
The plugin writes the grant into your page's <head>, ahead of everything else, in vite dev as well as vite build.
Any other bundler
Run the command-line stamp in every build, after your bundler, since many bundlers empty their output directory first:
npx chameleon-grant stamp --out dist
Then load the file it writes as a classic script, before your bundle:
<script src="/chameleon.grant.js"></script>
<script type="module" src="/main.js"></script>
Deploy chameleon.grant.js with your site, but don't commit it: it holds a live grant.
Using Rollup directly? Import chameleonGrant from @materia-technologies-inc/chameleon-grant/rollup and pass target: "application". Rollup can't tell an application from a library, so the plugin refuses to stamp a grant until you say which one you're building.
A library is not licensed on its own. Only an application carries a grant, because a library that carried one would hand your license to everyone who installs it. Vite refuses a build that sets build.lib (or environments.<name>.build.lib), and Rollup refuses one without target: "application". For a widget that other pages embed, stamp the host page with the command above.
What keeps working
- A deployed application keeps working. The grant in a shipped build never expires.
- You can build offline for up to 15 days after your last connected build on that machine. The tooling keeps the last grant it obtained.
- The token expires after 90 days, and the build warns you from 14 days before. Replace it in License management, then update every place that holds it, including CI secrets.
When a build fails with a Chameleon error code
| The build says | What it means | What to do |
|---|---|---|
CHAMELEON0001 |
This build cannot see a token | Set the CHAMELEON_LICENSE_TOKEN variable as above, then open a new terminal or restart your editor |
CHAMELEON0002 |
Chameleon could not be reached, and no grant on this machine is newer than 15 days | Check that your network allows chameleonui.io and grants.chameleonui.io, then build again |
CHAMELEON0003 |
The token was not accepted or was refused. It is mistyped, was revoked, is more than 90 days old, or your organization removed you | Copy the token again. If it still fails, create a new token; if you build for an organization, ask its administrator |
CHAMELEON0004 |
The build is a library | See the note on libraries above |
CHAMELEON0005 |
What came back was not a grant | Build again. If it persists, update the Chameleon packages, then contact support |
Check that it works
Put a button in a page and load it. Press it: a loaded button shows a press effect, and the placeholder the stylesheet draws before the components load doesn't. To be certain, run customElements.get("cw-button") in the browser console. It returns a class only when the components loaded.
<cw-button variant="filled">Save changes</cw-button>
Install the React package to use Chameleon as typed React components: CrButton, CrTextField, and the rest. The package includes the web components, so you don't install them separately.
Install the package
Add the package with your package manager of choice.
npm install @materia-technologies-inc/chameleon-components-react
Load the stylesheet
Import the package's stylesheet once, in your entry point, so the components don't flash unstyled while they load:
import "@materia-technologies-inc/chameleon-components-react/chameleon.css";
If you don't bundle CSS, link the same file from your page with a <link rel="stylesheet"> instead.
Load the fonts
Chameleon doesn't ship fonts. Add these two stylesheets once, in the <head> of your page: Material Symbols, without which every icon shows its name instead of its symbol, and Noto Sans, the text face the components use. If you prefer, you can download the fonts and add them to your application's CSS.
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200">
Ask for all four axes, as above. Without them Google Fonts serves a fixed version, and the icons' size, weight, fill, and grade settings have no effect.
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap">
Noto Sans is the default, in the only weights the components use: 400, 500 and 700, upright and italic. To use another font, load it and set the --ct-ref-typeface-plain and --ct-ref-typeface-brand tokens.
If your site sets a Content Security Policy, add https://fonts.googleapis.com to style-src-elem (or to style-src, if you don't set style-src-elem) and https://fonts.gstatic.com to font-src. If you self-host the fonts, font-src 'self' is enough.
License your build
Every build of your app needs a license grant: a small signed script that records which license the build is under. The grant tooling fetches it during the build. Without one, a build stops, because an app built without a grant would show every component watermarked and disabled.
The steps below are the same for both Community and Pro Chameleon.
Create a build token
- Sign in to chameleonui.io.
- Open your account page, press License management, then press Create build token.
- Copy the token straight away. You see it only once. If you lose it, Replace build token issues a new one, and the old one stops working at once.
A build token lasts 90 days. It's a credential, so keep it out of source control, build logs, and issue reports.
Set the token where you build
The build reads the token from the CHAMELEON_LICENSE_TOKEN environment variable. Set it so that it survives a restart. A variable set for one terminal is gone when that terminal closes.
Windows
In PowerShell:
[Environment]::SetEnvironmentVariable("CHAMELEON_LICENSE_TOKEN", "<your token>", "User")
Close and reopen every terminal and editor so that they pick it up.
macOS
The default shell is zsh. Add this line to ~/.zprofile with a text editor, which keeps the token out of your shell history:
export CHAMELEON_LICENSE_TOKEN="<your token>"
Open a new terminal. Visual Studio Code and JetBrains IDEs read your login shell's environment when they start, so restart them too. Start any other tool from a terminal rather than the Dock.
Linux
Add the same line to ~/.profile with a text editor:
export CHAMELEON_LICENSE_TOKEN="<your token>"
Sign out and back in, so that your desktop session and everything started from it has the variable. If your terminal does not read ~/.profile, add the line to ~/.bashrc as well.
Continuous integration
Store the token as a secret in your CI system, and pass it to the build step as CHAMELEON_LICENSE_TOKEN. In GitHub Actions:
- run: npm run build
env:
CHAMELEON_LICENSE_TOKEN: ${{ secrets.CHAMELEON_LICENSE_TOKEN }}
Add the grant to your build
Install the grant tooling as a development dependency. It needs Node 20 or newer.
npm install --save-dev @materia-technologies-inc/chameleon-grant
Vite
// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { chameleonGrant } from "@materia-technologies-inc/chameleon-grant/vite";
export default defineConfig({
plugins: [react(), chameleonGrant()],
});
The plugin writes the grant into your page's <head>, ahead of everything else, in vite dev as well as vite build.
If a server renders your pages the plugin has no page to write into. vite build then puts the grant at the top of your entry chunk instead, but vite dev adds nothing, so the components show watermarked and disabled while you develop. For a server-rendered app, run vite build --watch instead of vite dev, or use the command-line stamp below.
Any other bundler
Run the command-line stamp in every build, after your bundler, since many bundlers empty their output directory first:
npx chameleon-grant stamp --out dist
Then load the file it writes as a classic script, before your bundle:
<script src="/chameleon.grant.js"></script>
<script type="module" src="/main.js"></script>
Deploy chameleon.grant.js with your site, but don't commit it: it holds a live grant.
Using Rollup directly? Import chameleonGrant from @materia-technologies-inc/chameleon-grant/rollup and pass target: "application". Rollup can't tell an application from a library, so the plugin refuses to stamp a grant until you say which one you're building.
A library is not licensed on its own. Only an application carries a grant, because a library that carried one would hand your license to everyone who installs it. Vite refuses a build that sets build.lib (or environments.<name>.build.lib), and Rollup refuses one without target: "application". For a widget that other pages embed, stamp the host page with the command above.
What keeps working
- A deployed application keeps working. The grant in a shipped build never expires.
- You can build offline for up to 15 days after your last connected build on that machine. The tooling keeps the last grant it obtained.
- The token expires after 90 days, and the build warns you from 14 days before. Replace it in License management, then update every place that holds it, including CI secrets.
When a build fails with a Chameleon error code
| The build says | What it means | What to do |
|---|---|---|
CHAMELEON0001 |
This build cannot see a token | Set the CHAMELEON_LICENSE_TOKEN variable as above, then open a new terminal or restart your editor |
CHAMELEON0002 |
Chameleon could not be reached, and no grant on this machine is newer than 15 days | Check that your network allows chameleonui.io and grants.chameleonui.io, then build again |
CHAMELEON0003 |
The token was not accepted or was refused. It is mistyped, was revoked, is more than 90 days old, or your organization removed you | Copy the token again. If it still fails, create a new token; if you build for an organization, ask its administrator |
CHAMELEON0004 |
The build is a library | See the note on libraries above |
CHAMELEON0005 |
What came back was not a grant | Build again. If it persists, update the Chameleon packages, then contact support |
Check that it works
Render a button in a component. Press it: a loaded button shows a press effect, and the placeholder the stylesheet draws before the components load doesn't. To be certain, run customElements.get("cw-button") in the browser console. It returns a class only when the components loaded.
import { CrButton } from "@materia-technologies-inc/chameleon-components-react";
export function SaveBar() {
return <CrButton variant="filled">Save changes</CrButton>;
}
Install the Blazor package to use Chameleon as Razor components: CbButton, CbTextField, and the rest.
Install the package
Add the NuGet package to your project.
dotnet add package Chameleon.Components.Blazor
Register the namespace
Add a @using to _Imports.razor so the Cb* components and their enums are in scope across your app.
@using Chameleon.Blazor
The namespace is Chameleon.Blazor, which differs from the package name.
Register the services
Add Chameleon's services in Program.cs. Every Cb* component reads its configuration from them; without them, the first component throws.
using Chameleon.Blazor;
builder.Services.AddChameleonServices(options => { });
Program.cs needs its own using: the one in _Imports.razor reaches Razor files only.
Set app-wide defaults in the lambda, or leave it empty to keep Chameleon's.
Reference the scripts in your application
Add three references to the <head> of your root document, App.razor: the stylesheet, your license grant, and the component bundle.
<link rel="stylesheet" href="@Assets["_content/Chameleon.Components.Blazor/.generated-content/chameleon-components-blazor.css"]" />
<script src="chameleon.grant.js"></script>
<script type="module" src="@Assets["_content/Chameleon.Components.Blazor/.generated-content/chameleon-components-blazor.js"]"></script>
The chameleon.grant.js line loads your license grant, which the next section sets up.
Load the fonts
Chameleon doesn't ship fonts. Add these two stylesheets once, in the <head> of your page: Material Symbols, without which every icon shows its name instead of its symbol, and Noto Sans, the text face the components use. If you prefer, you can download the fonts and add them to your application's CSS.
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200">
Ask for all four axes, as above. Without them Google Fonts serves a fixed version, and the icons' size, weight, fill, and grade settings have no effect.
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap">
Noto Sans is the default, in the only weights the components use: 400, 500 and 700, upright and italic. To use another font, load it and set the --ct-ref-typeface-plain and --ct-ref-typeface-brand tokens.
If your site sets a Content Security Policy, add https://fonts.googleapis.com to style-src-elem (or to style-src, if you don't set style-src-elem) and https://fonts.gstatic.com to font-src. If you self-host the fonts, font-src 'self' is enough.
License your build
Every build of your app needs a license grant: a small signed script that records which license the build is under. The package fetches it during dotnet build and dotnet publish. Without one the build stops, because an app built without a grant would show every component watermarked and disabled.
The steps below are the same for both Community and Pro Chameleon.
Create a build token
- Sign in to chameleonui.io.
- Open your account page, press License management, then press Create build token.
- Copy the token straight away. You see it only once. If you lose it, Replace build token issues a new one, and the old one stops working at once.
A build token lasts 90 days. It's a credential, so keep it out of source control, build logs, and issue reports.
Set the token where you build
The build reads the token from the CHAMELEON_LICENSE_TOKEN environment variable. Set it so that it survives a restart. A variable set for one terminal is gone when that terminal closes.
Windows
In PowerShell:
[Environment]::SetEnvironmentVariable("CHAMELEON_LICENSE_TOKEN", "<your token>", "User")
Close and reopen every terminal and editor so that they pick it up. That includes Visual Studio and Rider.
macOS
The default shell is zsh. Add this line to ~/.zprofile with a text editor, which keeps the token out of your shell history:
export CHAMELEON_LICENSE_TOKEN="<your token>"
Open a new terminal. Visual Studio Code and JetBrains IDEs, Rider included, read your login shell's environment when they start, so restart them too. Start any other tool from a terminal rather than the Dock.
Linux
Add the same line to ~/.profile with a text editor:
export CHAMELEON_LICENSE_TOKEN="<your token>"
Sign out and back in, so that your desktop session and everything started from it has the variable. If your terminal does not read ~/.profile, add the line to ~/.bashrc as well.
Continuous integration
Store the token as a secret in your CI system, and pass it to the build step as CHAMELEON_LICENSE_TOKEN. In GitHub Actions:
- run: dotnet publish -c Release
env:
CHAMELEON_LICENSE_TOKEN: ${{ secrets.CHAMELEON_LICENSE_TOKEN }}
What the package does for you
Nothing to install. On every build and publish, the package fetches your grant and writes it to wwwroot/chameleon.grant.js, which the tag in App.razor loads.
- Keep that tag a classic script, not
type="module". A classic script runs as the page is parsed, so the grant is in place before any component asks for it. - Add
wwwroot/chameleon.grant.jsto your.gitignore. It holds a live grant. Deploy it, but don't commit it. - A Razor class library is never given a grant. Only an application carries one, because a library that carried one would hand your license to every project that references it. The application gets the grant when it builds, but only if it references the Chameleon package itself, not only through your library. Add the package to the application as well.
What keeps working
- A deployed application keeps working. The grant in a shipped build never expires.
- You can build offline for up to 15 days after your last connected build on that machine. The package keeps the last grant it obtained.
- The token expires after 90 days, and the build warns you from 14 days before. Replace it in License management, then update every place that holds it, including CI secrets.
When a build fails with a Chameleon error code
| The build says | What it means | What to do |
|---|---|---|
CHAMELEON0001 |
This build cannot see a token | Set the CHAMELEON_LICENSE_TOKEN variable as above, then open a new terminal or restart your editor |
CHAMELEON0002 |
Chameleon could not be reached, and no grant on this machine is newer than 15 days | Check that your network allows chameleonui.io and grants.chameleonui.io, then build again |
CHAMELEON0003 |
The token was not accepted or was refused. It is mistyped, was revoked, is more than 90 days old, or your organization removed you | Copy the token again. If it still fails, create a new token; if you build for an organization, ask its administrator |
CHAMELEON0004 |
A class library holds a chameleon.grant.js |
Delete the file. Only the application carries a grant, and only when it references the Chameleon package itself |
CHAMELEON0005 |
What came back was not a grant | Build again. If it persists, update the Chameleon packages, then contact support |
Check that it works
Put a button in a page and run the app. Press it: a loaded button shows a press effect, and the placeholder the stylesheet draws before the components load doesn't. To be certain, run customElements.get("cw-button") in the browser console. It returns a class only when the components loaded.
<CbButton Variant="ButtonVariant.Filled">Save changes</CbButton>
Install Chameleon this way when one app uses more than one framework or a framework in addition to the web components (cw-* elements). Install the web components once, then the lite package of each framework.
Full and lite
Every framework package comes in two variants, and the difference is only where the components themselves live.
| Variant | What it contains | Use it when |
|---|---|---|
| Full | The framework wrappers and a copy of the web components inside the same bundle | The application uses that one framework and nothing else |
| Lite | The framework wrappers alone. It requires the web components package beside it | The application uses more than one framework |
A custom element can only be registered once per page. Two full packages, or a full package beside the web components package, each load their own copy, and the second one to load throws NotSupportedError, because the components are already defined. So an application that uses two frameworks installs the web components once and takes lite everywhere.
One shape for every combination. Install @materia-technologies-inc/chameleon-components-web, then the lite package of each framework. That holds for two frameworks or five, and it is the only shape that keeps working as you add another.
Install the packages
The web components, once:
npm install @materia-technologies-inc/chameleon-components-web
Then the lite package of each framework you use. For React:
npm install @materia-technologies-inc/chameleon-components-react-lite
For Blazor, install both halves: the NuGet package your Razor components compile against, and the npm package your bundler builds:
dotnet add package Chameleon.Components.Blazor.Lite
npm install @materia-technologies-inc/chameleon-components-blazor-lite
Import them once, in one place
Your application has one JavaScript entry point, and every Chameleon import belongs in it. The web components import comes first, because everything else expects the elements to exist.
// main.js
import "@materia-technologies-inc/chameleon-components-web";
import "@materia-technologies-inc/chameleon-components-blazor-lite";
Each import has a side effect and no API to call: the first registers every cw-* element, the second installs the Blazor interop helpers. React's lite package is imported the ordinary way, by the components you use.
That gives one bundle with one copy of the components, and one bundle carries one grant.
Load the stylesheet
Import the package's stylesheet once, next to the package import in your entry point, so the components don't flash unstyled while they load:
import "@materia-technologies-inc/chameleon-components-web/chameleon.css";
If you don't bundle CSS, link the same file from your page with a <link rel="stylesheet"> instead. A Blazor host can skip this step: the Blazor package's own stylesheet already includes it.
Load the fonts
Chameleon doesn't ship fonts. Add these two stylesheets once, in the <head> of your page: Material Symbols, without which every icon shows its name instead of its symbol, and Noto Sans, the text face the components use. If you prefer, you can download the fonts and add them to your application's CSS.
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200">
Ask for all four axes, as above. Without them Google Fonts serves a fixed version, and the icons' size, weight, fill, and grade settings have no effect.
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap">
Noto Sans is the default, in the only weights the components use: 400, 500 and 700, upright and italic. To use another font, load it and set the --ct-ref-typeface-plain and --ct-ref-typeface-brand tokens.
If your site sets a Content Security Policy, add https://fonts.googleapis.com to style-src-elem (or to style-src, if you don't set style-src-elem) and https://fonts.gstatic.com to font-src. If you self-host the fonts, font-src 'self' is enough.
License the build once
The license grant is a signed script that says which license a build was made under, and an application carries exactly one. In a multi-framework application it is the JavaScript build that stamps it, whichever frameworks are in the page.
- Set
CHAMELEON_LICENSE_TOKENas the Web components tab describes. The steps, for each operating system and for CI, are the same. - Add the grant tooling to your bundler: the Vite plugin or the command-line stamp. In a Blazor host, read the grant reaches a Razor page differently first.
- Don't add a grant step to the
.csproj. The lite NuGet package doesn't stamp one, on purpose: an application carries one grant, and your bundle already has it.dotnet buildneeds no token at all.
When a build fails with a Chameleon error code
| The build says | What it means | What to do |
|---|---|---|
CHAMELEON0001 |
This build cannot see a token | Set the CHAMELEON_LICENSE_TOKEN variable as above, then open a new terminal or restart your editor |
CHAMELEON0002 |
Chameleon could not be reached, and no grant on this machine is newer than 15 days | Check that your network allows chameleonui.io and grants.chameleonui.io, then build again |
CHAMELEON0003 |
The token was not accepted or was refused. It is mistyped, was revoked, is more than 90 days old, or your organization removed you | Copy the token again. If it still fails, create a new token; if you build for an organization, ask its administrator |
CHAMELEON0004 |
The build is a library | Stamp the application instead. A library is never licensed on its own: only the application's bundle carries a grant |
CHAMELEON0005 |
What came back was not a grant | Build again. If it persists, update the Chameleon packages, then contact support |
The Blazor nuance
A Blazor host renders its own page, so two things differ from a pure JavaScript application. Both come from Blazor, not from Chameleon.
The grant reaches a Razor page differently
Blazor generates App.razor itself, so a tool that writes the grant into an HTML page has nothing to write into.
Two approaches work, and the first is simpler:
Vite, which puts the grant at the top of your entry chunk when there is no page to write into. Nothing else in the application has to know, and there is no file to deploy or ignore. Build with
vite build, and while you develop runvite build --watchrather thanvite dev: Blazor serves the page, not Vite's dev server, andvite devadds no grant. Each rebuild asks for the grant again. The worked example below uses it.The command line, for a bundler with no Chameleon plugin. Run it in every build, after the bundler, since many bundlers empty their output directory first:
npx chameleon-grant stamp --out wwwrootcontent_copy <script src="chameleon.grant.js"></script>content_copy Keep that tag a classic script, before your bundle, so the grant is in place before any component asks for it. Add
wwwroot/chameleon.grant.jsto your.gitignore: deploy it, but do not commit it.
Reference your bundle through @Assets[...]
Blazor fingerprints static files and rewrites the plain names through the <ImportMap /> in your page. Naming your bundle directly loads a second copy of it: every element is defined twice, and the console shows NotSupportedError.
<script type="module" src="@Assets["js/main.js"]"></script>
The rest of the Blazor setup is unchanged
@* _Imports.razor *@
@using Chameleon.Blazor
// Program.cs
builder.Services.AddChameleonServices(options => { });
AddChameleonServices registers the configuration service every Cb* component resolves; without it the first component throws. Prerendering needs no special handling: the prerendered cw-* elements upgrade once your bundle defines them. Blazor waits for DOMContentLoaded before it starts, and a browser fires that only after the page's module scripts have run, so define everything at the top level of your bundle, not after an await.
A worked example: React and Blazor in one application
A Blazor Web App that also renders React islands. It uses Vite, so the grant rides inside the bundle and the Razor page carries no grant script at all.
What it installs
dotnet add package Chameleon.Components.Blazor.Lite
npm install @materia-technologies-inc/chameleon-components-web \
@materia-technologies-inc/chameleon-components-react-lite \
@materia-technologies-inc/chameleon-components-blazor-lite \
react react-dom
npm install --save-dev @materia-technologies-inc/chameleon-grant \
vite @vitejs/plugin-react
Two lite packages and one copy of the components. The grant tooling is a build dependency: it fetches the grant, and your app doesn't ship it.
vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { chameleonGrant } from "@materia-technologies-inc/chameleon-grant/vite";
export default defineConfig({
plugins: [react(), chameleonGrant()],
build: {
// Blazor serves wwwroot, so the bundle goes there, under a name App.razor can use.
outDir: "wwwroot/js",
emptyOutDir: true,
rollupOptions: {
input: "frontend/main.jsx",
output: { entryFileNames: "main.js" },
},
},
});
The plugin puts the license grant at the top of the entry chunk, ahead of everything the bundle imports. That is why this application needs no chameleon.grant.js and no script tag for one.
frontend/main.jsx
// The components, once. Everything after this line uses that one copy.
import "@materia-technologies-inc/chameleon-components-web";
// The Blazor interop helpers, resolved by this bundler rather than by an import map.
import "@materia-technologies-inc/chameleon-components-blazor-lite";
import { createRoot } from "react-dom/client";
import { CrButton, CrTextField } from "@materia-technologies-inc/chameleon-components-react-lite";
function ProjectPanel() {
return (
<>
<CrTextField variant="outlined" label="Project name" />
<CrButton variant="filled" onClick={() => console.log("saved")}>Save changes</CrButton>
</>
);
}
// Blazor calls this once its component has rendered the host element.
window.mountProjectPanel = (host) => {
const root = createRoot(host);
root.render(<ProjectPanel />);
return root;
};
ProjectPanel.razor
React mounts only into an element a Razor component has rendered. An interactive page replaces its prerendered markup when Blazor starts, so a React tree mounted into that markup disappears a moment after it appears.
@inject IJSRuntime JS
@implements IAsyncDisposable
<div @ref="host"></div>
@code {
private ElementReference host;
private IJSObjectReference? root;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
root = await JS.InvokeAsync<IJSObjectReference>("mountProjectPanel", host);
}
}
public async ValueTask DisposeAsync()
{
if (root is not null)
{
try
{
await root.InvokeVoidAsync("unmount");
await root.DisposeAsync();
}
catch (JSDisconnectedException)
{
}
}
}
}
Blazor renders the host element and the rest of the page, and React renders inside it. Both drive the same custom elements.
App.razor
<head>
<link rel="stylesheet" href="@Assets["_content/Chameleon.Components.Blazor/.generated-content/chameleon-components-blazor.css"]" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap" />
<ImportMap />
<HeadOutlet />
</head>
<body>
<Routes />
<script type="module" src="@Assets["js/main.js"]"></script>
<script src="@Assets["_framework/blazor.web.js"]"></script>
</body>
Three things are deliberately absent: a grant script, a Chameleon component script, and an import map entry of your own. Your bundle carries the first two, and <ImportMap /> is Blazor's own, which is what @Assets[...] needs. The one Chameleon file the page names is the lite package's stylesheet: it holds the rules that hide each element until it has loaded. The two font stylesheets come from Google Fonts, as Load the fonts above describes.
A page that uses both
@page "/projects"
@rendermode InteractiveServer
@using Chameleon.Blazor
<CbButton Variant="ButtonVariant.Tonal" OnClick="Refresh">Refresh</CbButton>
<ProjectPanel />
@code {
private void Refresh() { }
}
The build order
npx vite build # fetches the grant, writes wwwroot/js/main.js
dotnet build # no token needed: the lite package stamps nothing
Run Vite first. dotnet build copies wwwroot as it finds it, so a bundle built afterwards doesn't reach the app. While you develop, run npx vite build --watch beside dotnet watch.
The lite NuGet package brings no component script into the page; your bundle carries it, so App.razor names only the lite package's stylesheet.
If you do not run a bundler, take the full package
The lite NuGet package also ships its JavaScript as a static file, and that file imports the web components by name. A browser cannot resolve a name on its own, so loading it directly needs an import map in your page:
<script type="importmap">
{ "imports": { "@materia-technologies-inc/chameleon-components-web": "/js/chameleon-components-web.js" } }
</script>
An application with a bundler never needs that. A Blazor application with no other framework should take Chameleon.Components.Blazor, the full package, which resolves nothing at runtime and stamps its own grant.
What each package needs
| Package | Needs beside it | Stamps a grant |
|---|---|---|
chameleon-components-web |
Nothing | Your bundler's grant tooling does |
chameleon-components-react-lite |
chameleon-components-web |
No |
chameleon-components-blazor-lite (npm) |
chameleon-components-web |
No |
Chameleon.Components.Blazor.Lite (NuGet) |
The npm package above, in your bundle | No |
Check that it works
For each package you use, follow the Check that it works section on its tab.