
Tooltips are one of those UI elements that seem trivial to implement until you actually have to build one properly. A basic version is straightforward to knock together; however, a reusable tooltip that can be dropped onto any element, positioned on any side, and doesn’t fall apart the moment its container starts scrolling or clipping overflow is a different story entirely.
For many years, the standard approach has involved some combination of absolute positioning, JavaScript to calculate coordinates, and a fair amount of fiddling with overflow styling on parent elements. However, CSS Anchor Positioning has changed the landscape considerably, and it pairs very nicely with the Blazor component model.
In this post, I’ll walk through building a reusable Tooltip component in Blazor that uses anchor positioning to tether the tooltip content to its trigger element, and I’ll explain why this approach is much more reliable than the traditional absolute positioning technique.
The problem with absolute positioning
The traditional way of building a tooltip involves setting the tooltip content to position: absolute and positioning it relative to a position: relative container.
This works fine in isolated examples, but it starts to break down in real applications for a couple of reasons.
Firstly, an absolutely positioned element is placed relative to its nearest positioned ancestor. If that tooltip container happens to sit inside another element with overflow: hidden or overflow: auto (e.g. a card, table cell, modal, or scrollable panel), the tooltip content can be clipped or completely hidden, even though there is plenty of room for it to render.
Secondly, calculating where the tooltip should actually appear (above, below, left, or right) relative to the trigger element usually means reaching for JavaScript. You end up measuring bounding rectangles with getBoundingClientRect, doing the arithmetic yourself, and re-running it on resize or scroll. It’s all doable, but it’s extra plumbing for something that feels like it should be a purely presentational concern.
Enter CSS Anchor Positioning
CSS Anchor Positioning solves both of these problems at the CSS level, with no JavaScript required.
The idea is straightforward. You designate an element as an anchor using the anchor-name property, assigning it a custom identifier. Any other element can then be positioned relative to that anchor, using the position-anchor property along with the anchor() function. The anchoring applies regardless of where the two elements sit in the DOM tree, and critically, without being constrained by the anchor’s containing block.
Since the positioned element is tethered directly to the anchor rather than to a positioned ancestor, it isn’t subject to the same overflow clipping issues that plague the absolute positioning approach. The browser handles the relationship between the two elements natively.
What we’ll build
We’re going to build a Blazor Tooltip component that leverages CSS anchor positioning, and demonstrate how the component can be used within the standard Blazor project template, specifically on the Counter page that ships with every new Blazor app. We’ll wrap the existing ‘Click me’ button with our new Tooltip component. Hovering over the button will display a short piece of explanatory text, positioned neatly to the right of the button using CSS anchor positioning, as shown below.

This is a deliberately simple example, but it’s a good one, since it demonstrates the two things a reusable tooltip component needs to get right: wrapping an arbitrary existing element without disturbing its markup or behaviour, and positioning the tooltip content reliably regardless of where that element sits on the page.
Building the Tooltip component
With that context in place and a vision of what we’re going to build, let’s look at the implementation of the component.
The markup and code
Below is the full listing for the Tooltip.razor file.
<div class="tooltip-container @PositionClass" style="--tooltip-anchor: --tt-@_tooltipAnchorId;"> @ChildContent <div class="tooltip-content"> <span class="tooltip-label">@Label</span> </div> </div> @code { [Parameter] public required string Label { get; set; } [Parameter] public required RenderFragment ChildContent { get; set; } [Parameter] public Position Position { get; set; } = Position.Top; private readonly string _tooltipAnchorId = Guid.NewGuid().ToString("N"); private string PositionClass => Position.ToString().ToLower(); }
The component takes three parameters:
Label– the text to display inside the tooltip.ChildContent– the trigger element(s) that the tooltip wraps around, making the component usable with any markup.Position– an enum (Top,Right,Bottom,Left) that determines which side of the trigger element the tooltip should appear on, defaulting toTop.
Below is the definition of the Position enum implementation for reference.
public enum Position {     Top,     Right,     Bottom,     Left }
One detail in the component code worth calling out is the _tooltipAnchorId readonly field. Each instance of the Tooltip component generates its own GUID string, which is used to build a unique anchor name (--tt-{guid}). This matters because anchor-name values need to be unique on the page; if we were to render several tooltips and they all shared the same anchor name, the browser would not be able to tell which tooltip content belongs to which trigger. Generating the identifier per instance means the component can be dropped anywhere, any number of times, without needing to manage anchor names manually. The identifier is passed down to the CSS via a custom property, --tooltip-anchor, that is set as an inline style on the container div. This is what links the Razor component to the isolated CSS file.
The CSS
Below is the full listing for the Tooltip.razor.css file.
.tooltip-container {     display: inline-block;     anchor-name: var(--tooltip-anchor); }     .tooltip-container:hover .tooltip-content {         display: flex;     } .tooltip-content {     display: none;     position: fixed;     position-anchor: var(--tooltip-anchor);     max-width: 12.5rem;     background-color: #2d2d2d;     color: #ffffff;     font: 0.875rem sans-serif;     border: 1px solid #2d2d2d;     border-radius: 0.25rem;     padding: 0.25rem 0.5rem;     z-index: 1000; } .tooltip-label {     display: -webkit-box;     -webkit-box-orient: vertical;     -webkit-line-clamp: 10;     overflow: hidden; } .tooltip-container.top .tooltip-content {     bottom: anchor(top);     left: anchor(center);     translate: -50% -0.3125rem; } .tooltip-container.right .tooltip-content {     left: anchor(right);     top: anchor(center);     translate: 0.3125rem -50%; } .tooltip-container.bottom .tooltip-content {     top: anchor(bottom);     left: anchor(center);     translate: -50% 0.3125rem; } .tooltip-container.left .tooltip-content {     right: anchor(left);     top: anchor(center);     translate: -0.3125rem -50%; } .tooltip-content::after {     content: '';     position: absolute;     border: 0.3125rem solid transparent; } .tooltip-container.top .tooltip-content::after {     top: 100%;     left: 50%;     translate: -50%;     border-top-color: #2d2d2d; } .tooltip-container.right .tooltip-content::after {     top: 50%;     right: 100%;     translate: 0 -50%;     border-right-color: #2d2d2d; } .tooltip-container.bottom .tooltip-content::after {     bottom: 100%;     left: 50%;     translate: -50%;     border-bottom-color: #2d2d2d; } .tooltip-container.left .tooltip-content::after {     top: 50%;     left: 100%;     translate: 0 -50%;     border-left-color: #2d2d2d; }
Let’s break down what’s actually going on here, as a few pieces of the Anchor Positioning API need to work together to achieve the desired outcome.
Declaring the anchor
.tooltip-container {     display: inline-block;     anchor-name: var(--tooltip-anchor); }
The .tooltip-container element is registered as an anchor by setting anchor-name to the custom property that was passed via the component’s inline style. This is what allows any other element on the page to reference this specific container as a positioning target.
Referencing the anchor
.tooltip-content {     display: none;     position: fixed;     position-anchor: var(--tooltip-anchor);     /* Remainder of styles omitted for brevity */ }
The tooltip content itself uses position: fixed combined with position-anchor, pointing back at the same custom property. This tells the browser that this element’s position should be calculated relative to the anchor, rather than relative to a positioned ancestor, which is the crucial difference from the traditional approach. It’s also worth noting that display: none is used here rather than visibility: hidden, since an anchor-positioned element still needs to be able to establish its position correctly once it’s shown on hover, and display: none avoids the tooltip content contributing to the layout while hidden.
Positioning relative to the anchor’s edges
.tooltip-container.top .tooltip-content {     bottom: anchor(top);     left: anchor(center);     translate: -50% -0.3125rem; }
This is where the anchor() function comes in. It lets you reference specific edges (or the centre) of the anchor element directly within standard positioning properties. For the top position, bottom: anchor(top) places the bottom edge of the tooltip content at the top edge of the anchor, and left: anchor(center) horizontally centres it. The small translate nudges are there to introduce a bit of breathing room and to correct for the fact that anchor(center) aligns edges rather than centre-points, so a -50% translate is needed to centre the tooltip over the trigger properly.
The same pattern repeats for right, bottom, and left, each one referencing a different combination of anchor edges.
The arrow
.tooltip-content::after {     content: '';     position: absolute;     border: 0.3125rem solid transparent; } .tooltip-container.top .tooltip-content::after {     top: 100%;     left: 50%;     translate: -50%;     border-top-color: #2d2d2d; }
The small triangular arrow pointing back at the trigger element is handled with a standard ::after pseudo-element and a transparent border trick, positioned absolutely within the tooltip content itself. This part doesn’t need anchor positioning at all, since it’s just relative to its own parent. Still, it’s a nice example of how anchor positioning and conventional CSS positioning can happily coexist within the same component.
Using the component
With the component in place, using it is refreshingly simple. Here’s how we can use it on the Counter page from the standard Blazor sample application to add a tooltip around the ‘Click me’ button:
<Tooltip Label="Click this button to increment the counter" Position=Position.Right>     <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> </Tooltip>
Since ChildContent accepts any markup, the tooltip can wrap a button, an icon, a piece of text, or any other element, and the Position parameter controls which side of the element the tooltip appears on without any additional wiring required.
Why this beats absolute positioning
Bringing it back to the comparison I raised earlier, there are a few concrete advantages here:
- No overflow clipping. Because
position-anchorties the tooltip to the anchor element directly rather than to a positioned ancestor, the tooltip isn’t at the mercy ofoverflow: hiddenon some unrelated parent container further up the DOM tree. - No JavaScript required. There’s no
getBoundingClientRect, no resize listeners, no scroll listeners. The browser recalculates the position natively whenever it needs to. - Positioning logic lives entirely in CSS. This keeps the component’s C# code minimal; its only real job is generating a unique anchor identifier, while the actual visual behaviour stays where it belongs, in the stylesheet.
- The anchor and the positioned element don’t need to be adjacent in the DOM. It’s less obvious with this particular component, since the tooltip content happens to be a sibling of the trigger. However, you could, for example, anchor a tooltip rendered at the very end of the document body to a trigger element buried deep inside a component tree if you wanted to.
Browser support
It’s worth being upfront and highlighting that CSS Anchor Positioning is a relatively recent addition to the web standards, with 81.67% global browser support at the time of writing, according to CanIUse. Since early 2026, the latest versions of all major browsers (Chrome, Edge, Firefox, Safari) support anchor positioning. However, if you know that you need to support clients on older browsers, you should consider a fallback, or stick with the JavaScript-based approach for a while longer.
Dropping anchor
CSS Anchor Positioning offers a genuinely better way to tether elements together compared to the traditional absolute positioning approach, particularly for components like tooltips, popovers, and dropdowns that need to be positioned relative to a trigger element without being constrained by that element’s position in the DOM or the overflow settings of its ancestors.
Wrapping the tooltip implementation into a reusable Blazor component keeps things tidy: the component itself is only responsible for generating a unique anchor identifier and exposing a Position parameter, while all of the actual positioning logic is isolated within a CSS file where it belongs. This is a great example of letting the platform do the heavy lifting rather than reaching for JavaScript out of habit.


Comments