January 2026

Building Our Website with Jaspr: A Pure Dart Journey

When we decided to rebuild our company website, we wanted to stay true to our Dart-first philosophy. Enter Jaspr — a modern web framework that lets you build websites entirely in Dart. Here's what we learned along the way.

What is Jaspr?

Jaspr is a web framework for Dart that brings Flutter's component model to web development. If you're familiar with Flutter, you'll feel right at home — you build UIs using StatelessComponent and StatefulComponent classes, manage state with setState(), and compose your UI from smaller, reusable components.

But Jaspr isn't just "Flutter for web". It's specifically designed for building websites with features like Static Site Generation (SSG), Server-Side Rendering (SSR), and seamless client-side hydration.

The SSR/SSG Paradigm: Understanding the Dual Environment

This is where things get interesting — and where many developers (including us initially) stumble. When you build with Jaspr, your code runs in two different environments:

  • Server-side: During build time (SSG) or request time (SSR), your Dart code runs on the server to generate HTML
  • Client-side: After the page loads in the browser, your Dart code is compiled to JavaScript and runs to add interactivity

This dual nature has profound implications. Code that works perfectly on the client might crash during server-side rendering. The classic example? Trying to access browser APIs like window or document on the server.

The dart:js_interop Challenge

We learned this lesson the hard way. We wanted to add a scroll listener to change our navigation bar's appearance. Simple enough, right? Just import dart:js_interop, convert a Dart function to JS, and add an event listener.

// This crashes on the server!
import 'dart:js_interop';

void _setupScrollListener() {
  _scrollCallback = _onScroll.toJS;  // Error: dart:js_interop not available
  window.addEventListener('scroll', _scrollCallback);
}

The problem? dart:js_interop simply doesn't exist on the server platform. The solution involves conditional imports and the universal_web package, which provides cross-platform stubs:

// Use universal_web instead - it provides stubs for server
import 'package:universal_web/js_interop.dart';
import 'package:universal_web/web.dart' as web;

void _setupScrollListener() {
  if (kIsWeb) {  // Only run on client
    _scrollCallback = _onScroll.toJS;
    web.window.addEventListener('scroll', _scrollCallback);
  }
}

The @client Annotation: Your Bridge to Interactivity

Jaspr provides the @client annotation to mark components that need client-side hydration. These components are pre-rendered on the server, then "hydrated" on the client to become interactive.

@client
class Navigation extends StatefulComponent {
  // This component will be interactive on the client
  // but still pre-rendered on the server for SEO
}

The key insight: always wrap browser-specific code in kIsWeb checks, and use universal_web instead of dart:html or dart:js_interop directly.

The RawText Trap: Staying Pure Dart

Here's a trap that's easy to fall into: Jaspr provides a RawText component that lets you inject raw HTML directly into your page. Need a script tag? Just wrap it in RawText. Need some complex HTML? RawText to the rescue.

// The easy way out - but defeats the purpose
RawText('''
  <script>
    document.getElementById('btn').addEventListener('click', () => {
      // JavaScript code here
    });
  </script>
''')

This works, but it completely defeats the purpose of using Jaspr. You're essentially writing a traditional HTML/JS website with extra steps. The whole point of Jaspr is to write pure Dart — to have type safety, refactoring support, and a unified codebase.

Instead, embrace Jaspr's event system:

// The Jaspr way - pure Dart, type-safe, refactorable
button(
  events: {
    'click': (event) {
      // Dart code here - fully type-safe!
      setState(() => _isClicked = true);
    },
  },
  [Component.text('Click me')],
)

Practical Tips from Our Migration

1. Model Classes Work Everywhere

Your data models, utility functions, and business logic can be shared between server and client. This is pure Dart — no platform-specific APIs needed.

2. Use GlobalNodeKey for DOM Access

When you need to access the actual DOM element (for scroll positions, form validation, etc.), use GlobalNodeKey instead of raw JavaScript:

final GlobalNodeKey<web.HTMLDivElement> _containerKey = GlobalNodeKey();

// In build method:
div(
  key: _containerKey,
  // ...
)

// To access:
final element = _containerKey.currentNode;
element?.scrollLeft  // Type-safe DOM access

3. Conditional Rendering Just Works

Unlike raw HTML, conditional rendering in Jaspr is natural Dart:

// Clean, readable, type-safe
div([
  if (_isMenuOpen) _buildMobileMenu(),
  if (_isLoading) LoadingSpinner() else ContentWidget(),
])

4. Extract Styles and Data

Keep your components clean by extracting hardcoded styles into style constants and hardcoded content into data classes. This makes your code more maintainable and easier to update.

Styling with Tailwind CSS

Jaspr has first-class support for Tailwind CSS through the jaspr_tailwind package. This was a game-changer for us — we get all the utility-first CSS goodness while staying in pure Dart.

// pubspec.yaml
dependencies:
  jaspr_tailwind: ^0.3.6

// In your component
div(
  classes: 'flex items-center justify-between p-4 bg-gray-900 rounded-lg',
  [/* children */],
)

The jaspr_tailwind package watches your Dart files and automatically generates the CSS for the Tailwind classes you use. No need for a separate build step or configuration headaches.

Custom Styles When Needed

Sometimes Tailwind classes aren't enough. Jaspr's Styles API lets you add custom CSS properties:

div(
  classes: 'flex items-center',  // Tailwind for common patterns
  styles: Styles(
    raw: {
      'font-family': 'Quicksand, sans-serif',  // Custom when needed
      'letter-spacing': '0.6px',
    },
  ),
  [/* children */],
)

We found this combination powerful: Tailwind for rapid prototyping and common patterns, custom styles for brand-specific details. The best of both worlds.

Conclusion

Building with Jaspr requires a mindset shift. You're not writing HTML templates with some Dart sprinkled in — you're building a proper application that happens to output HTML. Embrace the Dart-first approach:

  • Use kIsWeb to guard browser-specific code
  • Prefer universal_web over dart:html
  • Use events: {} instead of <script> tags
  • Let the type system help you catch errors
  • Resist the RawText escape hatch unless you truly need raw HTML

The result? A website that's fully type-safe, easy to refactor, and leverages all the tooling you love from the Dart ecosystem. Plus, you get SSG/SSR for free — your site loads fast and is SEO-friendly.

We're excited about Jaspr's future and proud to have our website running on pure Dart. If you're a Flutter developer looking to bring your skills to the web, give Jaspr a try!

By Bilal Aksoy • January 2026

← Back to Blog