Vue componentFree

Tool

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

Install
Full screen ↗

Code

15 files in the repo
<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>

Props

PropTypeDefaultDescription
tipString''—
contentSizeString'90%'—
backgroundLightString'rgba(238, 238, 238, 0.8)'—
backgroundDarkString'rgba(0, 0, 30, 0.8)'—
colorLightString'rgba(0, 0, 30, 0.8)'—
colorDarkString'rgb(255, 255, 255)'—
All modals & overlays →
Preview of Modal DialogFree

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.

by Vueplay
Preview of Slide-over PanelFree

Component · Modals & overlays

Slide-over Panel

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.

by Vueplay
Preview of Hero SplitFree

Component · Hero sections

Hero Split

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.

by Vueplay1 installs
Preview of Bento GridFree

Component · Feature sections

Bento Grid

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.

by Vueplay

Pro feature

Upgrade to Pro

Upgrade to unlock more of Vueplay. Cancel any time.

  • Custom domains with HTTPS
  • Private projects
  • No "Made with Vueplay" badge
  • GitHub sync

$120/year

Compare plansBilling settings