Component · Modals & overlays
Modal Dialog
Accessible Vue 3 modal dialog with v-model:open, focus trap, Escape and backdrop close, scroll lock and footer slot. Tailwind CSS, dark mode.
A lightweight and customizable tooltip component with dynamic dark/light themes, responsive positioning (top, bottom, left, right), and smooth hover effects with adaptive colors.
v1.0.0Updated last year
<template>
<div
class="tooltip-wrapper"
ref="wrapper"
style="width:100%"
>
<button
@pointerenter="handleEnter"
@pointerleave="handleLeave"
:style="{ background, color }"
>
<div
:style="{ width: contentSize }"
style="align: 1/1"
>
<slot>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 32 32"
>
<path
fill="currentColor"
d="M17.9 3.4c-2.1 0-4.2.9-5.8 2.5h0l-.6.6-3.6 3.6c-.7.7-.9 1.6-.6 2.5h0l-1.5 1.5-1.4-.4-1.5 1.5 3.7 3.7 1.5-1.5-.4-1.4 1.5-1.5h0c.9.3 1.8 0 2.5-.6l4.2-4.2h0c-.2-2.1.5-4.2 2-5.7l.5-.5z"
/>
<path
fill="currentColor"
d="M28.2 26.8c-1 1-2.7 1-3.6-.1L12.2 13.2l2.5-2.5 13.5 12.4c1 1 1 2.7 0 3.7"
/>
</svg>
</slot>
</div>
</button>
<div
v-if="tip && showTip"
:class="['tooltip', position]"
:style="{'--tooltip-bg': background, color}"
>{{ tip }}</div>
</div>
</template>
<script>
export default {
inject: ['theme'],
props: {
tip: {
type: String,
default: ''
},
contentSize: {
type: String,
default: '90%',
unit: '%',
control: 'slider'
},
backgroundLight: {
type: String,
default: 'rgba(238, 238, 238, 0.8)',
control: 'color'
},
backgroundDark: {
type: String,
default: 'rgba(0, 0, 30, 0.8)',
control: 'color'
},
colorLight: {
type: String,
default: 'rgba(0, 0, 30, 0.8)',
control: 'color'
},
colorDark: {
type: String,
default: 'rgb(255, 255, 255)',
control: 'color'
}
},
data: () => ({
over: false,
showTip: false,
position: 'bottom'
}),
computed: {
background() {
if (!this.over) return 'transparent';
if (this.theme === 'dark') {
return !this.over ? this.backgroundDark : this.backgroundLight;
}
return this.over ? this.backgroundDark : this.backgroundLight;
},
color() {
if (this.theme === 'dark') {
return !this.over ? this.colorDark : this.colorLight;
}
return this.over ? this.colorDark : this.colorLight;
}
},
methods: {
handleEnter() {
this.over = true;
this.setTooltipPosition();
this.showTip = true;
},
handleLeave() {
this.over = false;
this.showTip = false;
},
setTooltipPosition() {
const rect = this.$refs.wrapper.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
const topSpace = rect.top;
const bottomSpace = vh - rect.bottom;
const leftSpace = rect.left;
const rightSpace = vw - rect.right;
if (bottomSpace > topSpace && bottomSpace > 40) {
this.position = 'bottom';
} else if (topSpace > 40) {
this.position = 'top';
}
if (this.position === 'top' || this.position === 'bottom') {
if (leftSpace < 60) this.position = 'right';
else if (rightSpace < 60) this.position = 'left';
}
}
}
};
</script>
<style scoped>
.tooltip-wrapper {
position: relative;
display: inline-block;
}
button {
border-radius: 6px;
width: 100%;
aspect-ratio: 1/1;
justify-content: center;
align-items: center;
display: flex;
transition: .1s linear all;
overflow: hidden;
padding: 5px;
cursor: pointer;
height: 100%;
}
.tooltip {
position: absolute;
font-size: 12px;
padding: 6px 8px;
border-radius: 4px;
white-space: nowrap;
pointer-events: none;
opacity: 0.9;
z-index: 100000;
background: var(--tooltip-bg);
}
.tooltip::after {
content: '';
position: absolute;
width: 0;
height: 0;
border-style: solid;
}
.tooltip.top::after {
bottom: -6px;
left: 50%;
transform: translateX(-50%);
border-width: 6px 6px 0 6px;
border-color: var(--tooltip-bg) transparent transparent transparent;
}
.tooltip.bottom::after {
top: -6px;
left: 50%;
transform: translateX(-50%);
border-width: 0 6px 6px 6px;
border-color: transparent transparent var(--tooltip-bg) transparent;
}
.tooltip.left::after {
top: 50%;
right: -6px;
transform: translateY(-50%);
border-width: 6px 0 6px 6px;
border-color: transparent transparent transparent var(--tooltip-bg);
}
.tooltip.right::after {
top: 50%;
left: -6px;
transform: translateY(-50%);
border-width: 6px 6px 6px 0;
border-color: transparent var(--tooltip-bg) transparent transparent;
}
.tooltip.top {
bottom: 100%;
left: 50%;
transform: translateX(-50%) translateY(-6px);
}
.tooltip.bottom {
top: 100%;
left: 50%;
transform: translateX(-50%) translateY(6px);
}
.tooltip.left {
right: 100%;
top: 50%;
transform: translateY(-50%) translateX(-6px);
}
.tooltip.right {
left: 100%;
top: 50%;
transform: translateY(-50%) translateX(6px);
}
</style>| Prop | Type | Default | Description |
|---|---|---|---|
| tip | String | '' | — |
| contentSize | String | '90%' | — |
| backgroundLight | String | 'rgba(238, 238, 238, 0.8)' | — |
| backgroundDark | String | 'rgba(0, 0, 30, 0.8)' | — |
| colorLight | String | 'rgba(0, 0, 30, 0.8)' | — |
| colorDark | String | 'rgb(255, 255, 255)' | — |
Component · Modals & overlays
Accessible Vue 3 modal dialog with v-model:open, focus trap, Escape and backdrop close, scroll lock and footer slot. Tailwind CSS, dark mode.
Component · Modals & overlays
Vue 3 slide-over drawer that opens from the left or right, with v-model:open, focus trap, Escape close, header and footer slots. Tailwind CSS, dark mode.
Component · Hero sections
Split Vue 3 hero section with copy and two CTAs on the left and a Tailwind-drawn app window mockup on the right. Dark mode, typed props, no images.
Component · Feature sections
Bento-style Vue 3 feature grid with wide and tall tiles and small CSS-drawn visuals like maps, timelines and code. Tailwind CSS, dark mode, typed tiles.