Flow

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.

How it works

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 submissions
  • popstate — handles browser back/forward with scroll restoration
morphdom diff Instead of replacing the entire <body>, Flow uses morphdom to diff and patch only the nodes that actually changed. This preserves scroll position, focus, and CSS transitions.
Head sync <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 re-execution Inline <script> tags in the new body are re-executed automatically. Flow's own script and the LiveComponents runtime are skipped — they're already loaded.
View Transitions If the browser supports the View Transitions API (Chrome 111+), Flow wraps DOM updates in document.startViewTransition() for smooth animated page switches.
Include in your layout

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.

Opting out

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>
Navigation events

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);
});
Programmatic navigation
// 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 });
Prefetch

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());
}
Per-link timeout & progress bar
Override timeout per link (default: 8000ms)
<a href="/export/report" data-flow-timeout="15000">Export report</a>
Customize progress bar color
:root {
    --obsidian-flow-color: #6366f1;
}
💡 Important
  • • Use data-flow="false" on file uploads, external redirects, and OAuth flows
  • • Forms with live:submit are skipped automatically — handled by LiveComponents
  • • External links are never intercepted regardless of data-flow
  • • Use the /ws/ prefix for WebSocket paths to avoid conflicts with Flow
API reference
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 }.