Cursor Color Sampler
A 28 px white ring follows the pointer with no delay; on every mousemove, elementFromPoint identifies the hovered cell and copies its inline background into --sampler-color: the central disc and the glow (20 and 40 px) take the color, the glow within 150 ms. The grid hides the native pointer (cursor: none).
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. Cursors has 26 effects, including 1 free. Explore the category →
3 usage examples



How it works
The .sampler-zone is a CSS grid of 5 columns × 3 rows (repeat(5, 1fr) / repeat(3, 1fr)), 100% × 300px, position: relative, cursor: none. Its fifteen .sampler-cell elements have neither content nor color CSS: each carries its background inline, style="background:#6366f1" (indigo, pink, amber, emerald, cyan, violet…). Measured: cells of 148.8 × 100 px in a 744 px zone. The first child, .sampler-cursor, is the ring: position: absolute, 28 px, border: 3px solid #fff, round, pointer-events: none, translate(−50%, −50%), z-index: 10.
The color travels through a CSS variable. The ring has box-shadow: 0 0 20px var(--sampler-color, rgba(255,255,255,.5)), 0 0 40px var(--sampler-color, rgba(255,255,255,.3)) with transition: box-shadow .15s, and its ::after (inset: 3px, round, opacity: .8) has background: var(--sampler-color, white). As long as the variable doesn't exist, white glow and white disc. On every mousemove, the script positions the ring with left/top (no transition on the position: immediate follow), shows it, then calls document.elementFromPoint(clientX, clientY). If the element found carries the sampler-cell class, it writes on the zone --sampler-color = target.style.background || target.style.backgroundColor — the inline value, returned by the browser as rgb(234, 179, 8) (measured on the yellow cell). The disc changes instantly, the glow within 150 ms.
Two details make this possible. The ring is pointer-events: none: without it, elementFromPoint would return the ring itself and the color would never change. And the script reads target.style, i.e. only the style attribute: a cell colored through a class or a stylesheet returns an empty string, the variable isn't updated and the ring keeps the previous color. The CSS also contains .sampler-cell { transition: filter .3s } while no rule ever changes filter — a transition with nothing to animate, already inert on the original page.
mouseenter shows the ring, mouseleave hides it (inline display: none) — the variable, for its part, keeps the last color. The shipped CSS sets display: none on .sampler-cursor: before the first hover, nothing is visible — without that initial state, the white ring and its white glow showed up in the grid's top-left corner, at −6/−6 px (measured). Since the native pointer is hidden by cursor: none, the ring is the only position cue inside the grid; it is white at 3 px, which matters for legibility on the light cells (see Accessibility).
Accessibility
- prefers-reduced-motion: the only animated motion is the glow transition (150 ms); the ring's follow is instantaneous. Under
@media (prefers-reduced-motion: reduce), set.sampler-cursor { transition: none }— and if you prefer to disable the effect, also removecursor: none, otherwise the grid has no pointer at all. cursor: noneremoves the native pointer over the whole grid: the ring replaces it, but it only exists if the script runs and is only correctly placed after the firstmousemove. Without JavaScript, the grid is a hole with no pointer (the ring, atdisplay: none, never appears). Declarecursor: noneunder a class set by the script (.js .sampler-zone).- Ring contrast: white over the fifteen cells, it measures 4.5:1 on the indigo
#6366f1but 1.9:1 on the yellow#eab308, 2.0:1 on the green#84cc16, 1.8:1 on the cyan#22d3ee— below the 3:1 threshold for a graphical indicator on seven cells out of fifteen. Switch the border to#0a0a0f(≥ 4.4:1 on all fifteen) or keep the white with a dark hairline:box-shadow: 0 0 0 1px #0a0a0f, 0 0 20px var(--sampler-color), …. - Color alone isn't enough (WCAG 1.4.1): the hovered cell is only designated by the glow's hue, and the cells are empty, nameless
divs. If the swatch board is a real picker, give each cellrole="button",tabindex="0"and anaria-label(“Indigo, #6366f1”), and display the sampled value as text (the hex code) next to the ring. On touch, nomousemovemoves the ring: its initialdisplay: nonekeeps it from sitting frozen in a corner. - Integration: the cells' backgrounds must be inline (
style="background:…"), or replace the read withgetComputedStyle(target).backgroundColor; the ring must be a child of the zone (zone.querySelector('.sampler-cursor')) and staypointer-events: none; the CSS fixes the grid at 5 × 3 — adaptgrid-template-columns/rowsto your cell count, otherwise the extras create an implicit row. An explicit height is mandatory (cells have no height of their own). The reset on leave is done (display: none).
Browser compatibility
ES5 code (var, function). It requires document.elementFromPoint, CSS custom properties (var(), setProperty) and CSS grid. Zero dependencies.
Without JavaScript, the color grid displays but the native pointer is hidden (cursor: none) and the ring, at display: none, never appears: no cursor inside the grid — reserve cursor: none for a class added by the script. Without var() (2016 browsers), the box-shadow and disc background declarations are invalid: white ring with no glow and no disc. Without CSS grid, the empty, heightless cells measure 0 px: nothing is visible.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="sampler-zone" data-color-sampler>
<div class="sampler-cursor"></div>
<div class="sampler-cell" style="background:#6366f1;"></div>
<div class="sampler-cell" style="background:#ec4899;"></div>
<!-- … 15 in total, one per colour … -->
</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 |
|---|---|---|
Grid (CSS — repeat(5, 1fr) / repeat(3, 1fr)) |
5 columns × 3 rows | Number of cells per row and per column. The CSS fixes the grid: with 18 cells, switch to repeat(6, 1fr), otherwise the last three form an implicit fourth row that squashes the height. |
Cell colors (HTML — style="background:…") |
15 vivid shades (#6366f1 … #a855f7) | The script reads the style attribute: the color must be inline. Hex, rgb() or hsl() are accepted, the browser returns rgb(). An inline gradient also works as a glow (the whole string is copied), not as a disc color. |
Ring (CSS — .sampler-cursor 28 px, 3 px #fff border) |
28 px, white, 3 px | Diameter and color of the grid's only pointer. White, it drops below 3:1 on light cells; #0a0a0f holds on all of them. The inner disc follows through inset: 3px: keep inset equal to the border width. |
Glow (CSS — 0 0 20px … , 0 0 40px …, transition: box-shadow .15s) |
20 px + 40 px, 150 ms | Two drop shadows in the sampled color. Double the radii for a “lamp” look; 0 ms makes the change as instant as the disc. The rgba(255,255,255,.5) and .3 fallbacks apply before the first sample. |
Central disc (CSS — ::after, opacity: .8, inset: 3px) |
sampled color at 80% | Solid preview of the color. opacity: 1 for the exact color; remove the pseudo-element for a hollow ring that shows the cell through. |
Color source (JS — target.style.background) |
the cell's style attribute | Replace with getComputedStyle(target).backgroundColor to sample cells colored through a class — at the cost of one style computation per move, negligible on a grid. |
cursor: none (CSS — .sampler-zone) |
native pointer hidden | Essential for the ring to replace the arrow, dangerous without JavaScript. Declare it under a class set by the script; the ring's initial state is already covered by its display: none. |
FAQ
display: none of the .sampler-cursor rule. Absolutely positioned with no left/top, the ring sits at the grid's origin, shifted by half a diameter through translate(−50%, −50%) (−6/−6 px) — that is where it showed up, with its white fallback glow, as long as that initial state didn't exist (measured). The script writes display: block inline on mouseenter and mousemove, which beats the rule, then display: none on mouseleave. Remove the CSS declaration and the ring reappears in the corner from page load — on touch too, where no mousemove will ever move it.target.style.background || target.style.backgroundColor, i.e. the cell's style attribute, nothing else. If your cells are colored through a class (.swatch-indigo { background: #6366f1 }), the read returns an empty string, setProperty writes an empty value and the ring keeps its previous color or its white fallback. Two fixes: put the color inline, or replace the read with getComputedStyle(target).backgroundColor. Also check that the element under the pointer is the cell itself: a child (text, icon) placed inside the cell makes the classList.contains('sampler-cell') test fail — use target.closest('.sampler-cell').zone.style.getPropertyValue('--sampler-color') returns rgb(234, 179, 8). Listen to click on the zone and copy that value into a field or a label; convert to hex if needed. For accessibility, each cell must become a control: role="button", tabindex="0", an aria-label with the color's name and code, and a :focus-visible style — the ring doesn't follow the keyboard. Display the current value as text next to the ring: color alone cannot be the only information.