# External Widgets
*External [widgets](../../01_Onboarding/02_Concepts/03_Widgets.md) let you extend the Ivy Framework with custom React components built and bundled separately from the core framework. Use them for domain-specific UI (e.g. diagrams, charts, or rich editors) without coupling that code to the framework backend.*
## Architecture Overview
1. **C# proxy** — A record inheriting from `WidgetBase<T>` with `[ExternalWidget]`, defining [props](../../01_Onboarding/02_Concepts/03_Widgets.md) and [events](../../01_Onboarding/02_Concepts/05_EventHandlers.md).
2. **React component** — The actual UI, built with standard React and tooling (e.g. Vite).
3. **Build pipeline** — MSBuild runs the frontend build and embeds the output (JS/CSS) as resources in the widget assembly.
The host [app](../../01_Onboarding/02_Concepts/10_Apps.md) loads the script and CSS from embedded resources and renders your component, passing props and wiring events back to C#.
## Scaffolding with the CLI
You can generate a new external widget with the Ivy [CLI](../../01_Onboarding/03_CLI/01_CLIOverview.md) so namespace, names, and build match the framework:
```terminal
ivy widget
Namespace: ExternalWidget
Widget: MyWidget
```
## C# Backend
Create a record that inherits from `WidgetBase<T>` and mark it with `[ExternalWidget]`. The attribute tells the framework where to find the bundled script and (optionally) CSS, and which export/global name to use. See Widgets and Event handlers for the basics.
```csharp
using Ivy.Core;
using Ivy.Core.ExternalWidgets;
using Ivy.Shared;
namespace MyProject.Widgets;
[ExternalWidget(
"frontend/dist/ExternalWidget.js",
StylePath = "frontend/dist/style.css",
ExportName = "MyWidget",
GlobalName = "MyProject_Widgets_MyWidget")]
public record MyWidget : WidgetBase<MyWidget>
{
public MyWidget(string? label = null)
{
Label = label;
}
internal MyWidget() { }
[Prop] public string? Label { get; set; }
[Event] public Func<Event<MyWidget>, ValueTask>? OnClick { get; set; }
}
```
- **Script path** — Path to the JS file relative to the project (and to embedded resources). Often `frontend/dist/...`.
- **StylePath** — Optional path to a CSS file. If omitted, include styles in the JS bundle.
- **ExportName** — Name of the React component export the loader should use.
- **GlobalName** — Must match the Vite library `name` (and the global variable the IIFE assigns). Use the full namespace with dots replaced by underscores (e.g. `MyProject_Widgets_MyWidget`).
> **info:** Use `[Prop]` for data and `[Event]` for callbacks. You can add extension methods for a fluent API (e.g. `.Label("...")`, `.HandleClick(...)`).
## Frontend (Vite Library)
The frontend should be a separate project (e.g. a `frontend/` folder) set up as a **library** build.
### Vite configuration
Build an IIFE so the host can load one script and get a global. The `name` in `build.lib` must match `GlobalName` in C#.
```typescript
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
export default defineConfig({
plugins: [
react({
jsxRuntime: 'classic', // Use global React; required for Ivy host
}),
],
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
name: 'MyProject_Widgets_MyWidget',
fileName: () => 'ExternalWidget.js',
formats: ['iife'],
},
rollupOptions: {
external: ['react', 'react-dom'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
},
extend: false,
},
},
},
});
```
> **info:** Using `fileName: () => 'ExternalWidget.js'` avoids Vite adding suffixes like `.iife.js`, so the path matches what you put in `[ExternalWidget]`.
### Entry point
Export your widget component and assign it to `window` under the same name as `build.lib.name` so the IIFE loader can find it.
```typescript
// src/index.ts
import './style.css';
import { MyWidget } from './MyWidget';
if (typeof window !== 'undefined') {
(window as unknown as Record<string, unknown>).MyProject_Widgets_MyWidget = {
MyWidget,
};
}
export { MyWidget };
```
### React component
Ivy passes props (including `id`, `width`, `height`, `onIvyEvent`, `events`) and optional custom props (e.g. `label`). Use `onIvyEvent(eventName, widgetId, args)` to fire events back to C#.
```typescript
// src/MyWidget.tsx
import React from "react";
import { IvyEventHandler } from "./types";
import { getWidth, getHeight } from "./styles";
interface MyWidgetProps {
id: string;
width?: string;
height?: string;
onIvyEvent: IvyEventHandler;
events?: string[];
label?: string;
}
export const MyWidget: React.FC<MyWidgetProps> = ({
id,
width = "Full",
height = "Full",
onIvyEvent,
events = [],
label,
}) => {
const handleClick = () => {
if (events.includes("OnClick")) {
onIvyEvent("OnClick", id, []);
}
};
const style: React.CSSProperties = {
...getWidth(width),
...getHeight(height),
};
return (
<div
style={style}
className="p-4 border rounded-lg bg-[var(--background)] text-[var(--foreground)] border-[var(--border)]"
>
<button
onClick={handleClick}
className="px-4 py-2 rounded transition-colors bg-[var(--primary)] text-white hover:opacity-90"
>
{label ?? "Click me"}
</button>
</div>
);
};
```
Use Ivy theme variables ([Colors](../../04_ApiReference/IvyShared/Colors.md): `--primary`, `--background`, `--foreground`, `--border`, etc.) so the widget matches the host app ([Theming](../../01_Onboarding/02_Concepts/12_Theming.md)). Size props use Ivy’s [Size](../../04_ApiReference/IvyShared/Size.md) format (e.g. `Full`, `Units:80`); you can parse them in the component or in a small helper.
## Project structure and build
### Standalone widget project
For a reusable widget (e.g. NuGet or shared repo), use a dedicated project and folder **outside** any host app directory so the host does not compile the widget’s sources.
Typical layout:
```text
MyWidget/
├── MyWidget.cs
├── MyWidget.csproj
└── frontend/
├── package.json
├── vite.config.ts
├── tsconfig.json
└── src/
├── index.ts
├── MyWidget.tsx
└── style.css
```
In the `.csproj`:
- Embed the built assets.
- Run the frontend build before the C# build.
```xml
<ItemGroup>
<EmbeddedResource Include="frontend/dist/**/*" />
</ItemGroup>
<Target Name="BuildFrontend" BeforeTargets="Build" Condition="Exists('frontend/package.json')">
<Exec Command="npm install" WorkingDirectory="frontend" />
<Exec Command="npm run build" WorkingDirectory="frontend" />
</Target>
```
> **info:** Use forward slashes in paths (`frontend/dist/**/*`) for cross-platform builds.
### Integrated pattern (inside host app)
When the widget lives inside the host app (e.g. `HostApp/Widgets/MyWidget/`), add a `<ProjectReference>` to the widget project.
```xml
<ItemGroup>
<ProjectReference Include="Widgets/MyWidget/MyWidget.csproj" />
</ItemGroup>
```
### Multiple widgets in one bundle (same frontend)
You can ship **several widgets from one frontend project**: one Vite build produces a single JS bundle, and multiple C# widget records point to that same script. Each widget type is resolved by `ExportName` from the same global object. The backend serves the same embedded file for all of them; the browser may cache it per URL.
- **One frontend** — one `frontend/` project, one `npm run build`, one output file (e.g. `ExternalWidgets.js`).
- **Same script path and GlobalName** — every C# widget uses the same `[ExternalWidget("frontend/dist/ExternalWidgets.js", ..., GlobalName = "MyProject_Widgets")]` so they all use the same global object.
- **Different ExportName** — each C# record must specify the React component name explicitly: `ExportName = "MyWidget"`, `ExportName = "AnotherWidget"`, etc. Do not rely on `"default"` for multi-widget bundles.
**Vite and entry point:** Use one library name for the whole bundle and assign all components to that global:
```typescript
// vite.config.ts — one name for the whole bundle
name: 'MyProject_Widgets',
fileName: () => 'ExternalWidgets.js',
// src/index.ts
(window as any).MyProject_Widgets = {
MyWidget,
AnotherWidget,
};
```
**C#:** Use the same `Script path` and `GlobalName` for each widget, and different `ExportName` values so the loader picks the right component from the same global:
```csharp
[ExternalWidget("frontend/dist/ExternalWidgets.js", ExportName = "MyWidget", GlobalName = "MyProject_Widgets")]
public record MyWidget : WidgetBase<MyWidget> { ... }
[ExternalWidget("frontend/dist/ExternalWidgets.js", ExportName = "AnotherWidget", GlobalName = "MyProject_Widgets")]
public record AnotherWidget : WidgetBase<AnotherWidget> { ... }
```
## Host requirements
External widgets that externalize React expect the host app to provide React (and ReactDOM) on the global object.
The host’s entry point should set:
```typescript
(window as any).React = React;
(window as any).ReactDOM = ReactDOM; // or createRoot etc.
```
Ivy’s standard host (e.g. [Chrome](../../01_Onboarding/02_Concepts/11_Chrome.md)) does this. If you see “Global not found” or React-related errors, ensure the host app exposes these globals before any external widget script runs.
## Troubleshooting
| Issue | What to check |
| ----- | ------------- |
| **Script resource not found** | Path in `[ExternalWidget]` must match the embedded path (project-relative, e.g. `frontend/dist/ExternalWidget.js`). Resource name is assembly name + path with `/` → `.`. After changing `fileName` in Vite, run `dotnet clean` then `dotnet build`. |
| **Global not found** | `GlobalName` in C# must equal Vite `build.lib.name`. In `src/index.ts`, assign the export to `window[GlobalName]`. Use `jsxRuntime: 'classic'` so the bundle uses the global React. |
| **Duplicate type / CS0579** | Widget project is under the host app and the host is compiling its files. Exclude the widget directory in the host's `.csproj` so only the widget project builds it (see below). |
| **Invalid hook call / multiple React** | Widget must not bundle React when the host provides it. Keep `react` and `react-dom` in `external` and `globals` in Vite, and use `jsxRuntime: 'classic'`. |
| **Wrong filename (.iife.js)** | Set `fileName: () => 'ExternalWidget.js'` in `vite.config.ts` so the output name has no extra suffix. |
**Excluding the widget folder (integrated pattern):** When the widget project lives inside the host app directory, exclude it from the host’s compilation so the host does not compile the widget’s sources. In the host’s `.csproj`:
```xml
<PropertyGroup>
<DefaultItemExcludes>$(DefaultItemExcludes);Widgets/MyWidget/**</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="Widgets/MyWidget/MyWidget.csproj" />
</ItemGroup>
```
Alternatively use explicit removes: `<Compile Remove="Widgets/MyWidget/**/*.cs" />` and `<None Remove="Widgets/MyWidget/**/*" />`.