Wikipedia Previews Minimal Vanilla JS
The HTML...
<span data-wikip="JavaScript">JavaScript</span>The Code >>
<style>
[data-wikip] {
cursor: help;
text-decoration: underline dotted;
}
.wikip-tooltip {
position: fixed;
left: 0;
top: 0;
z-index: 10000;
max-width: min(320px, calc(100vw - 16px));
min-width: 180px;
padding: 10px 12px;
border: 1px solid #d7dbe0;
border-radius: 8px;
background: #ffffff;
color: #1f2328;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.16);
font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
pointer-events: none;
opacity: 0;
visibility: hidden;
transform: translateY(4px);
transition: opacity 0.14s ease, transform 0.14s ease, visibility 0s linear 0.14s;
}
.wikip-tooltip.is-visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
pointer-events: auto;
transition: opacity 0.14s ease, transform 0.14s ease, visibility 0s linear 0s;
}
.wikip-tooltip__arrow {
position: absolute;
width: 10px;
height: 10px;
background: #ffffff;
border-left: 1px solid #d7dbe0;
border-top: 1px solid #d7dbe0;
transform: rotate(45deg);
}
.wikip-tooltip[data-placement="top"] .wikip-tooltip__arrow {
bottom: -6px;
border-left: 0;
border-top: 0;
border-right: 1px solid #d7dbe0;
border-bottom: 1px solid #d7dbe0;
}
.wikip-tooltip[data-placement="bottom"] .wikip-tooltip__arrow {
top: -6px;
}
.wikip-tooltip__content p {
margin: 0;
}
.wikip-tooltip__content a {
color: #0969da;
text-decoration: underline;
}
.wikip-tooltip__content a:hover,
.wikip-tooltip__content a:focus {
color: #0550ae;
}
[data-wikip]:focus {
outline: 2px solid #0969da;
outline-offset: 2px;
border-radius: 4px;
}
@media (prefers-reduced-motion: reduce) {
.wikip-tooltip {
transition: none;
}
}
</style>
<script>
(() => {
const GAP = 10;
const EDGE = 8;
let activeWikipTrigger = null;
let wikipTooltip = null;
let wikipTooltipContent = null;
let wikipTooltipArrow = null;
let hideTimer = null;
function ensureWikipTooltip() {
if (wikipTooltip) return;
wikipTooltip = document.createElement("div");
wikipTooltip.className = "wikip-tooltip";
wikipTooltip.setAttribute("role", "tooltip");
wikipTooltip.hidden = true;
wikipTooltipArrow = document.createElement("div");
wikipTooltipArrow.className = "wikip-tooltip__arrow";
wikipTooltipArrow.setAttribute("aria-hidden", "true");
wikipTooltipContent = document.createElement("div");
wikipTooltipContent.className = "wikip-tooltip__content";
wikipTooltip.append(wikipTooltipArrow, wikipTooltipContent);
document.body.appendChild(wikipTooltip);
wikipTooltip.addEventListener("mouseenter", () => {
clearTimeout(hideTimer);
});
wikipTooltip.addEventListener("mouseleave", (event) => {
if (activeWikipTrigger && movedBetween(activeWikipTrigger, wikipTooltip, event)) return;
hideWikipTooltip();
});
}
function movedBetween(fromElement, toElement, event) {
const nextTarget = event.relatedTarget;
return nextTarget instanceof Node &&
(fromElement.contains(nextTarget) || toElement.contains(nextTarget));
}
async function fetchWikipExtract(pageTitle) {
const url =
"https://en.wikipedia.org/w/api.php?" +
new URLSearchParams({
origin: "*",
action: "parse",
page: pageTitle,
prop: "text",
format: "json",
section: "0"
});
const response = await fetch(url);
if (!response.ok) throw new Error(response.status);
const data = await response.json();
const html = data?.parse?.text?.["*"];
if (!html) throw new Error("No content");
const parsedDocument = new DOMParser().parseFromString(html, "text/html");
parsedDocument.querySelectorAll("table, style, script").forEach((element) => element.remove());
for (const paragraph of parsedDocument.querySelectorAll("p")) {
if (!paragraph.textContent.trim()) continue;
const contentNode = paragraph.cloneNode(true);
contentNode.querySelectorAll("sup.reference").forEach((element) => element.remove());
contentNode.querySelectorAll("a").forEach((link) => {
const href = link.getAttribute("href");
link.target = "_blank";
link.rel = "noopener noreferrer";
if (href && href.startsWith("/")) {
link.href = "https://en.wikipedia.org" + href;
}
});
if (contentNode.textContent.trim()) return contentNode;
}
throw new Error("No paragraph");
}
function positionWikipTooltip(triggerElement) {
const triggerRect = triggerElement.getBoundingClientRect();
const tooltipRect = wikipTooltip.getBoundingClientRect();
let placement = "top";
let top = triggerRect.top - tooltipRect.height - GAP;
if (top < EDGE) {
placement = "bottom";
top = triggerRect.bottom + GAP;
}
let left = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
left = Math.max(EDGE, Math.min(left, window.innerWidth - tooltipRect.width - EDGE));
if (placement === "bottom" && top + tooltipRect.height > window.innerHeight - EDGE) {
placement = "top";
top = Math.max(EDGE, triggerRect.top - tooltipRect.height - GAP);
}
wikipTooltip.dataset.placement = placement;
wikipTooltip.style.left = left + "px";
wikipTooltip.style.top = top + "px";
const arrowLeft = triggerRect.left + triggerRect.width / 2 - left - 5;
wikipTooltipArrow.style.left =
Math.max(12, Math.min(arrowLeft, tooltipRect.width - 22)) + "px";
}
async function showWikipTooltip(triggerElement) {
clearTimeout(hideTimer);
ensureWikipTooltip();
activeWikipTrigger = triggerElement;
if (
!triggerElement.hasAttribute("tabindex") &&
!/^(A|BUTTON|INPUT|SELECT|TEXTAREA)$/.test(triggerElement.tagName)
) {
triggerElement.tabIndex = 0;
}
const tooltipId =
triggerElement.dataset.wikipTooltipId ||
`wikip-tooltip-${Math.random().toString(36).slice(2)}`;
triggerElement.dataset.wikipTooltipId = tooltipId;
wikipTooltip.id = tooltipId;
triggerElement.setAttribute("aria-describedby", tooltipId);
wikipTooltipContent.textContent = "Loading…";
wikipTooltip.hidden = false;
wikipTooltip.classList.add("is-visible");
positionWikipTooltip(triggerElement);
try {
const pageTitle = triggerElement.dataset.wikip;
if (
triggerElement._wikipCacheKey !== pageTitle ||
!triggerElement._wikipCacheNode
) {
triggerElement._wikipCacheNode = await fetchWikipExtract(pageTitle);
triggerElement._wikipCacheKey = pageTitle;
}
wikipTooltipContent.replaceChildren(
triggerElement._wikipCacheNode.cloneNode(true)
);
} catch {
wikipTooltipContent.textContent = "Error. Try again.";
}
positionWikipTooltip(triggerElement);
}
function hideWikipTooltip() {
clearTimeout(hideTimer);
hideTimer = setTimeout(() => {
if (!wikipTooltip) return;
wikipTooltip.classList.remove("is-visible");
wikipTooltip.hidden = true;
if (activeWikipTrigger) {
activeWikipTrigger.removeAttribute("aria-describedby");
}
activeWikipTrigger = null;
}, 60);
}
function bindWikipTrigger(triggerElement) {
if (triggerElement.dataset.wikipBound) return;
triggerElement.dataset.wikipBound = "true";
triggerElement.addEventListener("mouseenter", () => showWikipTooltip(triggerElement));
triggerElement.addEventListener("focus", () => showWikipTooltip(triggerElement));
triggerElement.addEventListener("mouseleave", (event) => {
if (wikipTooltip && movedBetween(triggerElement, wikipTooltip, event)) return;
hideWikipTooltip();
});
triggerElement.addEventListener("blur", (event) => {
if (wikipTooltip && movedBetween(triggerElement, wikipTooltip, event)) return;
hideWikipTooltip();
});
triggerElement.addEventListener("keydown", (event) => {
if (event.key === "Escape") hideWikipTooltip();
});
}
function initWikipTooltips() {
document.querySelectorAll("[data-wikip]").forEach(bindWikipTrigger);
}
window.addEventListener(
"scroll",
() => {
if (wikipTooltip && activeWikipTrigger && !wikipTooltip.hidden) {
positionWikipTooltip(activeWikipTrigger);
}
},
true
);
window.addEventListener("resize", () => {
if (wikipTooltip && activeWikipTrigger && !wikipTooltip.hidden) {
positionWikipTooltip(activeWikipTrigger);
}
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") hideWikipTooltip();
});
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initWikipTooltips);
} else {
initWikipTooltips();
}
})();</script>