Pulsar Web Framework
Pulsar is a simple Dart-first web framework focused on clarity, explicit behavior, and long-term maintainability.
It combines the simplicity of the web platform with the strengths of Dart, without hidden magic or unnecessary abstractions.
π Website: https://pulsar-web.netlify.app
Getting Started
Installation
Pulsar projects are created and served using the official CLI.
Activate the CLI globally:
dart pub global activate pulsar_cli
Create a new project:
pulsar create hello
cd hello
Run the development server:
pulsar serve
Note: The app will be available in your browser with live reload enabled if you use the
--watchoption.
Project Structure
A typical Pulsar project looks like this:
lib/
ββ app.dart
ββ components/
ββ hello.dart
web/
ββ index.html
ββ main.dart
ββ styles/
ββ hello.css
lib/contains all Dart components and layoutsweb/contains the entry point, HTML, and CSS assets
Pulsar intentionally avoids placing components inside web/ to prevent fragile relative imports and keep Dart code clean and package-based.
Basic Usage
Creating a Component
Components are plain Dart classes that extend Component.
import 'package:pulsar_web/pulsar.dart';
class Counter extends Component {
int count = 0;
void increment(Event event) => setState(() => count++);
void decrement(Event event) => setState(() => count--);
@override
PulsarNode render() {
return div(
children: [
h2(children: [text('$count')]),
button(onClick: decrement, children: [text('-')]),
button(onClick: increment, children: [text('+')]),
],
);
}
}
State is managed directly in Dart.
Calling setState triggers a re-render of the component.
Styling Components
Components can define their own stylesheets:
@override
List<Stylesheet> get styles => [
css('components/counter/counter.css'),
];
Note: The
css()path is always relative to theweb/directory.
Routing & Layouts
Pulsar includes a simple router with layout support.
Defining a Layout
class App extends Component {
final Component child;
App(this.child);
@override
PulsarNode render() {
return div(
children: [
h1(children: [text('Persistent Header')]),
ComponentNode(component: child),
],
);
}
}
Configuring Routes
void main() {
mountApp(
RouterComponent(
router: Router(
routes: [
Route(path: '/', builder: (_) => HomePage()),
Route(path: '/items', builder: (_) => ListPage()),
Route(
path: '/items/:id',
builder: (params) => ItemPage(params['id']!),
),
],
notFound: () => NotFoundPage(),
),
layout: (page) => AppLayout(page),
),
);
}
Routes can declare dynamic segments using :paramName.
The resolved values are passed to the builder via the params map.
Navigating Between Routes
Programmatic navigation:
navigateTo('/items/abc123');
Declarative navigation:
a(
href: '/coffees/abc123',
onClick: (e) {
e.preventDefault();
navigateTo('/coffees/abc123');
},
children: [text('Open coffee')],
);
Both approaches produce the same result. No reloads, no remounts unless necessary.
Data fetching
Pulsar does not wrap or replace the web platform.
Fetching data is done using the browser fetch API via universal_web.
import 'dart:convert';
class ListPage extends Component {
List<Map<String, dynamic>> items = [];
bool loading = true;
@override
void onMount() {
_load();
}
Future<void> _load() async {
final res = await fetch(
'https://example.com/api/items',
);
final text = await res.text().toDart;
final data = jsonDecode(text.toDart) as Map<String, dynamic>;
setState(() {
items = List<Map<String, dynamic>>.from(data['items']);
loading = false;
});
}
@override
PulsarNode render() {
if (loading) {
return div(children: [text('Loading...')]);
}
return ul(
children: items.map((c) {
return li(
children: [
text('${c['name']} β \$${c['id']}'),
],
);
}).toList(),
);
}
}
There are no implicit retries, observers, or hidden subscriptions. What you write is exactly what runs.
Component Lifecylce
Pulsar components expose a small, explicit lifecycle.
class Example extends Component {
@override
void onMount() {
// Runs once when the component is attached
}
@override
void onUpdate() {
// Runs after each update triggered by setState or update()
}
@override
void onUnmount() {
// Cleanup logic (timers, listeners, etc.)
}
@override
PulsarNode render() {
return div(children: [text('Hello')]);
}
}
Lifecycle methods are optional. If you donβt need them, you donβt pay for them.
Morphic Components
Pulsar components are morphic by design.
A component is not tied to a single immutable configuration. It can evolve over time without being destroyed and recreated.
This allows patterns such as:
- Updating route parameters without remounting
- Reusing the same component instance across navigations
- Preserving internal state intentionally
class CoffeePage extends Component {
String id;
CoffeePage(this.id);
void updateId(String newId) {
if (id != newId) {
id = newId;
update();
}
}
@override
PulsarNode render() {
return div(children: [text('Coffee ID: $id')]);
}
}
This model avoids:
- Hooks
- Controllers
- Providers
- Implicit dependency graphs
Components are stateful objects, not render functions.
Core Philosophy
Pulsar is built around one simple idea: clarity over cleverness.
It embraces Dart as it is, without fighting the language or hiding behavior behind magic. The framework favors explicit code, predictable behavior, and direct use of the web platform.
Pulsar grows through reflection, not constant iteration. Every feature exists for a reason, and unnecessary abstractions are intentionally avoided.
The goal is not to be the most powerful framework, but the one that stays understandable, maintainable, and honest over time.
Principles
Pulsar is guided by a small set of principles that shape every design decision.
-
Clarity over cleverness
If something feels smart but unclear, it does not belong in Pulsar. -
Dart first, always
Pulsar embraces Dartβs type system, null safety, and tooling. -
Explicit by design
What you write is what runs. -
Minimal abstraction
Abstractions must earn their place. -
Respect the web platform
Pulsar works with HTML, CSS, and browser semantics. -
Reflection over iteration
Design decisions are deliberate, not trend-driven.
Golden Rules
These rules act as guardrails for Pulsarβs development.
-
If a feature requires magic, it should be reconsidered.
-
If Dart already solves the problem, Pulsar must not reimplement it.
-
If an abstraction obscures behavior, it is not acceptable.
-
If something cannot be explained simply, it is probably wrong.
-
If a feature exists only for convenience, it must justify its cost.
-
If removing a feature improves clarity, it should be removed.
Pulsar values long-term clarity over short-term convenience.
Status & Stability
Pulsar is actively developed.
Version 0.4.x is considered stable and suitable for real projects.
Feedback, issues, and discussions are welcome:
π https://github.com/IgnacioFernandez1311/pulsar_web
Support Pulsar
Pulsar is free and open-source.
If it helps you build better software, consider supporting its development through GitHub Sponsors.
Your support helps keep Pulsar independent, focused, and thoughtfully evolving.
Libraries
- core/component
- engine/attribute/attribute
- engine/node/factories
- engine/node/node
- engine/renderer/diff
- engine/renderer/dom_factory
- engine/renderer/render_context
- engine/renderer/renderer
- engine/renderer/web_renderer
- engine/router/route
- engine/router/route_match
- engine/router/router
- engine/router/router_component
- engine/runtime/component_runtime
- engine/stylesheet/css_file
- engine/stylesheet/style_registry
- engine/stylesheet/stylesheet
- pulsar
- types
- utils/fetch
- utils/mount_app