Flow is a client-side navigation system that intercepts link clicks and form submissions to perform AJAX-based page transitions without full reloads. It automatically re-initializes LiveComponents and re-executes scripts after each navigation.
Flow is initialized automatically on page load. It attaches three global listeners:
click — intercepts <a> tag navigation (same-origin only)submit — intercepts GET and POST form submissionspopstate — handles browser back/forward with scroll restoration<body>, Flow uses morphdom to diff and patch only the nodes that actually changed. This preserves scroll position, focus, and CSS transitions.
<meta> and <link> tags are synced on every navigation — new ones are added, stale ones removed. Scripts are skipped to avoid re-loading JS.
<script> tags in the new body are re-executed automatically. Flow's own script and the LiveComponents runtime are skipped — they're already loaded.
document.startViewTransition() for smooth animated page switches.
Add {{ flow_scripts | raw }} in your base layout. The | raw filter is required — without it Pebble escapes the script tag.
<body>
{% block content %}{% endblock %}
{{ flow_scripts | raw }}
</body>
In production, the script is versioned as flow.js?v=1.0.0. In development, a timestamp is used to force re-fetch on every restart.
Add data-flow="false" to any link or form to bypass Flow and trigger a full page reload instead.
<!-- Default — AJAX navigation -->
<a href="/dashboard">Dashboard</a>
<!-- Full reload -->
<a href="/download/report.pdf" data-flow="false">Download PDF</a>
<!-- Form opt-out -->
<form action="/upload" method="post" data-flow="false">
<input type="file" name="file">
<button type="submit">Upload</button>
</form>
Flow dispatches events on window after each navigation — use them to re-initialize third-party libraries or track page views.
window.addEventListener('obsidian:flow:load', (event) => {
console.log('Navigated to:', event.detail.url);
hljs.highlightAll();
analytics.page(event.detail.url);
});
// Detect POST → GET redirects
window.addEventListener('obsidian:flow:redirect', (event) => {
console.log('Redirected from', event.detail.from, 'to', event.detail.to);
});
// GET navigation
window.ObsidianFlow.navigate('/profile');
// Without pushing to history
window.ObsidianFlow.navigate('/modal-content', { pushState: false });
// POST navigation
const data = new FormData();
data.append('username', 'alice');
window.ObsidianFlow.navigate('/login', { method: 'POST', body: data });
Flow prefetches links on hover to reduce perceived latency. Prefetch requests include the X-Obsidian-Prefetch: 1 header — use it server-side to skip analytics or side effects.
@GET("/dashboard")
public Object dashboard(Request req, Response res) {
if (!req.attribute("prefetch")) {
analytics.trackPageView(req);
}
return render("dashboard.html", Map.of());
}
<a href="/export/report" data-flow-timeout="15000">Export report</a>
:root {
--obsidian-flow-color: #6366f1;
}
data-flow="false" on file uploads, external redirects, and OAuth flowslive:submit are skipped automatically — handled by LiveComponentsdata-flow/ws/ prefix for WebSocket paths to avoid conflicts with Flow| Method / Property / Event | Type | Description |
|---|---|---|
| navigate(url, options?) | Promise<void> | Navigates to the given URL. Options: pushState, method, body, timeout. |
| navigating | boolean | True while a navigation fetch is in progress. |
| currentUrl | string | URL of the last successfully loaded page. |
| timeout | number | Global fetch timeout in ms. Default: 8000. Override per link via data-flow-timeout. |
| obsidian:flow:load | CustomEvent | Dispatched after each navigation. Detail: { url: string }. |
| obsidian:flow:redirect | CustomEvent | Dispatched when a POST lands on a different URL. Detail: { from: string, to: string }. |