alpine-turnout
v1.2.4
Published
AlpineJS Turnout Switch
Downloads
181
Maintainers
Readme
Alpine Turnout
A lightweight SPA Switch for Alpine.js built for speed and state persistence.
Unlike traditional routers that destroy and recreate DOM elements on every navigation, Alpine Turnout focuses on DOM preservation.
It treats your routes like railroad tracks: every section stays "alive" in the DOM. This preserves the internal state—meaning input fields, scroll positions, and component variables remain exactly as the user left them—while the "Turnout" logic reactively switches the view and URL to the correct destination.
Why Turnout?
🛤️ Zero-Config: Set up your HTML layout normally and use the
x-routedirective to declare your "tracks." No complex routing manifests required.💾 State Persistence: Forms, scroll positions, and component data are preserved. When you navigate away and back, everything is exactly where you left it.
⚡ Instant Switching: No re-mounting or re-fetching logic on every click. It’s the fastest way to navigate an Alpine app.
🍦 Alpine-Native: Built specifically for the Alpine ecosystem. It uses a global store and works with a single declarative directive.
🎬 Seamless Transitions: Works out-of-the-box with Alpine's built-in
x-transitionfor smooth UI entries and exits.🪶 Ultra-Lightweight: The entire library is only 3 kB. High performance with zero bloat.
🔍 SEO Ready: Since your content lives in the DOM, it remains fully indexable by search engines like Google and DuckDuckGo.
Example Code Snippet
<div x-data>
<nav>
<a href="/">Home</a>
<a href="/user/123">User Profile</a>
</nav>
<section x-route="/">
<h1>Welcome Home</h1>
<input type="text" placeholder="I stay alive!">
</section>
<section x-route="/user/:id">
<h1 x-text="'User Profile: ' + id"></h1>
</section>
</div>No config, just your Alpine.js HTML as you are used to. Add a x-route = /user/:id attribute and have your id directly available!
Kickstart
Turnout Playground Example 1
Turnout Playground Example 2
Turnout Playground Example 3
Installation
Via CDN (recommended)
Include the script before Alpine.js:
<script src="https://unpkg.com/alpine-turnout" defer></script>
<script src="https://unpkg.com/alpinejs" defer></script>Via NPM module
Install Alpine Turnout:
npm install alpine-turnoutInitialize Alpine.js and Alpine Turnout as modules with the following code:
import Alpine from 'alpinejs';
import AlpineTurnout from 'alpine-turnout';
Alpine.plugin(AlpineTurnout);
Alpine.start();Then launch your dev environment with vite:
npm run devUsage
1. Define your tracks(/routes)
Create a nice layout in html. Then use the x-route and x-title directives:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Alpine Turnout</title>
<script src="//unpkg.com/alpine-turnout" defer></script>
<script src="//unpkg.com/alpinejs" defer></script>
<link rel="stylesheet" href="//unpkg.com/@picocss/pico">
</head>
<body class="container" x-data="{}">
<h1 x-data x-text="$store.turnout.title"></h1>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/user/john">Profile</a></li>
<li><a href="/search">Search</a></li>
</ul>
</nav>
<article>
<div x-route="/" x-title="Welcome Home" x-transition>
<p>This is the homepage.</p>
</div>
<div x-route="/user/:name" x-title="User Profile" x-transition>
<p>Hello, <strong x-text="name"></strong>!</p>
</div>
<div x-route="/search" x-title="Search" x-transition>
<div x-data="{ query: '' }">
<input type="text" x-model="query" placeholder="Type here...">
<p>Your input is preserved even if you switch tabs!</p>
</div>
</div>
</article>
</body>
</html>Go Here for a more
extensivelive example!Go Here for a more functional
note applive example!Go Here for a demonstration
studio applive example!Go Here and check out our
/examples/*directory for more.
2. Navigation
Turnout automatically intercepts any internal <a href="/user/john">Visit John</a> links. You can also navigate programmatically:
<button @click="$store.turnout.go('/user/john')">Visit John</button>
How it Works
When you define an x-route, Alpine Turnout does three things:
Registers the path: Adds the pattern to a global registry.
Injects Scope: Makes route parameters (like
:name) available directly to the HTML inside that div.Manages Visibility: Uses
x-showlogic under the hood. When the URL matches, the "track" becomes visible; otherwise, it is hidden withdisplay: none.
API Reference
Global Store: $store.turnout
Property | Type | Description
--- | --- | ---
path | String | The current URL pathname.
title | String | The value of x-title for the active route.
notFound | Boolean | True if the current path matches no registered routes.
go(path) | Function | Programmatically navigate to a new route.
Directives
x-route="[path]"
Used on a div or section to define a "track". Elements with this directive are automatically toggled based on the URL.
Static Routes:
x-route="/about"Dynamic Routes:
x-route="/post/:id"(makesidavailable in local scope).Wildcard (Custom 404):
x-route="*"
x-arrive="[expression]"
The "Arrival" hook. Fires every time the route becomes active.
Use this to fetch fresh data or reset a form when the user navigates to the page.
Example: x-arrive="getWeather()"
x-leave="[expression]"
The "Departure" hook. Fires when the user navigates away from this route.
Use this to stop timers, cancel requests, or save draft data.
Example: x-leave="stopAutoRefresh()"
Features & Behavior
📜 Scroll Memory
Alpine Turnout automatically remembers the scroll position of every route.
When navigating via links, it returns you to your previous scroll depth on that specific "track".
When clicking a new unique path for the first time, it defaults to the top (0,0) with a smooth behavior.
⚓ Anchor Support (#hash)
The router detects fragment identifiers. If a URL contains a #, the router will:
Resolve the correct x-route.
Wait for the DOM to render.
Smoothly scroll the element with the matching id into view.
🛰️ Link Interception
<a> tags are intercepted automatically if they point to an internal path (starting with /).
Internal links: Trigger an Alpine Turnout "switch" without a page reload.
External links: Ignored by the router, allowing standard browser behavior.
Default 404 Behavior
If no x-route="*" is found and the user hits an unregistered path, Turnout automatically injects a "Dead End" 404 section into your main element to prevent a blank screen.
Transitions
Because Turnout uses Alpine's visibility toggling, you can use standard transitions. Note that we recommend setting a leave.duration.0ms if you want the "old" page to disappear instantly while the new one fades in.
<div x-route="/fast"
x-transition.duration.500ms
x-transition:leave.duration.0ms>
...
</div>Comparison with alpine-router(s)
Subject | alpine-router(s) | alpine-turnout --- | --- | --- DOM Logic | Destroys/Creates | Hides/Shows (Persistent) State | Reset on nav | Preserved (Forms/Input) Performance | Lower Memory | Faster Switching Best For | Massive apps | One-pagers & Dashboards
SEO Proof
Most modern routers (React Router, Vue Router) are "empty" until JavaScript runs. Bots often see a blank page on the first pass.
Alpine Turnout’s Edge: Since all your "tracks" (the divs with x-route) are physically present in your HTML file, a crawler like Googlebot sees all your content immediately when it reads the source code.
The Result: Your internal pages are indexed much more easily than with a standard SPA.
🚀 Deployment
Since this is a Single Page Application (SPA) using the History API, your web server should be configured to serve index.html for all requests that don't match a static file.
Example for Nginx:
location / {
try_files $uri $uri/ /index.html;
}Example for Netlify:
Simply include a file named netlify.toml in the publish directory of your repository:
[[redirects]]
from = "/*"
to = "/index.html"
status = 200🧪 Testing
This project is tested using Vitest and JSDOM. Because Alpine.js initializes asynchronously, the test suite ensures that routes are correctly registered and cleared.
To run the tests:
npm install
npm testTest Cases
Our suite covers the following test cases:
initializes and shows the home route by default
navigates to a parameterized "track" and updates the view
updates parameters reactively without re-mounting the element
renders a 404 terminal when a "track" is not found
persists state (like attributes or input) when switching "tracks"
intercepts internal links and prevents default behavior
ignores external links and allows standard navigation
triggers x-arrive when a route becomes active
triggers x-leave when moving away from a route





