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:
<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
<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:
<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:
<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:
<div data-on-intersect__half="$video.play()">
Video auto-plays when half-visible
</div>full
Triggers only when the entire element is visible:
<div data-on-intersect__full="$animationStarted = true">
Animation triggers when fully in view
</div>Combining Modifiers
<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:
<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:
<!-- Every 5 seconds -->
<div data-on-interval__duration.5s="@get('/stats')">
Auto-refreshes every 5 seconds
</div>Leading Execution
Run immediately, then at intervals:
<div data-on-interval__duration.5s.leading="@get('/notifications')">
Runs immediately, then every 5 seconds
</div>Common Patterns
Lazy Loading Images
<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
<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
<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
<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
<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
<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
<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
<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
- Events - Handle user interactions
- Display & Binding - React to changes
- Cookbook: Infinite Scroll - Complete infinite scroll implementation
- Cookbook: Live Dashboard - Build auto-refreshing dashboards
- Datastar Documentation - Complete reference for observers

