dist/plugins/web-utilities-vueuse/skills/web-utilities-vueuse/SKILL.md
VueUse composable utility collection for Vue 3 - browser, sensor, network, state, animation, and component utilities
npx skillsauth add agents-inc/skills web-utilities-vueuseInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Quick Guide: VueUse provides 250+ composable functions for Vue 3 that wrap browser APIs, sensors, network, state, and component logic into reactive composables. Import individual functions from
@vueuse/corefor tree-shaking. All composables return reactive refs and follow Vue Composition API conventions. UseuseLocalStoragefor persistent state,useFetchfor reactive HTTP,useIntersectionObserverfor visibility detection,createGlobalStatefor shared state, anduseVModelfor two-way binding helpers. Always call composables in<script setup>orsetup()synchronously. Clean up is automatic via Vue's lifecycle.
<critical_requirements>
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST call VueUse composables synchronously inside <script setup> or setup() -- NEVER inside async callbacks, setTimeout, or conditional blocks)
(You MUST use shallowRef instead of ref for large objects/arrays in VueUse composables to avoid deep reactivity overhead)
(You MUST handle SSR by checking typeof window !== "undefined" or using VueUse's built-in SSR-safe composables -- browser APIs crash during server rendering)
(You MUST import individual composables from @vueuse/core -- NEVER use import * as vueuse which defeats tree-shaking)
</critical_requirements>
Auto-detection: VueUse, vueuse, @vueuse/core, @vueuse/integrations, useLocalStorage, useSessionStorage, useClipboard, useFetch, useMouse, useScroll, useIntersectionObserver, useResizeObserver, useElementVisibility, useMediaQuery, usePreferredDark, useDark, useToggle, useCounter, useCycleList, useWebSocket, useEventSource, useEventListener, useTransition, useRafFn, createGlobalState, useRefHistory, syncRef, useVModel, useVirtualList, onClickOutside, onKeyStroke, createFetch
When to use:
When NOT to use:
Key patterns covered:
useToggle, useCounter, useCycleList)useLocalStorage, useClipboard, useMediaQuery, useEventListener)useMouse, useScroll, useIntersectionObserver, useResizeObserver)useFetch, useWebSocket, useEventSource)createGlobalState, useRefHistory, syncRef)useVModel, useVirtualList, onClickOutside, onKeyStroke)useTransition, useRafFn)Detailed Resources:
VueUse follows the Composition API composable pattern: encapsulate reactive logic in reusable functions that return refs and methods. Each composable wraps a single concern (a browser API, sensor, or utility) and handles setup/teardown automatically via Vue's lifecycle.
Key principle: VueUse composables are designed for tree-shaking. Import only what you use -- each function is independently importable with minimal overhead.
import { useLocalStorage, useDark, useMediaQuery } from "@vueuse/core";
// Reactive localStorage -- syncs across tabs
const userName = useLocalStorage("user-name", "Guest");
// Dark mode with auto-detection
const isDark = useDark();
// Responsive breakpoint
const MOBILE_BREAKPOINT = "(max-width: 768px)";
const isMobile = useMediaQuery(MOBILE_BREAKPOINT);
VueUse v14.x is the current stable release, supporting Vue 3.5+. All composables are written in TypeScript with full type inference.
</philosophy>Persistent reactive state that syncs with browser storage and across tabs.
<script setup lang="ts">
import { useLocalStorage, useSessionStorage } from "@vueuse/core";
interface UserPreferences {
theme: "light" | "dark";
fontSize: number;
sidebarCollapsed: boolean;
}
const DEFAULT_PREFERENCES: UserPreferences = {
theme: "light",
fontSize: 14,
sidebarCollapsed: false,
};
// ✅ Good Example - Typed reactive localStorage
const preferences = useLocalStorage<UserPreferences>(
"user-prefs",
DEFAULT_PREFERENCES,
);
// Read and write like a normal ref
preferences.value.theme = "dark"; // auto-persists to localStorage
// Session-only storage (cleared on tab close)
const sessionToken = useSessionStorage("session-token", "");
// Remove from storage
preferences.value = null; // removes the key from localStorage
</script>
Why good: automatic serialization/deserialization, cross-tab sync via storage events, typed with generics, cleanup is automatic
// ❌ Bad Example - Manual localStorage
const prefs = ref(JSON.parse(localStorage.getItem("user-prefs") ?? "null"));
watch(
prefs,
(val) => {
localStorage.setItem("user-prefs", JSON.stringify(val)); // manual sync
},
{ deep: true },
);
// No cross-tab sync, no automatic cleanup, error-prone serialization
Why bad: manual serialization is error-prone, no cross-tab sync, no SSR safety, deep watcher on every change is expensive
Auto-cleaned event listeners with type safety.
<script setup lang="ts">
import { useEventListener } from "@vueuse/core";
import { useTemplateRef } from "vue";
const buttonRef = useTemplateRef("button");
// ✅ Good Example - Auto-cleanup event listeners
// Automatically removed when component unmounts
useEventListener(window, "resize", () => {
console.log("Window resized:", window.innerWidth);
});
// Works with template refs -- waits for mount
useEventListener(buttonRef, "click", (event) => {
console.log("Button clicked:", event);
});
// Multiple events on same target
useEventListener(document, "visibilitychange", () => {
console.log("Visibility:", document.visibilityState);
});
</script>
Why good: automatic cleanup on unmount prevents memory leaks, handles ref lifecycle (waits for mount), type-safe event names
// ❌ Bad Example - Manual event listener
onMounted(() => {
window.addEventListener("resize", handleResize);
});
onUnmounted(() => {
window.removeEventListener("resize", handleResize); // easy to forget!
});
Why bad: manual add/remove is error-prone -- missing onUnmounted cleanup causes memory leaks
Reactive responsive design and theme management.
<script setup lang="ts">
import {
useMediaQuery,
usePreferredDark,
useDark,
useToggle,
} from "@vueuse/core";
// ✅ Good Example - Reactive breakpoints
const MOBILE_BREAKPOINT = "(max-width: 768px)";
const TABLET_BREAKPOINT = "(min-width: 769px) and (max-width: 1024px)";
const isMobile = useMediaQuery(MOBILE_BREAKPOINT);
const isTablet = useMediaQuery(TABLET_BREAKPOINT);
// System preference detection
const prefersDark = usePreferredDark();
// Full dark mode with toggle (adds class to html element)
const isDark = useDark();
const toggleDark = useToggle(isDark);
</script>
<template>
<nav :class="{ 'nav--compact': isMobile }">
<button @click="toggleDark()">
{{ isDark ? "Light Mode" : "Dark Mode" }}
</button>
</nav>
</template>
Why good: reactive media queries update the ref automatically on resize, useDark handles class toggling and persistence, useToggle provides a clean toggle function
Reactive clipboard API with permission handling.
<script setup lang="ts">
import { useClipboard } from "@vueuse/core";
// ✅ Good Example - Clipboard with feedback
const source = ref("Text to copy");
const { text, copy, copied, isSupported } = useClipboard({ source });
async function handleCopy(): Promise<void> {
await copy(); // copies source.value to clipboard
// `copied` ref is true for 1.5s (default) after copy
}
</script>
<template>
<div v-if="isSupported">
<button @click="handleCopy">
{{ copied ? "Copied!" : "Copy" }}
</button>
</div>
<div v-else>Clipboard API not supported</div>
</template>
Why good: isSupported check for graceful degradation, copied provides automatic feedback timing, source reactive binding
Simple reactive utilities for common state patterns.
<script setup lang="ts">
import { useToggle, useCounter, useCycleList } from "@vueuse/core";
// ✅ Good Example - Toggle state
const [isOpen, toggleOpen] = useToggle(false);
// Counter with bounds
const MIN_COUNT = 0;
const MAX_COUNT = 100;
const { count, inc, dec, reset } = useCounter(0, {
min: MIN_COUNT,
max: MAX_COUNT,
});
// Cycle through options
const themes = ["light", "dark", "auto"] as const;
const {
state: currentTheme,
next: nextTheme,
prev: prevTheme,
} = useCycleList([...themes]);
</script>
<template>
<div>
<button @click="toggleOpen()">{{ isOpen ? "Close" : "Open" }}</button>
<div>
<button @click="dec()">-</button>
<span>{{ count }}</span>
<button @click="inc()">+</button>
</div>
<button @click="nextTheme()">Theme: {{ currentTheme }}</button>
</div>
</template>
Why good: eliminates boilerplate for common patterns, useCounter enforces min/max bounds, useCycleList handles wrapping automatically
Reactive data fetching with abort, refetch, and interceptors.
<script setup lang="ts">
import { useFetch } from "@vueuse/core";
import { computed } from "vue";
const userId = ref(1);
const apiUrl = computed(() => `/api/users/${userId.value}`);
// ✅ Good Example - Reactive fetch with auto-refetch
const { data, isFetching, error, abort } = useFetch(apiUrl, {
refetch: true, // re-fetches when apiUrl changes
}).json<User>();
// Manual fetch with immediate: false
const { data: submitResult, execute } = useFetch("/api/submit", {
immediate: false,
})
.post(formData)
.json();
async function handleSubmit(): Promise<void> {
await execute(); // manually trigger the fetch
}
</script>
Why good: URL reactivity triggers automatic refetch, .json<T>() provides type-safe parsing, abort for cancellation, immediate: false for manual control
See examples/network.md for createFetch, interceptors, and advanced patterns.
Lightweight global state without a state management library.
// stores/counter.ts
import { createGlobalState } from "@vueuse/core";
import { computed, shallowRef } from "vue";
// ✅ Good Example - Global state with computed
export const useGlobalCounter = createGlobalState(() => {
const count = shallowRef(0);
const doubleCount = computed(() => count.value * 2);
const isPositive = computed(() => count.value > 0);
function increment(): void {
count.value++;
}
function decrement(): void {
count.value--;
}
function reset(): void {
count.value = 0;
}
return { count, doubleCount, isPositive, increment, decrement, reset };
});
<!-- Any component can use the shared state -->
<script setup lang="ts">
import { useGlobalCounter } from "@/stores/counter";
const { count, doubleCount, increment, decrement } = useGlobalCounter();
</script>
Why good: shared state across components, no provider/inject boilerplate, composable pattern with computed derivations, shallowRef for performance
<red_flags>
High Priority:
async functions, setTimeout, or event handlers -- composables must be called synchronously in setup() to bind to the component lifecycleref instead of shallowRef for large objects in storage composables -- causes unnecessary deep reactivity tracking on every nested propertyuseLocalStorage, useClipboard, useMouse crash during server-side renderingMedium Priority:
import * from "@vueuse/core" -- defeats tree-shaking, imports the entire 250+ function libraryisSupported check from composables that wrap browser APIs -- causes runtime errors in unsupported environmentswatch + ref to track DOM state that VueUse composables handle reactively (useMouse, useScroll, etc.)Gotchas & Edge Cases:
useLocalStorage returns a RemovableRef -- setting .value = null removes the key from storage entirely, not storing nulluseFetch with refetch: true fires immediately AND on URL change -- use immediate: false if you only want manual controlcreateGlobalState creates a singleton per import -- state persists until page reload, even after all components unmountuseIntersectionObserver callback fires immediately with current state on creation -- don't assume first callback means element just became visibleuseDark modifies the html element's class -- ensure your CSS expects the class name it adds (default: dark)useWebSocket auto-reconnects by default -- pass autoReconnect: false if you want manual controluseEventListener with a template ref waits for onMounted internally -- the listener is NOT active during setup()useRefHistory tracks every change by default -- use { deep: false } for shallow tracking or useManualRefHistory for explicit snapshots</red_flags>
<critical_reminders>
All code must follow project conventions in CLAUDE.md
(You MUST call VueUse composables synchronously inside <script setup> or setup() -- NEVER inside async callbacks, setTimeout, or conditional blocks)
(You MUST use shallowRef instead of ref for large objects/arrays in VueUse composables to avoid deep reactivity overhead)
(You MUST handle SSR by checking typeof window !== "undefined" or using VueUse's built-in SSR-safe composables -- browser APIs crash during server rendering)
(You MUST import individual composables from @vueuse/core -- NEVER use import * as vueuse which defeats tree-shaking)
Failure to follow these rules will cause SSR crashes, performance degradation, and lifecycle errors.
</critical_reminders>
development
Material Design component library for Vue 3
development
VitePress 1.x — Vue-powered static site generator for documentation sites, built on Vite
tools
Docusaurus 3.x documentation framework — site configuration, docs/blog plugins, sidebars, versioning, MDX, swizzling, and deployment
development
TanStack Form patterns - useForm, form.Field, validators, arrays, linked fields, createFormHook, type safety