Chart Data Labels
CSS-only bar chart: data labels appear on hover, anchored to each bar top and connected by a CSS triangle arrow. Zero JavaScript.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Navigation has 36 effects, including 4 free. Explore the category →
3 usage examples



How it works
The effect is entirely CSS — no JavaScript. Each .chart-bar-group uses flex-direction: column to stack the bar above its axis label. The .chart-data-label is position: absolute with bottom: calc(100% + 8px): it floats 8 px above the bar top regardless of bar height, and centers itself via translateX(-50%).
The label reveal relies on the descendant selector .chart-bar:hover .chart-data-label. At rest, the label is hidden (opacity: 0) and slightly shifted down (translateY(4px)). On hover, opacity moves to 1 and translateY to 0 over 200 ms — a subtle float-up motion that visually anchors the value to the bar. The bar simultaneously gets filter: brightness(1.2) and transform: scaleY(1.04) with transform-origin: center bottom, making it "swell" upward without shifting its baseline.
The label's pointer arrow is a ::after pseudo-element using the CSS border trick: zero width, border-top-color set to the tooltip background, the three other sides transparent — producing a downward-pointing triangle visually connected to the bar top. pointer-events: none on the label prevents it from capturing mouse events over the gap between bar and label, avoiding hover flicker.
The "automatic overlap avoidance" comes from each label following its own bar's height: bars of different heights mechanically produce labels at different vertical positions. There is no collision-detection algorithm — two bars of very similar height would produce adjacent labels at the same level with no correction.
Accessibility
- prefers-reduced-motion: not handled in the code —
transition: opacity .2s, transform .2sfires unconditionally. Add manually:@media (prefers-reduced-motion: reduce) { .chart-data-label { transition: none } }. - Keyboard navigation: the effect relies exclusively on
:hover. Notabindexor:focus-withinis defined on.chart-bar— keyboard users cannot reveal labels. - Text contrast:
#fffonrgba(15, 15, 30, .92)≈ 16:1 — WCAG AAA. Values pass both AA and AAA comfortably. - Screen reader legibility: values (
72%,53%…) are present in the DOM and readable by assistive technology, even though they are visually hidden at rest. - No
roleoraria-labelon the.chart-scenecontainer — addrole="img" aria-label="…"to give the chart meaning in an accessible document.
Browser compatibility
Pure CSS3: flexbox, absolute positioning, transitions, gradients, pseudo-elements — supported in all modern browsers since 2016.
Without CSS transitions, labels switch instantly from hidden to visible on hover with no animation — data remains accessible. On touch devices, a tap activates the <code>:hover</code> state and reveals the label; tapping elsewhere hides it.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="anchor-demo">
<div class="chart-scene">
<div class="chart-bar-group">
<div class="chart-bar chart-bar-1">
<span class="chart-data-label">72%</span>
</div>
<span class="chart-bar-label">Mon</span>
</div>
<div class="chart-bar-group">
<div class="chart-bar chart-bar-2">
<span class="chart-data-label">53%</span>
</div>
<span class="chart-bar-label">Tue</span>
</div>
<div class="chart-bar-group">
<div class="chart-bar chart-bar-3">
<span class="chart-data-label">91%</span>
</div>
<span class="chart-bar-label">Wed</span>
</div>
<div class="chart-bar-group">
<div class="chart-bar chart-bar-4">
<span class="chart-data-label">38%</span>
</div>
<span class="chart-bar-label">Thu</span>
</div>
<div class="chart-bar-group">
<div class="chart-bar chart-bar-5">
<span class="chart-data-label">65%</span>
</div>
<span class="chart-bar-label">Fri</span>
</div>
</div>
</div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| .chart-bar-N { height } | 110px / 80px / 140px / 60px / 100px | Each bar's height proportional to its data value. Adjust to reflect your real metrics. |
| .chart-bar-N { background } | indigo / pink / green / amber / orange gradients | Brand color per bar — replace the linear-gradient() in each .chart-bar-N with your palette. |
| bottom: calc(100% + 8px) | 8px | Gap between the label and the bar top. Increase to 12px if you have decorations above the bars. |
| transition: opacity .2s, transform .2s | 0.2s | Entry/exit animation duration. Drop to .12s for a snappier feel, raise to .35s for smoother motion. |
| translateY(4px) → translateY(0) | 4px | Float-up amplitude on hover. Use 6px for more drama, 2px for a nearly imperceptible shift. |
| background: rgba(15, 15, 30, .92) | .92 | Tooltip background — lower opacity (e.g. .75) for a semi-transparent label, or change color for a light theme. |
FAQ
:hover state and reveals its label; tapping elsewhere hides it. The behavior works but is not optimized for touch: no visual affordance suggests interactivity. For mobile-first use, consider showing labels permanently (opacity: 1) or replacing them with static annotations.prefers-reduced-motion media query. Add this rule after the effect CSS: @media (prefers-reduced-motion: reduce) { .chart-data-label { transition: none; } .chart-bar { transition: none; } }. Labels remain visible on hover (opacity: 1) — only the animation is removed.