Skip to content

Basics

Client-side navigation allows your application to feel like a single-page app while maintaining the server-driven architecture that Laravel developers know and trust. Instead of full page reloads, Hyper fetches new content and updates the page seamlessly, preserving scroll position and providing instant feedback.

What Is Client-Side Navigation?

Traditional web navigation triggers a full page reload every time you click a link. The browser discards the entire page, fetches new HTML from the server, and rebuilds everything from scratch. This works, but it's slow and creates a jarring experience.

Client-side navigation intercepts link clicks, fetches the new content via AJAX, and updates only the parts of the page that changed. The browser's address bar updates, the back button works naturally, but your page never fully reloads.

The data-navigate Attribute (Hyper)

Hyper provides the data-navigate attribute to enable client-side navigation on links and forms. Add it to any container, and Hyper automatically intercepts navigation within that element.

blade
<a href="/dashboard" data-navigate="true">Dashboard</a>

When clicked, this link doesn't trigger a full page reload. Instead, Hyper:

  1. Prevents the default navigation
  2. Fetches the new content via AJAX with special headers
  3. Updates the browser's URL
  4. Patches the DOM with the server's response

The result feels instant and smooth.

The simplest way to use client-side navigation is on standard links:

blade
<nav data-navigate="true">
    <a href="/">Home</a>
    <a href="/about">About</a>
    <a href="/contact">Contact</a>
</nav>

Every link inside the nav element now uses client-side navigation. The attribute works on the container, so you don't need to add it to every individual link.

You can also add it directly to specific links:

blade
<a href="/users" data-navigate="true">View Users</a>
<a href="/reports" data-navigate="true">View Reports</a>

How It Works

When you click a navigated link, here's what happens behind the scenes:

  1. Click Intercepted: Hyper's event listener catches the click
  2. Server Request: An AJAX request is sent with these special headers:
    • HYPER-NAVIGATE: true
    • HYPER-NAVIGATE-KEY: true (or your custom key)
  3. Server Response: Your Laravel controller returns content (usually a fragment)
  4. URL Update: The browser's address bar updates via the History API
  5. DOM Patch: The new content replaces the target element using DOM morphing
  6. Signals Sync: Any signal updates from the server apply automatically

The browser's back and forward buttons work naturally because Hyper uses the History API properly.

Navigation keys let you target specific parts of your page for updates. This is powerful for complex layouts where different links should update different sections.

blade
<!-- Main navigation updates the full page -->
<a href="/dashboard" data-navigate__key.main="true">Dashboard</a>

<!-- Sidebar navigation updates only the sidebar -->
<a href="/sidebar/recent" data-navigate__key.sidebar="true">Recent Items</a>

The __key.name modifier sets a navigation key. Your backend controller can check which key was used and respond accordingly:

php
public function dashboard()
{
    $data = ['stats' => $this->getStats()];

    // Main navigation: return full page
    if (request()->isHyperNavigate('main')) {
        return hyper()->fragment('dashboard', 'page', $data);
    }

    // Sidebar navigation: return only sidebar
    if (request()->isHyperNavigate('sidebar')) {
        return hyper()->fragment('dashboard', 'sidebar', $data);
    }

    // Direct visit: return full view
    return view('dashboard', $data);
}

This pattern lets you create sophisticated layouts where different navigation contexts update different page regions without duplicating controller logic.

Form Navigation

GET forms automatically work with navigation. When a user submits a search form, Hyper intercepts it and navigates to the result:

blade
<form action="/search" method="GET" data-navigate="true">
    <input type="text" name="q" placeholder="Search..." />
    <button type="submit">Search</button>
</form>

Submitting this form triggers navigation to /search?q=your+query, updating the page without a reload. This is perfect for search interfaces, filters, and any GET-based form.

POST, PUT, PATCH, and DELETE forms use Hyper's action system instead (covered in the Actions documentation).

The @navigate Action (Hyper)

While data-navigate works great on links and containers, sometimes you need to trigger navigation from within event handlers or expressions. The @navigate() action gives you programmatic control over navigation from the frontend.

Basic Usage

Trigger navigation from any event:

blade
<button data-on:click="@navigate('/dashboard')">
    Go to Dashboard
</button>

When clicked, this navigates to /dashboard just like a data-navigate link would, but you have full control over when and how it happens.

Pass a navigation key as the second parameter:

blade
<button data-on:click="@navigate('/sidebar/recent', 'sidebar')">
    Load Recent
</button>

The backend can detect this key and respond with just the sidebar fragment.

JSON Query Navigation

Instead of manually building query strings, pass an object as the first parameter:

blade
<button data-on:click="@navigate({category: 'electronics', page: 1})">
    Show Electronics
</button>

This navigates to ?category=electronics&page=1. The object approach is cleaner and handles encoding automatically.

Combining with Signal Values

Use signal values in your navigation:

blade
<div data-signals="{search: '', category: ''}">
    <input data-bind="search" placeholder="Search..." />

    <select data-bind="category">
        <option value="">All</option>
        <option value="electronics">Electronics</option>
        <option value="books">Books</option>
    </select>

    <button data-on:click="@navigate({search: $search, category: $category, page: 1}, 'filters')">
        Apply Filters
    </button>
</div>

When clicked, the button navigates with the current signal values. This pattern is perfect for filter forms where you want explicit submission rather than live updates.

The third parameter accepts options for parameter merging and history control:

blade
<!-- Merge with existing parameters -->
<button data-on:click="@navigate({sort: 'price'}, 'filters', {merge: true})">
    Sort by Price
</button>

<!-- Preserve only specific parameters -->
<button data-on:click="@navigate({page: 1}, 'filters', {only: ['search', 'category']})">
    Reset to Page 1
</button>

<!-- Replace history instead of push -->
<button data-on:click="@navigate('/modal', 'modal', {replace: true})">
    Open Modal
</button>

Available options:

  • merge - Merge with existing query parameters
  • only - Array of parameters to preserve
  • except - Array of parameters to exclude
  • replace - Use replaceState instead of pushState

Clearing Parameters

Pass empty strings to remove parameters:

blade
<button data-on:click="@navigate({search: '', category: '', page: 1})">
    Clear All Filters
</button>

Empty values are removed from the URL, keeping it clean.

Real-World Pattern: Search Form

Here's a complete search interface using @navigate:

blade
<div data-signals="{
    _search: '{{ request()->search ?? '' }}',
    _category: '{{ request()->category ?? '' }}'
}">
    <form data-on:submit__prevent="@navigate({search: $_search, category: $_category, page: 1}, 'filters', {merge: true})">
        <input
            type="search"
            data-bind="_search"
            placeholder="Search products..."
        />

        <select data-bind="_category">
            <option value="">All Categories</option>
            <option value="electronics">Electronics</option>
            <option value="books">Books</option>
        </select>

        <button type="submit">Search</button>
    </form>

    <button data-on:click="$_search = ''; $_category = ''; @navigate({search: '', category: '', page: 1}, 'clear')">
        Clear
    </button>
</div>

This pattern:

  • Uses local signals (_search, _category) for form state
  • Navigates on form submission with current filter values
  • Resets to page 1 when filters change
  • Provides a clear button that resets both signals and URL

The @navigate() action is covered in much more depth in the Advanced Patterns section, including timing modifiers, complex options, and integration with custom JavaScript.

Skipping Navigation

Sometimes you need certain links to bypass client-side navigation. Use data-navigate-skip to opt out:

blade
<div data-navigate="true">
    <!-- This link uses navigation -->
    <a href="/internal">Internal Page</a>

    <!-- This link does not (full page reload) -->
    <a href="/logout" data-navigate-skip>Logout</a>
</div>

Hyper also automatically skips:

  • External links (different origin)
  • Download links (download attribute present)
  • Links explicitly skipped with data-navigate-skip

Progressive Enhancement

Navigation works as progressive enhancement. If JavaScript fails to load or is disabled, links fall back to standard browser navigation automatically. Your application remains functional.

When building your backend responses, use the web() method to provide fallback content:

php
public function index()
{
    $contacts = Contact::paginate(10);

    return hyper()
        ->fragment('contacts.index', 'list', compact('contacts'))
        ->web(view('contacts.index', compact('contacts')));
}

If the request is a Hyper navigation request, the fragment is returned. If it's a regular page load (first visit, JavaScript disabled, direct URL access), the full view is returned.

Common Patterns

blade
<nav class="navbar" data-navigate__key.main="true">
    <a href="/" class="nav-link">Home</a>
    <a href="/products" class="nav-link">Products</a>
    <a href="/about" class="nav-link">About</a>
</nav>
blade
<div data-navigate__key.pagination="true">
    {{ $contacts->links() }}
</div>

Laravel's pagination links automatically work with navigation when wrapped in a navigation-enabled container.

Tab Switching

blade
<div>
    <!-- Tab headers -->
    <div class="tabs" data-navigate__key.tabs="true">
        <a href="/profile/overview">Overview</a>
        <a href="/profile/settings">Settings</a>
        <a href="/profile/security">Security</a>
    </div>

    <!-- Tab content updated by server response -->
    @fragment('tab-content')
    <div id="tab-content">
        <!-- Server determines which tab to show -->
    </div>
    @endfragment
</div>
  • Query Parameters - Learn how to preserve filters and search terms during navigation
  • Programmatic Navigation - Navigate from backend controllers using hyper()->navigate()
  • Advanced Patterns - Combine navigation with modifiers for debouncing, throttling, and more
  • Actions - Understand how POST/PUT/PATCH/DELETE operations differ from GET navigation