Skip to content

Refs & Observers

Datastar provides tools for accessing DOM elements directly and triggering actions based on visibility or time intervals. These features enable patterns like lazy loading, infinite scroll, auto-refresh dashboards, and element manipulation without writing imperative JavaScript.

Element References

data-ref (Datastar)

The data-ref attribute creates a signal that contains a reference to the DOM element itself:

blade
<div data-signals="{}">
    <button data-on:click="$container.scrollIntoView({behavior: 'smooth'})" class="p-1 bg-blue-500 text-white rounded">
        Click to Scroll
    </button>
    
    <!-- Add spacing to make scroll visible -->
    <div style="height: 150vh; background: linear-gradient(to bottom, #f0f0f0, #e0e0e0); display: flex; align-items: center; justify-content: center;">
        <p style="font-size: 2rem; color: #666;">Scroll down to see the target...</p>
    </div>
    
    <div data-ref="container" class="p-4 border" style="background: yellow; padding: 2rem; margin: 2rem 0;">
        <h2 style="font-size: 1.5rem; font-weight: bold;">Content Container (Target)</h2>
        <p>This is the element we're scrolling to!</p>
    </div>
</div>

When you use data-ref="container", Datastar creates a reference accessible via $container.

Accessing Multiple Refs

blade
<div data-signals="{}">
    <input data-ref="emailInput" type="email" />
    <input data-ref="passwordInput" type="password" />

    <button data-on:click="$emailInput.focus()">
        Focus Email
    </button>
</div>

Intersection Observer

data-on-intersect (Datastar)

The data-on-intersect attribute executes an expression when an element enters the viewport. This uses the browser's native IntersectionObserver API:

blade
<div data-signals="{loadedImage: false}">
    <div data-on-intersect="$loadedImage = true">
        <img
            data-attr:src="$loadedImage ? '/large-image.jpg' : ''"
            alt="Lazy loaded image" />
    </div>
</div>

The image URL is only set when the container enters the viewport, implementing lazy loading.

Modifiers

once

Triggers only the first time the element enters the viewport:

blade
<div data-on-intersect__once="@get('/analytics/viewed')">
    Article content
</div>

Perfect for tracking when content has been viewed.

half

Triggers when at least 50% of the element is visible:

blade
<div data-on-intersect__half="$video.play()">
    Video auto-plays when half-visible
</div>

full

Triggers only when the entire element is visible:

blade
<div data-on-intersect__full="$animationStarted = true">
    Animation triggers when fully in view
</div>

Combining Modifiers

blade
<div data-on-intersect__once__half="$hasSeenContent = true">
    Content tracked when 50% visible, only once
</div>

Interval Observer

data-on-interval (Datastar)

The data-on-interval attribute executes an expression at regular intervals:

blade
<div data-signals="{time: Date.now()}">
    <div data-on-interval="$time = Date.now()">
        Current time: <span data-text="new Date($time).toLocaleTimeString()"></span>
    </div>
</div>

By default, the expression runs every 1000ms (1 second).

Custom Interval

Specify the interval using a modifier:

blade
<!-- Every 5 seconds -->
<div data-on-interval__duration.5s="@get('/stats')">
    Auto-refreshes every 5 seconds
</div>

Leading Execution

Run immediately, then at intervals:

blade
<div data-on-interval__duration.5s.leading="@get('/notifications')">
    Runs immediately, then every 5 seconds
</div>

Common Patterns

Lazy Loading Images

blade
<div data-signals="{imageLoaded: false}">
    <div data-on-intersect__once="$imageLoaded = true">
        <img
            data-attr:src="$imageLoaded ? '/path/to/image.jpg' : '/placeholder.jpg'"
            data-class:opacity-0="!$imageLoaded"
            data-class:opacity-100 transition-opacity="$imageLoaded"
            alt="Lazy loaded" />
    </div>
</div>

Infinite Scroll Pagination

blade
<div data-signals="{page: 1, loading: false}">
    <div class="status">
        Page: <span data-text="$page"></span>
    </div>

    <div id="items-container">
        <div class="item">
            <div class="item-title">Item 1</div>
            <div class="item-description">This is the description for item 1. Scroll down to load more items!</div>
        </div>
        <div class="item">
            <div class="item-title">Item 2</div>
            <div class="item-description">This is the description for item 2. Keep scrolling!</div>
        </div>
        <div class="item">
            <div class="item-title">Item 3</div>
            <div class="item-description">This is the description for item 3.</div>
        </div>
        <div class="item">
            <div class="item-title">Item 4</div>
            <div class="item-description">This is the description for item 4.</div>
        </div>
        <div class="item">
            <div class="item-title">Item 5</div>
            <div class="item-description">This is the description for item 5.</div>
        </div>
        <div class="item">
            <div class="item-title">Item 6</div>
            <div class="item-description">This is the description for item 6.</div>
        </div>
        <div class="item">
            <div class="item-title">Item 7</div>
            <div class="item-description">This is the description for item 7.</div>
        </div>
        <div class="item">
            <div class="item-title">Item 8</div>
            <div class="item-description">This is the description for item 8.</div>
        </div>
        <div class="item">
            <div class="item-title">Item 9</div>
            <div class="item-description">This is the description for item 9.</div>
        </div>
        <div class="item">
            <div class="item-title">Item 10</div>
            <div class="item-description">This is the description for item 10.</div>
        </div>
    </div>

    <!-- Infinite scroll trigger -->
    <div 
        class="loading-trigger"
        data-on-intersect="$loading = true; $page++; console.log('Loading page ' + $page); setTimeout(() => $loading = false, 2000)">
        <div data-show="$loading" class="loading-text">
            🔄 Loading more items for page <span data-text="$page"></span>...
        </div>
        <div data-show="!$loading" style="color: #999;">
            ↓ Scroll to load more ↓
        </div>
    </div>
</div>

When the user scrolls to the bottom, the next page loads automatically.

Auto-Refresh Dashboard

blade
<div @signals(['stats' => $stats])>
    <div data-on-interval__duration.10s="@get('/dashboard/stats')">
        <h2>Live Statistics</h2>
        <div>
            <strong>Users Online:</strong>
            <span data-text="$stats.usersOnline"></span>
        </div>
        <div>
            <strong>Orders Today:</strong>
            <span data-text="$stats.ordersToday"></span>
        </div>
    </div>
</div>

The stats refresh every 10 seconds without user interaction.

Scroll Animations

blade
<div data-signals="{
    section1Visible: false,
    section2Visible: false,
}">
    <div
        data-on-intersect__once__half="$section1Visible = true"
        data-class="{
            'translate-y-10 opacity-0 text-gray-400': !$section1Visible,
            'translate-y-0 opacity-100 text-red-400': $section1Visible
        }"
        class="transition-all duration-700">
        Section 1 fades in
    </div>

    <div
        data-on-intersect__once__half="$section2Visible = true"
        data-class="{
            'translate-y-10 opacity-0 text-gray-400': !$section2Visible,
            'translate-y-0 opacity-100 text-red-400': $section2Visible
        }"
        class="transition-all duration-700">
        Section 2 fades in
    </div>
</div>

Each section animates into view as you scroll.

View Tracking

blade
<div @signals(['articleId' => $article->id])>
    <div data-on-intersect__once__duration.3s="
        @postx('/articles/' + $articleId + '/view')
    ">
        Article content...
    </div>
</div>

Tracks an article view only after it's been visible for 3 seconds.

Auto-Save Draft

blade
<div data-signals="{draft: '', lastSaved: null}">
    <textarea
        data-bind="draft"
        rows="10"></textarea>

    <div data-on-interval__duration.15s="$lastSaved = new Date().toLocaleTimeString(); @patchx('/drafts/auto-save')">
        <div data-show="$lastSaved">
            Last saved: <span data-text="$lastSaved"></span>
        </div>
    </div>
</div>

Auto-saves the draft every 15 seconds.

Focus Management

blade
<div data-signals="{step: 1}">
    <div data-show="$step === 1">
        <input data-ref="nameInput" placeholder="Name" />
        <button data-on:click="$step = 2; setTimeout(() => $emailInput.focus(), 10)">
            Next
        </button>
    </div>

    <div data-show="$step === 2">
        <input data-ref="emailInput" type="email" placeholder="Email" />
        <button data-on:click="$step = 1">Back</button>
    </div>
</div>

Moves focus to the next input when progressing through a multi-step form.

Countdown Timer

blade
<div data-signals="{timeLeft: 10}">
    <div data-on-interval__duration.1s="
        if ($timeLeft > 0) $timeLeft = $timeLeft - 1;
        else @postx('/timer-expired')
    ">
        <div data-text="$timeLeft + ' seconds remaining'"></div>
        <div data-show="$timeLeft === 0">Time's up!</div>
    </div>
</div>

Learn More