Color Tooltip
Original · freecoloured semantic tooltips
byWeb InnoventixReact + Tailwind
tipcolortooltips
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/tip-color.jsontip-color.tsx
"use client";
import { useId, useState, type ReactNode } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Tone = "info" | "success" | "warning" | "danger" | "neutral" | "accent";
type Placement = "top" | "bottom";
const cn = (...classes: Array<string | false | null | undefined>): string =>
classes.filter(Boolean).join(" ");
interface ToneCfg {
label: string;
renderIcon: (className: string) => ReactNode;
bubble: string;
arrowBg: string;
chip: string;
pill: string;
dot: string;
iconBtn: string;
}
const strokeIcon = (className: string, children: ReactNode): ReactNode => (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
{children}
</svg>
);
const TONE: Record<Tone, ToneCfg> = {
info: {
label: "Info",
renderIcon: (c) =>
strokeIcon(
c,
<>
<circle cx="12" cy="12" r="9" />
<path d="M12 16v-4" />
<path d="M12 8h.01" />
</>,
),
bubble:
"bg-sky-600 text-white ring-sky-400/40 dark:bg-sky-500 dark:text-sky-50",
arrowBg: "bg-sky-600 dark:bg-sky-500",
chip: "border-sky-200 bg-sky-50 text-sky-700 hover:bg-sky-100 focus-visible:ring-sky-500 dark:border-sky-400/25 dark:bg-sky-400/10 dark:text-sky-300 dark:hover:bg-sky-400/20",
pill: "bg-sky-50 text-sky-700 ring-sky-600/20 dark:bg-sky-400/10 dark:text-sky-300 dark:ring-sky-400/30",
dot: "bg-sky-500",
iconBtn:
"text-sky-500 hover:bg-sky-50 hover:text-sky-600 focus-visible:ring-sky-500 dark:text-sky-400 dark:hover:bg-sky-400/10",
},
success: {
label: "Success",
renderIcon: (c) =>
strokeIcon(
c,
<>
<circle cx="12" cy="12" r="9" />
<path d="M8.4 12.4l2.4 2.4 4.8-5.2" />
</>,
),
bubble:
"bg-emerald-600 text-white ring-emerald-400/40 dark:bg-emerald-500 dark:text-emerald-50",
arrowBg: "bg-emerald-600 dark:bg-emerald-500",
chip: "border-emerald-200 bg-emerald-50 text-emerald-700 hover:bg-emerald-100 focus-visible:ring-emerald-500 dark:border-emerald-400/25 dark:bg-emerald-400/10 dark:text-emerald-300 dark:hover:bg-emerald-400/20",
pill: "bg-emerald-50 text-emerald-700 ring-emerald-600/20 dark:bg-emerald-400/10 dark:text-emerald-300 dark:ring-emerald-400/30",
dot: "bg-emerald-500",
iconBtn:
"text-emerald-500 hover:bg-emerald-50 hover:text-emerald-600 focus-visible:ring-emerald-500 dark:text-emerald-400 dark:hover:bg-emerald-400/10",
},
warning: {
label: "Warning",
renderIcon: (c) =>
strokeIcon(
c,
<>
<path d="M12 4 3 19h18z" />
<path d="M12 10v4" />
<path d="M12 16.5h.01" />
</>,
),
bubble:
"bg-amber-400 text-amber-950 ring-amber-500/40 dark:bg-amber-400 dark:text-amber-950",
arrowBg: "bg-amber-400",
chip: "border-amber-200 bg-amber-50 text-amber-800 hover:bg-amber-100 focus-visible:ring-amber-500 dark:border-amber-400/25 dark:bg-amber-400/10 dark:text-amber-300 dark:hover:bg-amber-400/20",
pill: "bg-amber-50 text-amber-800 ring-amber-600/25 dark:bg-amber-400/10 dark:text-amber-300 dark:ring-amber-400/30",
dot: "bg-amber-500",
iconBtn:
"text-amber-500 hover:bg-amber-50 hover:text-amber-600 focus-visible:ring-amber-500 dark:text-amber-400 dark:hover:bg-amber-400/10",
},
danger: {
label: "Danger",
renderIcon: (c) =>
strokeIcon(
c,
<>
<path d="M8.6 2.6h6.8L21.4 8.6v6.8L15.4 21.4H8.6L2.6 15.4V8.6z" />
<path d="M12 8v4" />
<path d="M12 16h.01" />
</>,
),
bubble:
"bg-rose-600 text-white ring-rose-400/40 dark:bg-rose-500 dark:text-rose-50",
arrowBg: "bg-rose-600 dark:bg-rose-500",
chip: "border-rose-200 bg-rose-50 text-rose-700 hover:bg-rose-100 focus-visible:ring-rose-500 dark:border-rose-400/25 dark:bg-rose-400/10 dark:text-rose-300 dark:hover:bg-rose-400/20",
pill: "bg-rose-50 text-rose-700 ring-rose-600/20 dark:bg-rose-400/10 dark:text-rose-300 dark:ring-rose-400/30",
dot: "bg-rose-500",
iconBtn:
"text-rose-500 hover:bg-rose-50 hover:text-rose-600 focus-visible:ring-rose-500 dark:text-rose-400 dark:hover:bg-rose-400/10",
},
neutral: {
label: "Neutral",
renderIcon: (c) =>
strokeIcon(
c,
<>
<circle cx="12" cy="12" r="9" />
<path d="M12 7.5V12l3 1.8" />
</>,
),
bubble:
"bg-slate-800 text-slate-50 ring-white/10 dark:bg-slate-700 dark:text-slate-50",
arrowBg: "bg-slate-800 dark:bg-slate-700",
chip: "border-slate-200 bg-slate-50 text-slate-700 hover:bg-slate-100 focus-visible:ring-slate-500 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700",
pill: "bg-slate-100 text-slate-700 ring-slate-500/20 dark:bg-slate-800 dark:text-slate-300 dark:ring-slate-600/40",
dot: "bg-slate-400",
iconBtn:
"text-slate-500 hover:bg-slate-100 hover:text-slate-700 focus-visible:ring-slate-500 dark:text-slate-400 dark:hover:bg-slate-800",
},
accent: {
label: "Feature",
renderIcon: (c) => (
<svg className={c} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 3l2 6 6 2-6 2-2 6-2-6-6-2 6-2z" />
</svg>
),
bubble:
"bg-violet-600 text-white ring-violet-400/40 dark:bg-violet-500 dark:text-violet-50",
arrowBg: "bg-violet-600 dark:bg-violet-500",
chip: "border-violet-200 bg-violet-50 text-violet-700 hover:bg-violet-100 focus-visible:ring-violet-500 dark:border-violet-400/25 dark:bg-violet-400/10 dark:text-violet-300 dark:hover:bg-violet-400/20",
pill: "bg-violet-50 text-violet-700 ring-violet-600/20 dark:bg-violet-400/10 dark:text-violet-300 dark:ring-violet-400/30",
dot: "bg-violet-500",
iconBtn:
"text-violet-500 hover:bg-violet-50 hover:text-violet-600 focus-visible:ring-violet-500 dark:text-violet-400 dark:hover:bg-violet-400/10",
},
};
const helpIcon = (className: string): ReactNode =>
strokeIcon(
className,
<>
<circle cx="12" cy="12" r="9" />
<path d="M9.6 9.4a2.4 2.4 0 1 1 3.6 2.1c-.8.5-1.2 1-1.2 1.9" />
<path d="M12 17h.01" />
</>,
);
interface TipProps {
tone: Tone;
placement: Placement;
tip: ReactNode;
children: ReactNode;
triggerClassName?: string;
ariaLabel?: string;
}
function Tip({ tone, placement, tip, children, triggerClassName, ariaLabel }: TipProps) {
const [open, setOpen] = useState(false);
const reduce = useReducedMotion();
const tipId = useId();
const cfg = TONE[tone];
const bubblePos = placement === "top" ? "bottom-full mb-2.5" : "top-full mt-2.5";
const arrowPos =
placement === "top" ? "top-full -translate-y-1/2" : "bottom-full translate-y-1/2";
const offsetY = placement === "top" ? 4 : -4;
return (
<span className="relative inline-flex">
<button
type="button"
aria-label={ariaLabel}
aria-describedby={open ? tipId : undefined}
onPointerEnter={() => setOpen(true)}
onPointerLeave={() => setOpen(false)}
onFocus={() => setOpen(true)}
onBlur={() => setOpen(false)}
onKeyDown={(e) => {
if (e.key === "Escape") setOpen(false);
}}
className={triggerClassName}
>
{children}
</button>
<AnimatePresence>
{open && (
<motion.span
role="tooltip"
id={tipId}
initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.94, y: offsetY }}
animate={reduce ? { opacity: 1 } : { opacity: 1, scale: 1, y: 0 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.94, y: offsetY }}
transition={{ duration: reduce ? 0 : 0.16, ease: [0.16, 1, 0.3, 1] }}
className={cn(
"pointer-events-none absolute left-1/2 z-30 w-max max-w-[15rem] -translate-x-1/2 rounded-xl px-3 py-2 text-left text-xs font-medium leading-relaxed shadow-lg shadow-slate-900/15 ring-1 ring-inset dark:shadow-black/40",
bubblePos,
cfg.bubble,
)}
>
{tip}
<span
aria-hidden="true"
className={cn(
"absolute left-1/2 h-2.5 w-2.5 -translate-x-1/2 rotate-45 rounded-[2px]",
arrowPos,
cfg.arrowBg,
)}
/>
</motion.span>
)}
</AnimatePresence>
</span>
);
}
interface Showcase {
tone: Tone;
tip: string;
}
const SHOWCASE: Showcase[] = [
{ tone: "info", tip: "Builds run on Node 20 LTS. Pin your engines field so CI matches your local runtime." },
{ tone: "success", tip: "1,284 tests passed in 42s. Coverage held steady at 91% with no flaky retries." },
{ tone: "warning", tip: "Main chunk grew 38 KB versus main. Still under budget, but review before you release." },
{ tone: "danger", tip: "2 high-severity CVEs in transitive dependencies. This blocks the production promote." },
{ tone: "neutral", tip: "Last deployed 6 days ago by Priya from release/1.9 at 14:07 UTC." },
{ tone: "accent", tip: "New: preview environments now deploy automatically on every pull request." },
];
interface StatusRow {
name: string;
tone: Tone;
status: string;
tip: string;
}
const STATUS: StatusRow[] = [
{
name: "Build & compile",
tone: "success",
status: "Passing",
tip: "Compiled and cached in 1m 12s. Artifact signed and pushed to the registry.",
},
{
name: "Test suite",
tone: "success",
status: "Passing",
tip: "1,284 unit and integration tests green. No flaky retries in this run.",
},
{
name: "Dependency scan",
tone: "danger",
status: "Blocked",
tip: "2 high-severity CVEs found. Promote is blocked until they are patched or explicitly waived.",
},
{
name: "Bundle budget",
tone: "warning",
status: "Review",
tip: "Main chunk is 38 KB over the soft budget. It ships, but the trend is heading the wrong way.",
},
{
name: "Accessibility",
tone: "info",
status: "Notes",
tip: "3 minor colour-contrast notes on the marketing footer. Non-blocking for this release.",
},
{
name: "Preview environment",
tone: "accent",
status: "Ready",
tip: "Ephemeral preview is live at pr-482.preview.app and stays up for 72 hours.",
},
];
const PLACEMENTS: { value: Placement; label: string; chevron: ReactNode }[] = [
{
value: "top",
label: "Top",
chevron: strokeIcon("h-3.5 w-3.5", <path d="M6 15l6-6 6 6" />),
},
{
value: "bottom",
label: "Bottom",
chevron: strokeIcon("h-3.5 w-3.5", <path d="M6 9l6 6 6-6" />),
},
];
export default function TipColor() {
const [placement, setPlacement] = useState<Placement>("top");
const chipBase =
"inline-flex items-center gap-2 rounded-full border px-3.5 py-2 text-sm font-medium transition focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900";
const iconBtnBase =
"inline-grid h-7 w-7 place-items-center rounded-full transition focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900";
const pillBase =
"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-semibold ring-1 ring-inset";
return (
<section className="relative w-full bg-slate-50 px-4 py-16 sm:px-6 sm:py-20 dark:bg-slate-950">
<style>{`
@keyframes tipcolor-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.45; transform: scale(0.62); }
}
@keyframes tipcolor-ring {
0% { opacity: 0.55; transform: scale(1); }
100% { opacity: 0; transform: scale(2.4); }
}
.tipcolor-pulse-dot { animation: tipcolor-pulse 2.4s ease-in-out infinite; }
.tipcolor-ring { animation: tipcolor-ring 2.4s ease-out infinite; }
@media (prefers-reduced-motion: reduce) {
.tipcolor-pulse-dot, .tipcolor-ring { animation: none !important; }
}
`}</style>
<div className="mx-auto w-full max-w-3xl">
<div className="relative rounded-3xl border border-slate-200 bg-white p-6 shadow-xl shadow-slate-900/5 sm:p-8 dark:border-slate-800 dark:bg-slate-900 dark:shadow-black/30">
{/* Header */}
<div className="flex flex-col gap-5 sm:flex-row sm:items-start sm:justify-between">
<div>
<span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-medium text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">
<span className="relative flex h-2.5 w-2.5">
<span className="tipcolor-ring absolute inset-0 rounded-full bg-emerald-500" />
<span className="tipcolor-pulse-dot relative h-2.5 w-2.5 rounded-full bg-emerald-500" />
</span>
Live pipeline · checkout-service
</span>
<h2 className="mt-3 text-2xl font-semibold tracking-tight text-slate-900 sm:text-3xl dark:text-white">
Semantic tooltips
</h2>
<p className="mt-2 max-w-md text-sm leading-relaxed text-slate-600 dark:text-slate-400">
Colour is the fastest label. Each tooltip tints itself to the meaning it
carries — hover or focus any trigger to read the detail.
</p>
</div>
{/* Placement control */}
<fieldset className="shrink-0">
<legend className="sr-only">Tooltip placement</legend>
<div className="inline-flex rounded-full border border-slate-200 bg-slate-100 p-1 dark:border-slate-700 dark:bg-slate-800">
{PLACEMENTS.map((p) => (
<label key={p.value} className="relative cursor-pointer">
<input
type="radio"
name="tipcolor-placement"
value={p.value}
checked={placement === p.value}
onChange={() => setPlacement(p.value)}
className="peer sr-only"
/>
<span className="flex select-none items-center gap-1.5 rounded-full px-3.5 py-1.5 text-sm font-medium text-slate-600 transition peer-checked:bg-white peer-checked:text-slate-900 peer-checked:shadow-sm peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 dark:text-slate-300 dark:peer-checked:bg-slate-950 dark:peer-checked:text-white">
{p.chevron}
{p.label}
</span>
</label>
))}
</div>
</fieldset>
</div>
{/* Tone showcase */}
<div className="mt-8">
<div className="mb-3 flex items-baseline justify-between gap-3">
<h3 className="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
Tones
</h3>
<p className="text-xs text-slate-400 dark:text-slate-500">
Hover, or press Tab to focus
</p>
</div>
<div className="flex flex-wrap gap-2.5">
{SHOWCASE.map((s) => {
const cfg = TONE[s.tone];
return (
<Tip
key={s.tone}
tone={s.tone}
placement={placement}
tip={s.tip}
triggerClassName={cn(chipBase, cfg.chip)}
>
{cfg.renderIcon("h-4 w-4")}
{cfg.label}
</Tip>
);
})}
</div>
</div>
{/* Divider */}
<div className="my-8 h-px w-full bg-slate-200 dark:bg-slate-800" />
{/* Status panel */}
<div>
<div className="mb-4 flex items-baseline justify-between gap-3">
<h3 className="text-sm font-semibold text-slate-900 dark:text-white">
Release readiness
</h3>
<span className="text-xs font-medium text-slate-400 dark:text-slate-500">
Build #4821 · release/1.9
</span>
</div>
<ul className="divide-y divide-slate-100 rounded-2xl border border-slate-200 dark:divide-slate-800 dark:border-slate-800">
{STATUS.map((row) => {
const cfg = TONE[row.tone];
return (
<li
key={row.name}
className="flex items-center justify-between gap-3 px-4 py-3.5"
>
<div className="flex min-w-0 items-center gap-3">
<span className={cn("h-2 w-2 shrink-0 rounded-full", cfg.dot)} />
<span className="truncate text-sm font-medium text-slate-800 dark:text-slate-200">
{row.name}
</span>
</div>
<div className="flex shrink-0 items-center gap-2">
<span className={cn(pillBase, cfg.pill)}>
{cfg.renderIcon("h-3.5 w-3.5")}
{row.status}
</span>
<Tip
tone={row.tone}
placement={placement}
tip={row.tip}
ariaLabel={`Details about ${row.name}`}
triggerClassName={cn(iconBtnBase, cfg.iconBtn)}
>
{helpIcon("h-4 w-4")}
</Tip>
</div>
</li>
);
})}
</ul>
</div>
{/* Footer hint */}
<p className="mt-6 text-xs leading-relaxed text-slate-500 dark:text-slate-400">
Every tooltip opens on hover and on keyboard focus. Tab moves between triggers,
and{" "}
<kbd className="rounded border border-slate-300 bg-slate-100 px-1.5 py-0.5 font-sans text-[0.7rem] font-semibold text-slate-600 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-300">
Esc
</kbd>{" "}
dismisses the one in focus.
</p>
</div>
</div>
</section>
);
}Dependencies
motion
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Basic Tooltip
Originalbasic tooltips on hover

Arrow Tooltip
Originaltooltips with an arrow

Directions Tooltip
Originaltooltips in four directions

Rich Tooltip
Originalrich tooltip with title and body

Follow Cursor Tooltip
Originaltooltip that follows the cursor

Popover Tooltip
Originalclick popover with content

Popover Form Tooltip
Originalpopover containing a mini form

Hover Card Tooltip
Originalprofile hover card

Click Tooltip
Originalclick-to-reveal tooltip

Keyboard Hint Tooltip
Originalkeyboard shortcut hint tooltips

Glass Tooltip
Originalglassmorphic tooltips

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

