Creating Widgets
Add your own Flutter widgets to Fuse
Every Fuse widget has two parts: a JS type declaration (what props it accepts) and a Dart widget builder (how it renders). This page walks through creating a custom widget from scratch.
The two-file pattern
Let's build a <badge> widget that renders a Flutter Chip.
Step 1: Declare the JSX element
For app-level widgets, add a .d.ts file in your project:
import type { ColorInput, FlexInput } from "solid-fuse";
declare global {
namespace JSX {
interface IntrinsicElements {
badge: {
label: string;
color?: ColorInput;
children?: any;
flex?: FlexInput;
};
}
}
}For library packages, add the element to your package's jsx.d.ts instead.
Step 2: Write the Dart widget
import 'package:flutter/material.dart';
import 'package:solid_fuse/solid_fuse.dart';
class FuseBadge extends StatelessWidget {
const FuseBadge({super.key, required this.node});
final FuseNode node;
@override
Widget build(BuildContext context) {
final label = node.string('label') ?? '';
final color = node.color('color') ?? Colors.blue;
return Chip(
label: Text(label),
backgroundColor: color,
side: BorderSide.none,
);
}
}Step 3: Register
runtime.registerWidget('badge', FuseBadge.new);Now you can use it in JSX:
<badge label="New" color="green" />FuseNode prop accessors
FuseNode extends FuseMap, which provides typed accessors for reading props. All return nullable — if the prop wasn't set in JSX, you get null.
| Accessor | Returns | What it parses |
|---|---|---|
string(key) | String? | String values |
double(key) | double? | Numbers |
int(key) | int? | Integers |
bool(key) | bool? | Booleans |
map(key) | FuseMap? | Nested objects (also has typed accessors) |
list<T>(key) | List<T>? | Arrays |
color(key) | Color? | Hex strings, named colors, RGB/HSL objects |
edgeInsets(key) | EdgeInsets? | Number or {top, left, ...} object |
borderRadius(key) | BorderRadius? | Number or {topLeft, ...} object |
boxDecoration(key) | BoxDecoration? | Full decoration with color, borders, shadows, gradient |
alignment(key) | Alignment? | Position names ("center", "topLeft", etc.) |
clipBehavior(key) | Clip | "hardEdge", "antiAlias", "none" (defaults to Clip.none) |
Nested objects return FuseMap, which has the same accessors — so you can do node.map('config')?.string('mode').
The flex + flexChildren mechanic
If you add
flex?: FlexInputto your JSX type and usenode.flexChildrenin your Dart widget, layout just works. Users of your widget can passflex={{ direction: "horizontal", gap: 8 }}and children will arrange automatically — you don't write any layout code.
This is how every built-in element handles children. Here's a widget that supports it:
myCard: {
children?: any;
flex?: FlexInput; // ← add this
};class FuseMyCard extends StatelessWidget {
const FuseMyCard({super.key, required this.node});
final FuseNode node;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: node.flexChildren, // ← reads flex prop automatically
),
);
}
}Now users can do:
<myCard flex={{ gap: 8 }}>
<text>Title</text>
<text>Subtitle</text>
</myCard>How flexChildren works
- Reads the
flexprop from the node - If there's a
flexconfig: wraps children in a FlutterFlexwidget (Row/Column) with the specified gap, alignment, and justification - If there's only one child and no flex config: returns the child directly (no wrapper)
- This single-child optimization avoids unnecessary layout nodes
childWidgets vs flexChildren
node.flexChildren— reads theflexprop and applies layout. Use this for anything that should support user-configured layout. This is the recommended default.node.childWidgets— raw list of child widgets, no layout applied. Use this when you're passing children to a Flutter widget that manages its own layout (likeNavigatororStack).
Event callbacks
To support event callbacks from Dart to JS, use node.function():
// Returns a callback if the prop was set in JSX, null if not
final onTap = node.function('onTap');
// Safe to use directly — null means "no handler"
GestureDetector(
onTap: onTap,
child: node.flexChildren,
)Under the hood: when you write onTap={() => doSomething()} in JSX, the actual function is stored in a JS-side handler map. The Dart prop is just true — a marker that a handler exists. When Dart calls the callback, it sends a message back to JS via the bridge, which looks up and executes the real function.
To send data back with the callback:
node.function('onChange')?.call(newValue);StatefulWidget
Use StatefulWidget when your widget needs persistent Dart-side state (animations, text editing, controllers). Flutter preserves State across rebuilds because each FuseNodeWidget uses ValueKey(node.id) — so your state survives prop changes and tree updates.
class FuseTextInput extends StatefulWidget {
const FuseTextInput({super.key, required this.node});
final FuseNode node;
@override
State<FuseTextInput> createState() => _FuseTextInputState();
}
class _FuseTextInputState extends State<FuseTextInput> {
late final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
onChanged: (value) => widget.node.function('onChange')?.call(value),
);
}
}Where should your widget live?
| Location | When to use | How to register |
|---|---|---|
| In your app | Domain-specific, uses app-level deps | runtime.registerWidget(...) after packages |
| In a Fuse package | Reusable across apps, has its own deps | Via register() function + fuse link |
| In solid-fuse | Core Flutter primitives any app needs | PR to the framework |
For creating a reusable package, see Packaging.