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>

Resize Observer

data-resize (Hyper)

The data-resize attribute executes an expression when an element's dimensions change. It uses the browser's native ResizeObserver API and provides width and height as expression arguments:

blade
<div data-signals="{w: 0, h: 0}">
    <div data-resize="$w = width; $h = height" class="resize-me border p-4">
        <p>Resize this container</p>
        <p>Width: <span data-text="Math.round($w)"></span>px</p>
        <p>Height: <span data-text="Math.round($h)"></span>px</p>
    </div>
</div>

When the element resizes (due to window resize, content changes, or CSS), the expression runs with the new dimensions.

Document/Viewport Resize

Use the __document modifier to observe the viewport size instead of a specific element:

blade
<div data-signals="{vw: 0, vh: 0}">
    <div data-resize__document="$vw = width; $vh = height">
        <p>Viewport: <span data-text="$vw"></span> x <span data-text="$vh"></span></p>
    </div>
</div>

Responsive Breakpoint Detection

Detect screen size breakpoints reactively:

blade
<div data-signals="{screenMode: ''}">
    <div data-resize__document="
        $screenMode = width < 640 ? 'mobile' :
                      width < 1024 ? 'tablet' : 'desktop'
    ">
        <div data-show="$screenMode === 'mobile'">
            Mobile layout
        </div>
        <div data-show="$screenMode === 'tablet'">
            Tablet layout
        </div>
        <div data-show="$screenMode === 'desktop'">
            Desktop layout
        </div>
    </div>
</div>

Timing Modifiers

Control how often the resize handler executes:

blade
<!-- Debounce resize events (performance optimization) -->
<div data-resize__debounce.200ms="$w = width; $h = height">
    Only updates 200ms after resize stops
</div>

<!-- Throttle for frequent updates -->
<div data-resize__throttle.100ms="$w = width">
    Updates at most every 100ms during resize
</div>

<!-- Delay initial execution -->
<div data-resize__delay.500ms="$initialized = true">
    Waits 500ms before first execution
</div>

Available Modifiers

ModifierDescription
__documentObserve document/viewport instead of element
__debounce.[time]Debounce resize events (e.g., __debounce.200ms)
__throttle.[time]Throttle resize events (e.g., __throttle.100ms)
__delay.[time]Delay initial execution (e.g., __delay.500ms)

Responsive Component Example

Build components that adapt to their container size:

blade
<div data-signals="{cardSize: 'small'}">
    <div class="card-container"
         data-resize="$cardSize = width < 300 ? 'small' : width < 500 ? 'medium' : 'large'">
        <div data-class="{
            'p-2 text-sm': $cardSize === 'small',
            'p-4 text-base': $cardSize === 'medium',
            'p-6 text-lg': $cardSize === 'large'
        }">
            <h3>Responsive Card</h3>
            <p>Size: <span data-text="$cardSize"></span></p>
        </div>
    </div>
</div>

TIP

For performance, use __debounce or __throttle when the resize handler triggers expensive operations like server requests or complex calculations.

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