Renderless Vue.js components that expose VueUse composable functionality through declarative template-based interfaces
—
Components for tracking window and document state, focus, visibility, and dimensions.
Tracks window dimensions with reactive updates.
/**
* Component that tracks window dimensions
* @example
* <UseWindowSize v-slot="{ width, height }">
* <div>Window size: {{ width }} × {{ height }}</div>
* </UseWindowSize>
*/
interface UseWindowSizeProps {
/** Initial width value @default Infinity */
initialWidth?: number;
/** Initial height value @default Infinity */
initialHeight?: number;
/** Listen to orientationchange event @default true */
listenOrientation?: boolean;
/** Include scrollbars in dimensions @default true */
includeScrollbar?: boolean;
/** Window object @default defaultWindow */
window?: Window;
}
/** Slot data exposed by UseWindowSize component */
interface UseWindowSizeReturn {
/** Current window width */
width: Ref<number>;
/** Current window height */
height: Ref<number>;
}Usage Examples:
<template>
<!-- Basic window size tracking -->
<UseWindowSize v-slot="{ width, height }">
<div class="window-info">
<h3>Window Information</h3>
<div class="size-display">
<div class="dimension">
<span class="label">Width:</span>
<span class="value">{{ width }}px</span>
</div>
<div class="dimension">
<span class="label">Height:</span>
<span class="value">{{ height }}px</span>
</div>
<div class="dimension">
<span class="label">Aspect Ratio:</span>
<span class="value">{{ (width / height).toFixed(2) }}</span>
</div>
</div>
</div>
</UseWindowSize>
<!-- Responsive breakpoint detection -->
<UseWindowSize v-slot="{ width, height }">
<div class="breakpoint-info">
<h3>Responsive Breakpoints</h3>
<div class="breakpoint" :class="getBreakpointClass(width)">
<span class="breakpoint-name">{{ getBreakpointName(width) }}</span>
<span class="breakpoint-size">{{ width }}px</span>
</div>
<div class="orientation">
Orientation: {{ width > height ? 'Landscape' : 'Portrait' }}
</div>
</div>
</UseWindowSize>
<!-- Window size visualization -->
<UseWindowSize v-slot="{ width, height }">
<div class="size-viz">
<h3>Size Visualization</h3>
<div
class="window-preview"
:style="{
width: Math.max(50, width / 10) + 'px',
height: Math.max(30, height / 10) + 'px'
}"
>
<span class="preview-text">{{ width }}×{{ height }}</span>
</div>
</div>
</UseWindowSize>
</template>
<script setup>
import { UseWindowSize } from '@vueuse/components';
function getBreakpointName(width) {
if (width < 640) return 'Mobile';
if (width < 768) return 'Small Tablet';
if (width < 1024) return 'Tablet';
if (width < 1280) return 'Desktop';
return 'Large Desktop';
}
function getBreakpointClass(width) {
if (width < 640) return 'mobile';
if (width < 768) return 'sm';
if (width < 1024) return 'tablet';
if (width < 1280) return 'desktop';
return 'xl';
}
</script>
<style>
.window-info, .breakpoint-info, .size-viz {
border: 2px solid #ddd;
border-radius: 8px;
padding: 20px;
margin: 15px 0;
}
.size-display {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-top: 15px;
}
.dimension {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px;
background: #f5f5f5;
border-radius: 6px;
}
.label {
font-size: 0.9em;
color: #666;
margin-bottom: 5px;
}
.value {
font-size: 1.5em;
font-weight: bold;
color: #2196f3;
}
.breakpoint {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
border-radius: 8px;
margin: 15px 0;
font-weight: bold;
}
.breakpoint.mobile { background: #ffebee; }
.breakpoint.sm { background: #e8f5e8; }
.breakpoint.tablet { background: #e3f2fd; }
.breakpoint.desktop { background: #f3e5f5; }
.breakpoint.xl { background: #fff3e0; }
.window-preview {
border: 2px solid #2196f3;
background: #e3f2fd;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
min-width: 50px;
min-height: 30px;
margin: 15px 0;
}
.preview-text {
font-size: 10px;
font-weight: bold;
color: #1976d2;
}
</style>Tracks window focus state.
/**
* Component that tracks window focus state
* @example
* <UseWindowFocus v-slot="{ focused }">
* <div>Window focused: {{ focused ? 'Yes' : 'No' }}</div>
* </UseWindowFocus>
*/
interface UseWindowFocusProps {
/** Window object @default defaultWindow */
window?: Window;
}
/** Slot data exposed by UseWindowFocus component */
interface UseWindowFocusReturn {
/** Whether window is focused */
focused: Ref<boolean>;
}Usage Examples:
<template>
<!-- Basic focus tracking -->
<UseWindowFocus v-slot="{ focused }">
<div class="focus-indicator" :class="{ focused, blurred: !focused }">
<div class="status-icon">
{{ focused ? '👁️' : '😴' }}
</div>
<div class="status-text">
Window is {{ focused ? 'focused' : 'blurred' }}
</div>
<div class="hint">
{{ focused ? 'You are viewing this tab' : 'Switch to another tab to see blur state' }}
</div>
</div>
</UseWindowFocus>
<!-- Focus-based features -->
<UseWindowFocus v-slot="{ focused }">
<div class="focus-features">
<h3>Focus-Aware Features</h3>
<div class="feature" :class="{ active: focused }">
<span class="feature-name">Auto-refresh</span>
<span class="feature-status">{{ focused ? 'Active' : 'Paused' }}</span>
</div>
<div class="feature" :class="{ active: focused }">
<span class="feature-name">Real-time updates</span>
<span class="feature-status">{{ focused ? 'Enabled' : 'Disabled' }}</span>
</div>
<div class="feature" :class="{ active: !focused }">
<span class="feature-name">Battery saver</span>
<span class="feature-status">{{ !focused ? 'Active' : 'Inactive' }}</span>
</div>
</div>
</UseWindowFocus>
<!-- Notification helper -->
<UseWindowFocus v-slot="{ focused }">
<div class="notification-helper">
<h3>Smart Notifications</h3>
<p v-if="!focused" class="notification-tip">
🔔 Perfect time to show notifications - user is not actively viewing
</p>
<p v-else class="notification-tip">
👀 User is active - consider in-app notifications instead
</p>
<button @click="simulateNotification(focused)">
Test Notification Strategy
</button>
</div>
</UseWindowFocus>
</template>
<script setup>
import { UseWindowFocus } from '@vueuse/components';
function simulateNotification(focused) {
if (focused) {
alert('In-app notification (user is active)');
} else {
console.log('Browser notification sent (user is away)');
}
}
</script>
<style>
.focus-indicator {
padding: 20px;
border-radius: 8px;
text-align: center;
transition: all 0.3s;
border: 2px solid #ddd;
}
.focus-indicator.focused {
background: #e8f5e8;
border-color: #4caf50;
}
.focus-indicator.blurred {
background: #ffebee;
border-color: #f44336;
}
.status-icon {
font-size: 2em;
margin-bottom: 10px;
}
.status-text {
font-size: 1.2em;
font-weight: bold;
margin-bottom: 5px;
}
.hint {
font-size: 0.9em;
color: #666;
font-style: italic;
}
.focus-features {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
}
.feature {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
margin: 8px 0;
border-radius: 6px;
background: #f5f5f5;
transition: background 0.3s;
}
.feature.active {
background: #e8f5e8;
}
.feature-name {
font-weight: bold;
}
.feature-status {
font-size: 0.9em;
color: #666;
}
.notification-helper {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
}
.notification-tip {
padding: 10px;
border-radius: 6px;
background: #f0f8ff;
border-left: 4px solid #2196f3;
margin: 15px 0;
}
</style>Tracks document visibility state using the Page Visibility API.
/**
* Component that tracks document visibility state
* @example
* <UseDocumentVisibility v-slot="{ visibility }">
* <div>Page visibility: {{ visibility }}</div>
* </UseDocumentVisibility>
*/
interface UseDocumentVisibilityProps {
/** Document object @default defaultDocument */
document?: Document;
}
/** Slot data exposed by UseDocumentVisibility component */
interface UseDocumentVisibilityReturn {
/** Current document visibility state */
visibility: Ref<DocumentVisibilityState>;
}
type DocumentVisibilityState = 'visible' | 'hidden' | 'prerender';Usage Examples:
<template>
<!-- Basic visibility tracking -->
<UseDocumentVisibility v-slot="{ visibility }">
<div class="visibility-tracker" :class="visibility">
<div class="visibility-indicator">
<span class="status-dot"></span>
<span class="status-text">{{ getVisibilityText(visibility) }}</span>
</div>
<div class="visibility-description">
{{ getVisibilityDescription(visibility) }}
</div>
</div>
</UseDocumentVisibility>
<!-- Performance optimization guide -->
<UseDocumentVisibility v-slot="{ visibility }">
<div class="performance-guide">
<h3>Performance Optimization</h3>
<div class="optimization-item" :class="{ active: visibility === 'visible' }">
<span class="opt-name">Animations</span>
<span class="opt-status">{{ visibility === 'visible' ? 'Running' : 'Paused' }}</span>
</div>
<div class="optimization-item" :class="{ active: visibility === 'visible' }">
<span class="opt-name">API Polling</span>
<span class="opt-status">{{ visibility === 'visible' ? 'Active' : 'Reduced' }}</span>
</div>
<div class="optimization-item" :class="{ active: visibility === 'hidden' }">
<span class="opt-name">Background Tasks</span>
<span class="opt-status">{{ visibility === 'hidden' ? 'Optimized' : 'Normal' }}</span>
</div>
</div>
</UseDocumentVisibility>
<!-- User engagement tracking -->
<UseDocumentVisibility v-slot="{ visibility }">
<div class="engagement-tracker">
<h3>User Engagement</h3>
<div class="engagement-stats">
<div class="stat">
<span class="stat-label">Current Status:</span>
<span class="stat-value" :class="visibility">{{ visibility }}</span>
</div>
<div class="stat">
<span class="stat-label">Engagement Level:</span>
<span class="stat-value">{{ getEngagementLevel(visibility) }}</span>
</div>
</div>
<div class="engagement-actions">
<button
@click="logEngagement(visibility)"
:disabled="visibility !== 'visible'"
>
Log User Action
</button>
</div>
</div>
</UseDocumentVisibility>
</template>
<script setup>
import { UseDocumentVisibility } from '@vueuse/components';
function getVisibilityText(visibility) {
switch (visibility) {
case 'visible': return 'Page is Visible';
case 'hidden': return 'Page is Hidden';
case 'prerender': return 'Page is Pre-rendering';
default: return 'Unknown State';
}
}
function getVisibilityDescription(visibility) {
switch (visibility) {
case 'visible':
return 'The page is currently visible and active.';
case 'hidden':
return 'The page is hidden (minimized, in background tab, etc.).';
case 'prerender':
return 'The page is being pre-rendered in the background.';
default:
return 'Unable to determine page visibility state.';
}
}
function getEngagementLevel(visibility) {
switch (visibility) {
case 'visible': return 'High';
case 'hidden': return 'Low';
case 'prerender': return 'None';
default: return 'Unknown';
}
}
function logEngagement(visibility) {
console.log(`User action logged - Page visibility: ${visibility}`);
}
</script>
<style>
.visibility-tracker {
padding: 20px;
border-radius: 8px;
border: 2px solid #ddd;
transition: all 0.3s;
}
.visibility-tracker.visible {
background: #e8f5e8;
border-color: #4caf50;
}
.visibility-tracker.hidden {
background: #ffebee;
border-color: #f44336;
}
.visibility-tracker.prerender {
background: #fff3e0;
border-color: #ff9800;
}
.visibility-indicator {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.2em;
font-weight: bold;
margin-bottom: 10px;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: currentColor;
}
.visibility-description {
font-size: 0.9em;
color: #666;
}
.performance-guide, .engagement-tracker {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
}
.optimization-item, .stat {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
margin: 5px 0;
border-radius: 6px;
background: #f5f5f5;
}
.optimization-item.active {
background: #e3f2fd;
}
.engagement-stats {
margin: 15px 0;
}
.stat-value.visible {
color: #4caf50;
font-weight: bold;
}
.stat-value.hidden {
color: #f44336;
font-weight: bold;
}
.stat-value.prerender {
color: #ff9800;
font-weight: bold;
}
</style>Detects when the cursor leaves the page viewport.
/**
* Component that detects when cursor leaves page
* @example
* <UsePageLeave v-slot="{ isLeft }">
* <div>Cursor left page: {{ isLeft ? 'Yes' : 'No' }}</div>
* </UsePageLeave>
*/
interface UsePageLeaveProps {
/** Window object @default defaultWindow */
window?: Window;
}
/** Slot data exposed by UsePageLeave component */
interface UsePageLeaveReturn {
/** Whether cursor has left the page */
isLeft: Ref<boolean>;
}Usage Examples:
<template>
<!-- Basic page leave detection -->
<UsePageLeave v-slot="{ isLeft }">
<div class="page-leave-indicator" :class="{ left: isLeft, present: !isLeft }">
<div class="cursor-status">
<span class="cursor-icon">{{ isLeft ? '📤' : '📍' }}</span>
<span class="cursor-text">
Cursor {{ isLeft ? 'left the page' : 'is on the page' }}
</span>
</div>
</div>
</UsePageLeave>
<!-- Exit intent detection -->
<UsePageLeave v-slot="{ isLeft }">
<div class="exit-intent" v-if="isLeft">
<div class="exit-modal">
<h3>Wait! Don't leave yet! 👋</h3>
<p>Are you sure you want to leave? You might miss out on:</p>
<ul>
<li>✨ Special offers</li>
<li>🎯 Personalized content</li>
<li>📚 Helpful resources</li>
</ul>
<div class="exit-actions">
<button @click="stayOnPage" class="stay-btn">Stay</button>
<button @click="dismissModal" class="leave-btn">Leave</button>
</div>
</div>
<div class="exit-overlay" @click="dismissModal"></div>
</div>
</UsePageLeave>
<!-- User engagement tracking -->
<UsePageLeave v-slot="{ isLeft }">
<div class="engagement-monitor">
<h3>Engagement Monitor</h3>
<div class="engagement-indicator" :class="{ engaged: !isLeft, disengaged: isLeft }">
<span class="status">{{ isLeft ? '⚠️ Potential Exit' : '✅ User Engaged' }}</span>
</div>
<div class="engagement-tips" v-if="isLeft">
<p>💡 Consider:</p>
<ul>
<li>Showing exit-intent popup</li>
<li>Saving user progress</li>
<li>Triggering retention campaigns</li>
</ul>
</div>
</div>
</UsePageLeave>
</template>
<script setup>
import { ref } from 'vue';
import { UsePageLeave } from '@vueuse/components';
const showExitModal = ref(true);
function stayOnPage() {
showExitModal.value = false;
// Additional logic to engage user
}
function dismissModal() {
showExitModal.value = false;
}
</script>
<style>
.page-leave-indicator {
padding: 20px;
border-radius: 8px;
border: 2px solid #ddd;
text-align: center;
transition: all 0.3s;
}
.page-leave-indicator.present {
background: #e8f5e8;
border-color: #4caf50;
}
.page-leave-indicator.left {
background: #fff3cd;
border-color: #ffc107;
}
.cursor-status {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-size: 1.2em;
}
.cursor-icon {
font-size: 1.5em;
}
.exit-intent {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.exit-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
}
.exit-modal {
position: relative;
background: white;
border-radius: 12px;
padding: 30px;
max-width: 400px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
text-align: center;
}
.exit-modal h3 {
color: #333;
margin-bottom: 15px;
}
.exit-modal ul {
text-align: left;
margin: 15px 0;
}
.exit-actions {
display: flex;
gap: 15px;
margin-top: 20px;
}
.stay-btn, .leave-btn {
flex: 1;
padding: 12px 20px;
border: none;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
}
.stay-btn {
background: #4caf50;
color: white;
}
.leave-btn {
background: #f5f5f5;
color: #666;
}
.engagement-monitor {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
}
.engagement-indicator {
padding: 15px;
border-radius: 8px;
text-align: center;
font-weight: bold;
margin: 15px 0;
}
.engagement-indicator.engaged {
background: #e8f5e8;
color: #2e7d32;
}
.engagement-indicator.disengaged {
background: #fff3cd;
color: #f57c00;
}
.engagement-tips {
background: #f0f8ff;
border-left: 4px solid #2196f3;
padding: 15px;
margin-top: 15px;
}
.engagement-tips ul {
margin: 10px 0;
padding-left: 20px;
}
</style>/** Common types used across window and document components */
type MaybeRefOrGetter<T> = T | Ref<T> | (() => T);
interface RenderableComponent {
/** The element that the component should be rendered as @default 'div' */
as?: object | string;
}
/** Document visibility states */
type DocumentVisibilityState = 'visible' | 'hidden' | 'prerender';
/** Window size information */
interface WindowSize {
width: number;
height: number;
}
/** Configuration interfaces */
interface ConfigurableWindow {
/** Window object @default defaultWindow */
window?: Window;
}
interface ConfigurableDocument {
/** Document object @default defaultDocument */
document?: Document;
}Install with Tessl CLI
npx tessl i tessl/npm-vueuse--components