what we're building
by default, when you add a title="..." attribute to an html element, the browser shows its own plain tooltip: small grey text, no styling, no control over font or position. we're going to replace that with a <div> that follows your cursor and looks however we want, using the same title attribute you're already used to.
we'll use a small jquery plugin called style-my-tooltips to do the actual "follow the mouse and swap the title text into a div" logic, and then style that div ourselves with css. the plugin does the boring javascript part; the css is where all the personality happens.
live example: hover over the links below. this is the exact tooltip css from the code snippet at the top of this tutorial, running for real on this page.
try hovering this link, or this one, or even a longer one that will wrap.
step 1 — load the plugin
add jquery and the style-my-tooltips plugin to your <head>, after jquery:
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://static.tumblr.com/lspzyz3/xloqk6cgp/jquery.style-my-tooltips.js"></script>
this plugin watches every element with a title attribute. the moment you hover one, it hides the browser's native tooltip, builds a <div id="s-m-t-tooltip"> containing that title text, and moves it around to follow your mouse. that div is the only thing we need to style, and we never touch the html of the links themselves.
step 2 — call it on your links
somewhere after the plugin loads (usually right before </body>), tell it which elements should get the custom tooltip:
<script>
$(function() {
$("a[title], .tooltip[title]").style_my_tooltips({
tip_follows_cursor: true,
tip_delay_time: 30,
tip_fade_speed: 300
});
});
</script>
$("a[title], .tooltip[title]"): the selector for which elements get a tooltip. by default this grabs every link that has atitleattribute, plus anything with classtooltip, which is handy for spans or icons that aren't linkstip_follows_cursor: true: the tooltip tracks your mouse as it moves instead of staying pinned to one spottip_delay_time: 30: milliseconds to wait after hovering before the tooltip appears, in case you're just passing over ittip_fade_speed: 300: how long the fade-in/fade-out animation takes, in milliseconds
class="tooltip" and a title attribute: <span class="tooltip" title="hey!">hover me</span>.step 3 — style the tooltip box
once the plugin is running, hovering anything with a title will inject #s-m-t-tooltip into the page. that id is what we target with css, nothing fancier than styling any other div:
#s-m-t-tooltip {
max-width: 150px;
background-color: white;
border: 7px solid;
border-image: url(/media/borders/pink.png) 5 fill round;
border-image-outset: 2px;
outline: 1px dashed #e1b3c7;
outline-offset: -6px;
border-radius: 4px;
font-family: tahoma;
font-size: 7px;
letter-spacing: 2px;
color: #848484;
position: fixed;
z-index: 999999999 !important;
pointer-events: none;
padding: 4px;
text-align: center;
margin: 0;
}
let's go through it line by line:
max-width: 150px: keeps the tooltip from stretching all the way across the screen if the title text is long. anything longer just wraps onto a new linebackground-color: white: the plugin doesn't add any background of its own, so without this the text would just float over whatever's behind itborder: 7px solid+border-image: this is the fun part: instead of a flat colored border, we're stretching a small png (a strip of lace, in my case) around the edge.7px solidsets how thick the border area is, andborder-imageis what actually fills that area with the image instead of a solid colorborder-image-outset: 2px: lets the border-image poke slightly outside the box's normal edge, so it doesn't feel cramped against the cornersoutline: 1px dashed #e1b3c7+outline-offset: -6px: a second, thinner dashed line sitting just inside the border-image, pulled inward by-6px. outlines don't affect layout the way borders do, so this is a cheap way to add a second decorative line without needing extra wrapper divsfont-family: tahoma+font-size: 7px+letter-spacing: 2px: tiny, spaced-out text is a big part of what makes it feel like this specific aesthetic. try swapping in your own site's font hereposition: fixed: this is required, not optional. the plugin calculates cursor coordinates relative to the viewport, so the tooltip has to be positioned relative to the screen, not the page, or it'll drift out of place as soon as you scrollz-index: 999999999 !important: makes sure the tooltip renders above literally everything else on the page, even things with their own high z-index. the!importantis there because some themes set inline z-index styles the plugin can't otherwise beatpointer-events: none: stops the tooltip div itself from intercepting mouse events. without this, the tooltip can flicker or get "stuck" as your cursor technically hovers the tooltip instead of the link underneath it
about that border-image: the live example above is using this exact lace png (blog.png) as its border. let's break down the shorthand:
border-image: url(/media/blog.png) 5 fill round;
url(/media/blog.png): the image to use. it's a single small square png with the lace pattern along its edges5: the "slice" value, in pixels from each edge of the source image. css cuts the png into a 3x3 grid using this number, then uses the 4 corner pieces as-is and stretches/repeats the 4 edge pieces around your border, leaving the center piece unused since we want to see through tobackground-color: whiteunderneathfill: tells the browser to also render that unused center piece, so the middle of the source image gets used as the tooltip's background instead of the cssbackground-color. leave this keyword out if your source image doesn't have a background you want to reuseround: how the edge pieces repeat to fill the length of each side.roundsquishes or stretches whole copies of the pattern to fit evenly, with no cut-off tiles at the corners, which keeps a repeating lace pattern looking clean. the alternative,stretch, just stretches one copy the whole way, which suits borders that aren't a repeating pattern
if you swap in your own image, the slice number should roughly match how many pixels of pattern sit along the edge of your png. too small and you'll barely see any pattern, too large and it'll look squished.
step 4 — tighten the text and move it off the cursor
two small tweaks make a big difference in how polished the tooltip feels. neither is in the plugin, they're both plain css additions to the same #s-m-t-tooltip rule:
#s-m-t-tooltip {
...
line-height: 1.3;
margin: 14px 0 0 16px;
}
line-height: 1.3: at a tinyfont-sizelike 7-9px, the browser's default line-height leaves noticeably tall gaps between wrapped lines, it starts looking more like a spaced-out list than one line of text. a tighter line-height (somewhere around 1.2-1.4) keeps wrapped lines close together, which reads much better at small sizesmargin: 14px 0 0 16px: this is what pushes the tooltip out from under your cursor. the plugin positions the tooltip by setting inlinetop/leftvalues that track the mouse, and for aposition: fixedelement, those values place the margin box, not the visible border box. so adding margin on top and on the left doesn't move where the plugin thinks the tooltip is, it just pushes the visible box away from that point, down and to the right of your cursor instead of directly underneath it
step 5 (bonus) — matching right-click menu
the same aesthetic pairs nicely with a custom right-click context menu, so here's how to build one that shares the tooltip's color palette. start with the html: a hidden <ul> sitting anywhere on the page:
<ul id="context_menu">
<li onclick="document.execCommand('copy')">copy</li>
<li onclick="location.reload()">refresh</li>
</ul>
then the css:
#context_menu {
background: #FEF8FC;
border: 1px dotted #AAAAAA;
letter-spacing: 1px;
width: 70px;
font-family: Verdana;
font-size: 8px;
position: absolute;
display: none;
z-index: 999999999;
}
#context_menu ul li {
padding: 4px;
z-index: 100000;
}
#context_menu ul li:hover {
background: #FEF8FC;
color: #848484;
z-index: 100000;
}
display: none: the menu is hidden by default. a tiny script listening forcontextmenuevents is what flips it todisplay: blockand moves it to the cursor's positionposition: absolute: positioned relative to the page rather than the viewport, since (unlike the tooltip) it only needs to appear once, right where you clicked, not track the mouse continuously- note the selectors say
#context_menu ul li: that only works if your<li>s are nested one level deeper, inside a<ul>that's inside#context_menu. since the markup above just uses#context_menudirectly on the<ul>, you'd simplify those selectors to#context_menu li
the script to actually show/hide it on right-click:
<script>
const menu = document.getElementById("context_menu");
document.addEventListener("contextmenu", (e) => {
e.preventDefault();
menu.style.display = "block";
menu.style.left = e.pageX + "px";
menu.style.top = e.pageY + "px";
});
document.addEventListener("click", () => {
menu.style.display = "none";
});
</script>
live example: right-click inside this box.
result
with just those two css blocks, you've replaced two of the browser's plainest built-in ui elements, the title tooltip and the right-click menu, with tiny styled pieces that actually match your site. once you understand that both are really "one div, positioned with js, styled with css," you can apply the exact same pattern to build custom cursors, custom scrollbars, or custom drag ghosts too.