Quick Start with Pulsar Web.
How to build your first Pulsar application in 10 minutes.
Quick Start
Build your first Pulsar application in 10 minutes. This tutorial will guide you through creating a counter app with history tracking — covering components, state management, events, and styling.
Prerequisites
Before starting, make sure you have:
- Dart SDK ^3.9.0 installed
- Pulsar CLI installed:
dart pub global activate pulsar_cli
Step 1: Create a New Project
Create a new Pulsar project using the CLI:
pulsar create counter_app --template empty --use-cdn none -y
cd counter_app
This creates a minimal project structure. Open it in your editor.
Step 2: Build the Counter Component
Create a counter with history tracking. Open lib/main.dart and replace its contents:
import 'package:pulsar_web/pulsar.dart';
void main() {
mountApp(CounterApp());
}
final class CounterApp extends Component {
int count = 0;
List<String> history = [];
@override
List<Stylesheet> get styles => [
css("styles/app.css"),
];
void increment(Event e) {
morph(() {
count++;
history.add('Incremented to $count');
});
}
void decrement(Event e) {
morph(() {
count--;
history.add('Decremented to $count');
});
}
void reset(Event e) {
morph(() {
count = 0;
history.add('Reset to 0');
});
}
List<Morphic> get historyItems =>
history.reversed.map((entry) =>
Li().classes("history-item")([entry])
).toList();
@override
Morphic render() {
return Div().classes("app")([
H1()(['Counter with History']),
Div().classes("counter-display")([
Span().classes("count")(['$count']),
]),
Div().classes("controls")([
Button()
.classes("btn btn-decrement")
.onClick(decrement)
(['-']),
Button()
.classes("btn btn-reset")
.onClick(reset)
(['Reset']),
Button()
.classes("btn btn-increment")
.onClick(increment)
(['+']),
]),
if (history.isNotEmpty)
Div().classes("history")([
H2()(['History']),
Ul().classes("history-list")([...historyItems]),
]),
]);
}
}
Let's break down what's happening:
- State — count and history are fields on the component
- Events — increment, decrement, reset methods handle button clicks
- morph() — Wraps state changes to trigger re-renders
- Getter — historyItems transforms data outside render()
- render() — Pure description of UI structure
Step 3: Add Styling
Create web/styles/app.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.app {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 2rem;
max-width: 500px;
width: 100%;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 2rem;
font-size: 1.75rem;
}
.counter-display {
text-align: center;
margin-bottom: 2rem;
}
.count {
font-size: 4rem;
font-weight: 700;
color: #667eea;
display: block;
}
.controls {
display: flex;
gap: 1rem;
justify-content: center;
margin-bottom: 2rem;
}
.btn {
padding: 0.75rem 1.5rem;
font-size: 1.25rem;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-increment {
background: #10b981;
color: white;
}
.btn-increment:hover {
background: #059669;
transform: translateY(-2px);
}
.btn-decrement {
background: #ef4444;
color: white;
}
.btn-decrement:hover {
background: #dc2626;
transform: translateY(-2px);
}
.btn-reset {
background: #6b7280;
color: white;
}
.btn-reset:hover {
background: #4b5563;
transform: translateY(-2px);
}
.history {
margin-top: 2rem;
padding-top: 2rem;
border-top: 2px solid #e5e7eb;
}
.history h2 {
font-size: 1.25rem;
color: #333;
margin-bottom: 1rem;
}
.history-list {
list-style: none;
max-height: 200px;
overflow-y: auto;
}
.history-item {
padding: 0.5rem 1rem;
background: #f3f4f6;
border-radius: 6px;
margin-bottom: 0.5rem;
color: #4b5563;
font-size: 0.875rem;
}
.history-list::-webkit-scrollbar {
width: 6px;
}
.history-list::-webkit-scrollbar-track {
background: #f3f4f6;
border-radius: 3px;
}
.history-list::-webkit-scrollbar-thumb {
background: #9ca3af;
border-radius: 3px;
}
Step 4: Run the App
Start the development server:
pulsar serve --watch
Open your browser at http://localhost:8080 and you'll see your counter app running!
Try it out:
- Click + to increment
- Click - to decrement
- Click Reset to go back to 0
- Watch the history grow with each action
Step 5: Improve with Domain Objects (Optional)
Let's refactor to follow Domain Oriented UI principles. Create lib/domain/counter_state.dart:
class CounterState {
int count = 0;
List<String> history = [];
void increment() {
count++;
history.add('Incremented to $count');
}
void decrement() {
count--;
history.add('Decremented to $count');
}
void reset() {
count = 0;
history.add('Reset to 0');
}
List<String> get recentHistory => history.reversed.toList();
}
Now update lib/main.dart:
import 'package:pulsar_web/pulsar.dart';
import 'domain/counter_state.dart';
void main() {
mountApp(CounterApp());
}
final class CounterApp extends Component {
final CounterState state = CounterState();
@override
List<Stylesheet> get styles => [css("styles/app.css")];
void handleIncrement(Event e) {
morph(() => state.increment());
}
void handleDecrement(Event e) {
morph(() => state.decrement());
}
void handleReset(Event e) {
morph(() => state.reset());
}
List<Morphic> get historyItems =>
state.recentHistory.map((entry) =>
Li().classes("history-item")([entry])
).toList();
@override
Morphic render() {
return Div().classes("app")([
H1()(['Counter with History']),
Div().classes("counter-display")([
Span().classes("count")(['${state.count}']),
]),
Div().classes("controls")([
Button()
.classes("btn btn-decrement")
.onClick(handleDecrement)
(['-']),
Button()
.classes("btn btn-reset")
.onClick(handleReset)
(['Reset']),
Button()
.classes("btn btn-increment")
.onClick(handleIncrement)
(['+']),
]),
if (state.history.isNotEmpty)
Div().classes("history")([
H2()(['History']),
Ul().classes("history-list")([...historyItems]),
]),
]);
}
}
Now the business logic (increment, decrement, reset) lives in CounterState, making it testable without rendering UI.
DOU Benefit: CounterState can now be tested independently, reused in other contexts, and even used in CLI tools or backend code. The component is just a thin coordinator.
What You Learned
In this quick start, you learned:
- Components as objects — CounterApp is created once, state lives in fields
- State management — morph() wraps mutations and triggers renders
- Event handling — onClick passes method references, not calls
- Pure render() — Transformations in getters, render() describes structure
- Domain Oriented UI — Business logic extracted to CounterState
- Styling — CSS co-located in web/styles/
Next Steps
Now that you've built your first Pulsar app, explore:
- Components — Learn about component composition and lifecycle
- State Management — Lifting state, domain objects, complex state
- Routing — Build multi-page applications
- Styling — Domain styling classes, CSS frameworks
- Best Practices — Patterns that scale
Challenge: Extend the App
Try adding these features to practice:
- Add a "Clear History" button
- Add a step size input (increment/decrement by N)
- Add persistence using localStorage
- Extract styling logic to a domain object (ButtonStyles, HistoryStyles)
- Add keyboard shortcuts (arrow keys to increment/decrement)
Remember: Keep business logic in domain objects, styling logic in domain styling classes, and components focused on structure. This pattern scales from simple counters to complex applications.