Spark Components
Spark Components are reusable, encapsulated units of UI that render on the server (SSR) and hydrate on the client for interactivity. They leverage Declarative Shadow DOM for style encapsulation and consistent rendering.
Component File Structure
Components in Spark follow a three-file pattern. This structure enables isomorphic rendering where the same component code works on both server and client.
lib/components/
├── counter/
│ ├── counter.dart # Export with conditional import
│ ├── counter_base.dart # Source code you write
│ └── counter_base.impl.dart # GENERATED (don't edit)
- `counter.dart` - The export file with conditional imports
- `counter_base.dart` - Your component source code
- `counter_base.impl.dart` - Generated file with hydration logic (do not edit)
Conditional Imports
The export file uses conditional imports to load the correct implementation based on the platform:
// counter.dart
export 'counter_base.dart'
if (dart.library.html) 'counter_base.impl.dart'
if (dart.library.io) 'counter_base.impl.dart';
By default, this exports the base file. When running in the browser (`dart.library.html`) or VM (`dart.library.io`), it exports the generated impl file which includes the hydration and reactivity logic.
Writing a Component
Components are plain Dart classes with a few required elements. Here is a complete example:
import 'package:spark_framework/spark.dart';
@Component(tag: Counter.tag)
class Counter {
static const tag = 'interactive-counter';
Counter({this.value = 0});
@Attribute()
int value;
Element render() {
return div(className: 'counter', [
button(['-'], onClick: (_) => value--),
span([value]),
button(['+'], onClick: (_) => value++),
]);
}
}
Required Elements
1. @Component Annotation
The `@Component` annotation registers your class as a custom element. Pass the tag name:
@Component(tag: Counter.tag)
class Counter {
2. Static Tag Constant
Define a static constant for the custom element tag name. The tag must be hyphenated (contain a dash) per the Web Components spec:
static const tag = 'interactive-counter';
3. Constructor
Define a constructor with named parameters for initial state. These values can be set when using the component:
Counter({this.value = 0});
4. render() Method
The `render()` method returns the Element structure of your component. Use the element helper functions like `div()`, `button()`, `span()`:
Element render() {
return div(className: 'counter', [
button(['-'], onClick: (_) => value--),
span([value]),
button(['+'], onClick: (_) => value++),
]);
}
Reactive State
@Attribute Annotation
To make a field reactive, annotate it with `@Attribute`. When the field changes, Spark automatically:
- Updates the component state
- Re-renders the component via `render()`
- Patches the real DOM to match the new state
- Synchronizes the DOM attribute (e.g., `value="5"`) for styling or accessibility
@Attribute()
int value;
Auto-Hydration
When a component is hydrated on the client, Spark automatically parses the initial attributes from the SSR HTML and updates the component fields. This ensures your component state matches what the server rendered.
Styling WebComponents
Use the `adoptedStyleSheets` getter to define scoped styles for your component. These styles are encapsulated within the Shadow DOM and won't leak to other elements.
Stylesheet get adoptedStyleSheets => css({
'.counter': .typed(
display: .inlineFlex,
alignItems: .center,
gap: .px(16),
padding: .symmetric(.px(8), .px(16)),
background: 'var(--primary, #000)',
borderRadius: .px(8),
),
'button': .typed(
background: 'var(--primary, #000)',
color: .white,
border: .none,
width: .px(32),
height: .px(32),
borderRadius: .percent(50),
cursor: .pointer,
),
'button:hover': .typed(opacity: CssNumber(0.8)),
});
Key points about component styling:
- Styles are scoped to the component's Shadow DOM
- Use the `css()` function with selector keys
- Use `.typed()` for type-safe CSS properties
- Use helpers like `.px()`, `.rem()`, `.percent()` for values
- CSS variables like `var(--primary)` work for theming
See the Styling page for detailed documentation on CSS helpers.
Registering Components in Pages
To ensure the hydration script knows about your component, register it in your `Page` class using the `components` getter:
@Page(path: '/')
class HomePage extends WebsitePage<void> {
@override
List<Type> get components => [Counter];
// ...
}
Simply list the component types that your page uses. Spark will automatically register them for hydration.
Using Components
Use your component in pages by calling the constructor and then `render()`:
div([
h1(['My App']),
// Render with initial state
Counter(value: 5).render(),
]);
The initial `value` will be rendered on the server and hydrated on the client. Users can interact with the component immediately after hydration.
Isomorphic Rendering
Spark components work seamlessly on both server and client:
- **Server (SSR)**: The `render()` method produces HTML including the component tag and Declarative Shadow DOM.
- **Client (Hydration)**: The browser parses the HTML, upgrades the custom element, and Spark activates the event listeners.
This architecture provides fast initial page loads (no JavaScript needed for first paint) while enabling full interactivity after hydration.