Vue Material Component Framework implementing Google's Material Design specification with comprehensive UI components, theming system, and accessibility features.
—
Vue directives for adding interactive behaviors and functionality to DOM elements without requiring component wrappers.
Directive for handling clicks outside of an element, commonly used for closing dropdowns and modals.
/**
* Detects clicks outside the element and calls handler
*/
const ClickOutside: Directive;
interface ClickOutsideBinding {
/** Handler function called when click occurs outside */
handler?: (event: Event) => void;
/** Additional options */
options?: {
/** Elements to exclude from outside detection */
exclude?: (string | Element | ComponentPublicInstance)[];
/** Include the element itself in outside detection */
closeConditional?: (event: Event) => boolean;
};
}
// Usage syntax variations
type ClickOutsideValue =
| ((event: Event) => void)
| {
handler: (event: Event) => void;
exclude?: (string | Element)[];
closeConditional?: (event: Event) => boolean;
};Usage Examples:
<template>
<!-- Basic usage -->
<div v-click-outside="closeMenu" class="menu">
<button @click="menuOpen = !menuOpen">Toggle Menu</button>
<div v-show="menuOpen" class="menu-content">
Menu items here...
</div>
</div>
<!-- With options -->
<div
v-click-outside="{
handler: closeDropdown,
exclude: ['.ignore-outside', $refs.trigger]
}"
class="dropdown"
>
<button ref="trigger" @click="dropdownOpen = true">
Open Dropdown
</button>
<div v-show="dropdownOpen" class="dropdown-content">
<button class="ignore-outside">Won't trigger close</button>
</div>
</div>
<!-- With conditional close -->
<div
v-click-outside="{
handler: closeModal,
closeConditional: (e) => !e.target.closest('.modal-persistent')
}"
class="modal"
>
<div class="modal-content">
<button class="modal-persistent">Safe button</button>
<button @click="closeModal">Close</button>
</div>
</div>
</template>
<script setup>
const menuOpen = ref(false);
const dropdownOpen = ref(false);
const closeMenu = () => {
menuOpen.value = false;
};
const closeDropdown = () => {
dropdownOpen.value = false;
};
const closeModal = () => {
console.log('Modal closed');
};
</script>Directive using Intersection Observer API to detect when elements enter or leave the viewport.
/**
* Observes element intersection with viewport or ancestor
*/
const Intersect: Directive;
interface IntersectBinding {
/** Handler function called on intersection changes */
handler?: (
entries: IntersectionObserverEntry[],
observer: IntersectionObserver,
isIntersecting: boolean
) => void;
/** Intersection observer options */
options?: IntersectionObserverInit & {
/** Only trigger once when element enters */
once?: boolean;
/** Quiet mode - don't trigger on initial observation */
quiet?: boolean;
};
}
interface IntersectionObserverInit {
/** Element to use as viewport */
root?: Element | null;
/** Margin around root */
rootMargin?: string;
/** Threshold percentages for triggering */
threshold?: number | number[];
}
// Usage syntax variations
type IntersectValue =
| ((entries: IntersectionObserverEntry[], observer: IntersectionObserver, isIntersecting: boolean) => void)
| {
handler: (entries: IntersectionObserverEntry[], observer: IntersectionObserver, isIntersecting: boolean) => void;
options?: IntersectionObserverInit & { once?: boolean; quiet?: boolean };
};Usage Examples:
<template>
<!-- Basic intersection detection -->
<div v-intersect="onIntersect" class="observe-me">
This element is being watched
</div>
<!-- Lazy loading images -->
<img
v-for="image in images"
:key="image.id"
v-intersect="{
handler: loadImage,
options: { once: true, threshold: 0.1 }
}"
:data-src="image.src"
:alt="image.alt"
class="lazy-image"
/>
<!-- Infinite scroll trigger -->
<div class="content-list">
<div v-for="item in items" :key="item.id">
{{ item.title }}
</div>
<div
v-intersect="{
handler: loadMore,
options: { rootMargin: '100px' }
}"
class="scroll-trigger"
>
Loading more...
</div>
</div>
<!-- Animation on scroll -->
<div
v-intersect="{
handler: animateElement,
options: { threshold: 0.5, once: true }
}"
class="animate-on-scroll"
:class="{ 'is-visible': elementVisible }"
>
Animate when 50% visible
</div>
</template>
<script setup>
const images = ref([]);
const items = ref([]);
const elementVisible = ref(false);
const onIntersect = (entries, observer, isIntersecting) => {
console.log('Element intersecting:', isIntersecting);
};
const loadImage = (entries, observer, isIntersecting) => {
if (isIntersecting) {
const img = entries[0].target;
img.src = img.dataset.src;
img.classList.add('loaded');
}
};
const loadMore = async (entries, observer, isIntersecting) => {
if (isIntersecting) {
const newItems = await fetchMoreItems();
items.value.push(...newItems);
}
};
const animateElement = (entries, observer, isIntersecting) => {
elementVisible.value = isIntersecting;
};
</script>
<style>
.lazy-image {
opacity: 0;
transition: opacity 0.3s;
}
.lazy-image.loaded {
opacity: 1;
}
.animate-on-scroll {
transform: translateY(50px);
opacity: 0;
transition: all 0.6s ease;
}
.animate-on-scroll.is-visible {
transform: translateY(0);
opacity: 1;
}
</style>Directive using MutationObserver to watch for DOM changes within an element.
/**
* Observes DOM mutations within element
*/
const Mutate: Directive;
interface MutateBinding {
/** Handler function called when mutations occur */
handler?: (mutations: MutationRecord[], observer: MutationObserver) => void;
/** Mutation observer configuration */
options?: MutationObserverInit;
}
interface MutationObserverInit {
/** Watch for child node changes */
childList?: boolean;
/** Watch for attribute changes */
attributes?: boolean;
/** Watch for character data changes */
characterData?: boolean;
/** Watch changes in descendant nodes */
subtree?: boolean;
/** Report previous attribute values */
attributeOldValue?: boolean;
/** Report previous character data */
characterDataOldValue?: boolean;
/** Filter specific attributes to watch */
attributeFilter?: string[];
}
// Usage syntax variations
type MutateValue =
| ((mutations: MutationRecord[], observer: MutationObserver) => void)
| {
handler: (mutations: MutationRecord[], observer: MutationObserver) => void;
options?: MutationObserverInit;
};Usage Examples:
<template>
<!-- Watch for content changes -->
<div
v-mutate="onContentChange"
class="dynamic-content"
v-html="dynamicHtml"
/>
<!-- Watch for attribute changes -->
<div
v-mutate="{
handler: onAttributeChange,
options: {
attributes: true,
attributeFilter: ['class', 'style']
}
}"
:class="dynamicClass"
:style="dynamicStyle"
>
Watch my attributes
</div>
<!-- Watch for child additions -->
<ul
v-mutate="{
handler: onListChange,
options: {
childList: true,
subtree: true
}
}"
class="dynamic-list"
>
<li v-for="item in listItems" :key="item.id">
{{ item.name }}
<button @click="removeItem(item.id)">Remove</button>
</li>
</ul>
<!-- Monitor form changes -->
<form
v-mutate="{
handler: onFormChange,
options: {
attributes: true,
childList: true,
subtree: true,
attributeFilter: ['disabled', 'required']
}
}"
>
<input v-model="formData.name" :disabled="formDisabled" />
<input v-model="formData.email" :required="emailRequired" />
</form>
</template>
<script setup>
const dynamicHtml = ref('<p>Initial content</p>');
const dynamicClass = ref('initial-class');
const listItems = ref([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
]);
const onContentChange = (mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
console.log('Content changed:', mutation.addedNodes, mutation.removedNodes);
}
});
};
const onAttributeChange = (mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'attributes') {
console.log(`Attribute ${mutation.attributeName} changed from`,
mutation.oldValue, 'to', mutation.target[mutation.attributeName]);
}
});
};
const onListChange = (mutations) => {
console.log('List structure changed:', mutations.length, 'mutations');
};
const onFormChange = (mutations) => {
console.log('Form changed:', mutations);
// Handle form validation or auto-save
};
</script>Directive using ResizeObserver to detect element size changes.
/**
* Observes element resize events
*/
const Resize: Directive;
interface ResizeBinding {
/** Handler function called when element resizes */
handler?: (entries: ResizeObserverEntry[], observer: ResizeObserver) => void;
/** Resize observer options */
options?: {
/** Debounce resize events (ms) */
debounce?: number;
/** Only trigger on width changes */
watchWidth?: boolean;
/** Only trigger on height changes */
watchHeight?: boolean;
};
}
// Usage syntax variations
type ResizeValue =
| ((entries: ResizeObserverEntry[], observer: ResizeObserver) => void)
| {
handler: (entries: ResizeObserverEntry[], observer: ResizeObserver) => void;
options?: { debounce?: number; watchWidth?: boolean; watchHeight?: boolean };
};Usage Examples:
<template>
<!-- Basic resize detection -->
<div
v-resize="onResize"
class="resizable-container"
:style="{ width: containerWidth + 'px', height: containerHeight + 'px' }"
>
<p>Size: {{ currentSize.width }} x {{ currentSize.height }}</p>
<button @click="resizeContainer">Change Size</button>
</div>
<!-- Debounced resize handling -->
<textarea
v-resize="{
handler: onTextareaResize,
options: { debounce: 250 }
}"
v-model="textContent"
class="auto-resize-textarea"
/>
<!-- Chart responsive to container -->
<div
v-resize="{
handler: updateChart,
options: { debounce: 100 }
}"
class="chart-container"
>
<canvas ref="chartCanvas" />
</div>
<!-- Responsive grid -->
<div
v-resize="{
handler: updateGridColumns,
options: { watchWidth: true, debounce: 150 }
}"
class="responsive-grid"
:style="{ gridTemplateColumns: gridColumns }"
>
<div v-for="item in gridItems" :key="item.id" class="grid-item">
{{ item.name }}
</div>
</div>
</template>
<script setup>
const containerWidth = ref(400);
const containerHeight = ref(300);
const currentSize = ref({ width: 0, height: 0 });
const textContent = ref('');
const gridColumns = ref('1fr 1fr 1fr');
const chartCanvas = ref();
const onResize = (entries) => {
const { width, height } = entries[0].contentRect;
currentSize.value = { width: Math.round(width), height: Math.round(height) };
};
const onTextareaResize = (entries) => {
const { width, height } = entries[0].contentRect;
console.log(`Textarea resized to ${width}x${height}`);
// Auto-save or adjust UI based on size
};
const updateChart = (entries) => {
const { width, height } = entries[0].contentRect;
if (chartCanvas.value) {
chartCanvas.value.width = width;
chartCanvas.value.height = height;
// Redraw chart with new dimensions
redrawChart();
}
};
const updateGridColumns = (entries) => {
const { width } = entries[0].contentRect;
if (width < 600) {
gridColumns.value = '1fr';
} else if (width < 900) {
gridColumns.value = '1fr 1fr';
} else {
gridColumns.value = '1fr 1fr 1fr';
}
};
const resizeContainer = () => {
containerWidth.value = Math.random() * 400 + 200;
containerHeight.value = Math.random() * 300 + 150;
};
</script>Directive for adding Material Design ripple effect to elements on interaction.
/**
* Adds Material Design ripple effect
*/
const Ripple: Directive;
interface RippleBinding {
/** Ripple configuration */
value?: boolean | {
/** Ripple color */
color?: string;
/** Center the ripple effect */
center?: boolean;
/** Custom CSS class */
class?: string;
};
}
// Usage syntax variations
type RippleValue =
| boolean
| {
color?: string;
center?: boolean;
class?: string;
};Usage Examples:
<template>
<!-- Basic ripple effect -->
<button v-ripple class="custom-button">
Click for ripple
</button>
<!-- Disabled ripple -->
<button v-ripple="false" class="no-ripple-button">
No ripple effect
</button>
<!-- Custom color ripple -->
<div
v-ripple="{ color: 'red' }"
class="ripple-card"
@click="handleClick"
>
Custom red ripple
</div>
<!-- Centered ripple -->
<v-icon
v-ripple="{ center: true }"
size="large"
@click="toggleFavorite"
>
{{ isFavorite ? 'mdi-heart' : 'mdi-heart-outline' }}
</v-icon>
<!-- Conditional ripple -->
<button
v-ripple="!disabled"
:disabled="disabled"
class="conditional-ripple"
>
{{ disabled ? 'Disabled' : 'Enabled' }}
</button>
<!-- Custom ripple class -->
<div
v-ripple="{ class: 'custom-ripple-class', color: 'purple' }"
class="fancy-card"
>
Custom styled ripple
</div>
</template>
<script setup>
const isFavorite = ref(false);
const disabled = ref(false);
const handleClick = () => {
console.log('Card clicked with ripple');
};
const toggleFavorite = () => {
isFavorite.value = !isFavorite.value;
};
</script>
<style>
.custom-button {
padding: 12px 24px;
background: #1976d2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.ripple-card {
padding: 20px;
background: #f5f5f5;
border-radius: 8px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.custom-ripple-class {
animation-duration: 0.8s !important;
opacity: 0.3 !important;
}
</style>Directive for handling scroll events with customizable options and performance optimizations.
/**
* Handles scroll events with options
*/
const Scroll: Directive;
interface ScrollBinding {
/** Scroll handler function */
handler?: (event: Event) => void;
/** Scroll configuration options */
options?: {
/** Throttle scroll events (ms) */
throttle?: number;
/** Target element to watch for scroll */
target?: string | Element | Window;
/** Passive event listener */
passive?: boolean;
};
}
// Usage syntax variations
type ScrollValue =
| ((event: Event) => void)
| {
handler: (event: Event) => void;
options?: {
throttle?: number;
target?: string | Element | Window;
passive?: boolean;
};
};Usage Examples:
<template>
<!-- Basic scroll handling -->
<div
v-scroll="onScroll"
class="scrollable-content"
style="height: 400px; overflow-y: auto;"
>
<div v-for="n in 100" :key="n" class="scroll-item">
Item {{ n }}
</div>
</div>
<!-- Throttled scroll -->
<div
v-scroll="{
handler: onThrottledScroll,
options: { throttle: 100 }
}"
class="performance-scroll"
style="height: 300px; overflow-y: auto;"
>
<div style="height: 2000px; background: linear-gradient(to bottom, #f0f0f0, #d0d0d0);">
Large scrollable content
</div>
</div>
<!-- Window scroll (global) -->
<div
v-scroll="{
handler: onWindowScroll,
options: {
target: 'window',
throttle: 50,
passive: true
}
}"
>
<!-- This element watches window scroll -->
<div class="scroll-indicator" :style="{ width: scrollProgress + '%' }"></div>
</div>
<!-- Scroll to top button -->
<div class="page-content" style="height: 200vh; padding: 20px;">
<div
v-scroll="{
handler: updateScrollTop,
options: { target: 'window', throttle: 100 }
}"
>
Long page content...
</div>
<v-fab
v-show="showScrollTop"
location="bottom right"
icon="mdi-chevron-up"
@click="scrollToTop"
/>
</div>
<!-- Infinite scroll implementation -->
<div
v-scroll="{
handler: checkInfiniteScroll,
options: { throttle: 200 }
}"
class="infinite-list"
style="height: 400px; overflow-y: auto;"
>
<div v-for="item in infiniteItems" :key="item.id" class="list-item">
{{ item.name }}
</div>
<div v-if="loading" class="loading">Loading more...</div>
</div>
</template>
<script setup>
const scrollProgress = ref(0);
const showScrollTop = ref(false);
const infiniteItems = ref([]);
const loading = ref(false);
const onScroll = (event) => {
const target = event.target;
console.log('Scroll position:', target.scrollTop);
};
const onThrottledScroll = (event) => {
const target = event.target;
const scrollPercent = (target.scrollTop / (target.scrollHeight - target.clientHeight)) * 100;
console.log('Scroll percent:', scrollPercent.toFixed(1));
};
const onWindowScroll = () => {
const scrolled = window.scrollY;
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
scrollProgress.value = (scrolled / maxScroll) * 100;
};
const updateScrollTop = () => {
showScrollTop.value = window.scrollY > 300;
};
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
const checkInfiniteScroll = (event) => {
const target = event.target;
const scrollBottom = target.scrollHeight - target.scrollTop - target.clientHeight;
if (scrollBottom < 100 && !loading.value) {
loadMoreItems();
}
};
const loadMoreItems = async () => {
loading.value = true;
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
const newItems = Array.from({ length: 20 }, (_, i) => ({
id: infiniteItems.value.length + i + 1,
name: `Item ${infiniteItems.value.length + i + 1}`
}));
infiniteItems.value.push(...newItems);
loading.value = false;
};
</script>
<style>
.scroll-indicator {
height: 4px;
background: linear-gradient(to right, #2196f3, #21cbf3);
transition: width 0.1s ease;
position: fixed;
top: 0;
left: 0;
z-index: 9999;
}
.scroll-item,
.list-item {
padding: 16px;
border-bottom: 1px solid #eee;
}
.loading {
padding: 20px;
text-align: center;
color: #666;
}
</style>Directive for handling touch gestures and swipe interactions on mobile devices.
/**
* Handles touch gestures and swipes
*/
const Touch: Directive;
interface TouchBinding {
/** Touch handler functions */
handlers?: {
/** Start touch handler */
start?: (wrapper: TouchWrapper) => void;
/** End touch handler */
end?: (wrapper: TouchWrapper) => void;
/** Move touch handler */
move?: (wrapper: TouchWrapper) => void;
/** Left swipe handler */
left?: (wrapper: TouchWrapper) => void;
/** Right swipe handler */
right?: (wrapper: TouchWrapper) => void;
/** Up swipe handler */
up?: (wrapper: TouchWrapper) => void;
/** Down swipe handler */
down?: (wrapper: TouchWrapper) => void;
};
/** Touch configuration options */
options?: {
/** Minimum distance for swipe */
threshold?: number;
/** Prevent default touch behavior */
passive?: boolean;
};
}
interface TouchWrapper {
/** Original touch event */
touchstartX: number;
touchstartY: number;
touchmoveX: number;
touchmoveY: number;
touchendX: number;
touchendY: number;
offsetX: number;
offsetY: number;
}
// Usage syntax variations
type TouchValue = {
[K in 'start' | 'end' | 'move' | 'left' | 'right' | 'up' | 'down']?: (wrapper: TouchWrapper) => void;
} & {
options?: {
threshold?: number;
passive?: boolean;
};
};Usage Examples:
<template>
<!-- Basic swipe detection -->
<div
v-touch="{
left: () => nextSlide(),
right: () => previousSlide()
}"
class="swipe-container"
>
<div class="slide" :style="{ transform: `translateX(-${currentSlide * 100}%)` }">
<div v-for="(slide, index) in slides" :key="index" class="slide-item">
Slide {{ index + 1 }}
</div>
</div>
</div>
<!-- Full gesture handling -->
<div
v-touch="{
start: onTouchStart,
move: onTouchMove,
end: onTouchEnd,
left: onSwipeLeft,
right: onSwipeRight,
up: onSwipeUp,
down: onSwipeDown,
options: { threshold: 50 }
}"
class="gesture-area"
:style="{
transform: `translate(${position.x}px, ${position.y}px)`,
background: touchActive ? '#e3f2fd' : '#f5f5f5'
}"
>
<p>Touch and swipe me!</p>
<p>Position: {{ position.x }}, {{ position.y }}</p>
<p>Last gesture: {{ lastGesture }}</p>
</div>
<!-- Swipeable cards -->
<div class="card-stack">
<div
v-for="(card, index) in cards"
v-show="index >= currentCardIndex"
:key="card.id"
v-touch="{
left: () => swipeCard('left', index),
right: () => swipeCard('right', index),
options: { threshold: 100 }
}"
class="swipe-card"
:style="{
zIndex: cards.length - index,
transform: `scale(${1 - (index - currentCardIndex) * 0.05})`
}"
>
<h3>{{ card.title }}</h3>
<p>{{ card.content }}</p>
</div>
</div>
<!-- Mobile drawer -->
<div
v-touch="{
right: openDrawer,
options: { threshold: 30 }
}"
class="drawer-edge"
>
<!-- Edge area for drawer gesture -->
</div>
<div
v-show="drawerOpen"
v-touch="{
left: closeDrawer,
options: { threshold: 50 }
}"
class="drawer"
:class="{ open: drawerOpen }"
>
<div class="drawer-content">
<h3>Mobile Drawer</h3>
<p>Swipe left to close</p>
</div>
</div>
</template>
<script setup>
const currentSlide = ref(0);
const slides = ref(['Slide 1', 'Slide 2', 'Slide 3']);
const position = ref({ x: 0, y: 0 });
const touchActive = ref(false);
const lastGesture = ref('none');
const currentCardIndex = ref(0);
const drawerOpen = ref(false);
const cards = ref([
{ id: 1, title: 'Card 1', content: 'Swipe left or right' },
{ id: 2, title: 'Card 2', content: 'Another card to swipe' },
{ id: 3, title: 'Card 3', content: 'Last card in stack' },
]);
const nextSlide = () => {
if (currentSlide.value < slides.value.length - 1) {
currentSlide.value++;
}
};
const previousSlide = () => {
if (currentSlide.value > 0) {
currentSlide.value--;
}
};
const onTouchStart = (wrapper) => {
touchActive.value = true;
console.log('Touch started at:', wrapper.touchstartX, wrapper.touchstartY);
};
const onTouchMove = (wrapper) => {
position.value = {
x: wrapper.touchmoveX - wrapper.touchstartX,
y: wrapper.touchmoveY - wrapper.touchstartY
};
};
const onTouchEnd = (wrapper) => {
touchActive.value = false;
// Reset position with animation
setTimeout(() => {
position.value = { x: 0, y: 0 };
}, 100);
};
const onSwipeLeft = () => {
lastGesture.value = 'swipe left';
};
const onSwipeRight = () => {
lastGesture.value = 'swipe right';
};
const onSwipeUp = () => {
lastGesture.value = 'swipe up';
};
const onSwipeDown = () => {
lastGesture.value = 'swipe down';
};
const swipeCard = (direction, index) => {
if (index === currentCardIndex.value) {
console.log(`Card swiped ${direction}`);
currentCardIndex.value++;
}
};
const openDrawer = () => {
drawerOpen.value = true;
};
const closeDrawer = () => {
drawerOpen.value = false;
};
</script>
<style>
.swipe-container {
width: 300px;
height: 200px;
overflow: hidden;
border-radius: 8px;
background: #f0f0f0;
}
.slide {
display: flex;
transition: transform 0.3s ease;
}
.slide-item {
min-width: 300px;
height: 200px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.gesture-area {
width: 300px;
height: 200px;
border-radius: 12px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: transform 0.2s ease, background 0.2s ease;
cursor: grab;
user-select: none;
}
.card-stack {
position: relative;
width: 280px;
height: 180px;
}
.swipe-card {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
padding: 20px;
transition: transform 0.2s ease;
cursor: grab;
}
.drawer-edge {
position: fixed;
left: 0;
top: 0;
width: 20px;
height: 100vh;
z-index: 1000;
}
.drawer {
position: fixed;
left: -300px;
top: 0;
width: 300px;
height: 100vh;
background: white;
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
transition: left 0.3s ease;
z-index: 999;
}
.drawer.open {
left: 0;
}
.drawer-content {
padding: 20px;
}
</style>Directive version of tooltip for simple text tooltips without component overhead.
/**
* Simple tooltip directive
*/
const Tooltip: Directive;
interface TooltipBinding {
/** Tooltip text or configuration */
value?: string | {
/** Tooltip text */
text: string;
/** Tooltip position */
location?: 'top' | 'bottom' | 'left' | 'right';
/** Show delay in ms */
delay?: number;
/** Disabled state */
disabled?: boolean;
};
}
// Usage syntax variations
type TooltipValue =
| string
| {
text: string;
location?: 'top' | 'bottom' | 'left' | 'right';
delay?: number;
disabled?: boolean;
};Usage Examples:
<template>
<!-- Simple text tooltip -->
<button v-tooltip="'This is a simple tooltip'">
Hover for tooltip
</button>
<!-- Positioned tooltip -->
<v-icon
v-tooltip="{
text: 'Save your work',
location: 'bottom'
}"
@click="saveWork"
>
mdi-content-save
</v-icon>
<!-- Tooltip with delay -->
<v-chip
v-tooltip="{
text: 'Click to remove',
location: 'top',
delay: 1000
}"
closable
@click:close="removeChip"
>
Delayed tooltip
</v-chip>
<!-- Conditional tooltip -->
<v-btn
v-tooltip="{
text: 'Please fill all required fields',
disabled: formValid
}"
:disabled="!formValid"
@click="submitForm"
>
Submit
</v-btn>
<!-- Dynamic tooltip content -->
<div
v-for="item in items"
:key="item.id"
v-tooltip="getTooltipText(item)"
class="item"
>
{{ item.name }}
</div>
</template>
<script setup>
const formValid = ref(false);
const items = ref([
{ id: 1, name: 'Item 1', description: 'First item description' },
{ id: 2, name: 'Item 2', description: 'Second item description' },
]);
const saveWork = () => {
console.log('Work saved');
};
const removeChip = () => {
console.log('Chip removed');
};
const submitForm = () => {
if (formValid.value) {
console.log('Form submitted');
}
};
const getTooltipText = (item) => {
return {
text: item.description,
location: 'right',
delay: 500
};
};
</script>
<style>
.item {
padding: 8px 16px;
margin: 4px;
background: #f5f5f5;
border-radius: 4px;
cursor: pointer;
}
.item:hover {
background: #e0e0e0;
}
</style>// Common directive binding interface
interface DirectiveBinding<T = any> {
value: T;
oldValue: T;
arg?: string;
modifiers: Record<string, boolean>;
instance: ComponentPublicInstance | null;
dir: ObjectDirective<any, T>;
}
// Event types
interface TouchWrapper {
touchstartX: number;
touchstartY: number;
touchmoveX: number;
touchmoveY: number;
touchendX: number;
touchendY: number;
offsetX: number;
offsetY: number;
}
// Observer types
interface IntersectionObserverEntry {
boundingClientRect: DOMRectReadOnly;
intersectionRatio: number;
intersectionRect: DOMRectReadOnly;
isIntersecting: boolean;
rootBounds: DOMRectReadOnly | null;
target: Element;
time: number;
}
interface ResizeObserverEntry {
borderBoxSize: ResizeObserverSize[];
contentBoxSize: ResizeObserverSize[];
contentRect: DOMRectReadOnly;
target: Element;
}
interface MutationRecord {
type: 'childList' | 'attributes' | 'characterData';
target: Node;
addedNodes: NodeList;
removedNodes: NodeList;
previousSibling: Node | null;
nextSibling: Node | null;
attributeName: string | null;
attributeNamespace: string | null;
oldValue: string | null;
}
// Location types for tooltips and positioning
type DirectiveLocation = 'top' | 'bottom' | 'left' | 'right';Install with Tessl CLI
npx tessl i tessl/npm-vuetify