Crosshair Cursor Effect
A custom crosshair cursor with smooth tracking, live X and Y coordinates, interactive hover targeting and responsive behavior.
Crosshair Cursor Effect
Move your cursor around the preview area and hover over the interactive targets.
<div class="cr-crosshair" id="crCrosshair">
<div class="cr-crosshair__panel">
<div class="cr-crosshair__content">
<span class="cr-crosshair__eyebrow">
INTERACTIVE CURSOR
</span>
<h2>Crosshair Cursor Effect</h2>
<p>
Move your cursor around the preview area and hover over
the interactive targets.
</p>
<div class="cr-crosshair__targets">
<button
class="cr-crosshair__target"
type="button">
<span class="cr-crosshair__target-icon">
01
</span>
<span class="cr-crosshair__target-text">
<strong>Explore</strong>
<small>Hover target</small>
</span>
</button>
<button
class="cr-crosshair__target"
type="button">
<span class="cr-crosshair__target-icon">
02
</span>
<span class="cr-crosshair__target-text">
<strong>Discover</strong>
<small>Hover target</small>
</span>
</button>
<button
class="cr-crosshair__target"
type="button">
<span class="cr-crosshair__target-icon">
03
</span>
<span class="cr-crosshair__target-text">
<strong>Launch</strong>
<small>Hover target</small>
</span>
</button>
</div>
</div>
<div class="cr-crosshair__hint">
<span class="cr-crosshair__hint-dot"></span>
Move cursor inside this area
</div>
<div
class="cr-crosshair__cursor"
aria-hidden="true">
<span
class="cr-crosshair__line cr-crosshair__line--top">
</span>
<span
class="cr-crosshair__line cr-crosshair__line--right">
</span>
<span
class="cr-crosshair__line cr-crosshair__line--bottom">
</span>
<span
class="cr-crosshair__line cr-crosshair__line--left">
</span>
<span class="cr-crosshair__ring">
<span class="cr-crosshair__dot"></span>
</span>
<span
class="cr-crosshair__coordinate cr-crosshair__coordinate--x">
X 000
</span>
<span
class="cr-crosshair__coordinate cr-crosshair__coordinate--y">
Y 000
</span>
</div>
</div>
</div>
.cr-crosshair {
--cr-bg: #050816;
--cr-panel: #090d1f;
--cr-border: rgba(148, 163, 184, 0.18);
--cr-text: #f8fafc;
--cr-muted: #94a3b8;
--cr-cyan: #22d3ee;
--cr-purple: #a855f7;
width: 100%;
min-height: 620px;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
box-sizing: border-box;
background:
radial-gradient(
circle at 20% 20%,
rgba(34, 211, 238, 0.08),
transparent 30%
),
radial-gradient(
circle at 80% 80%,
rgba(168, 85, 247, 0.08),
transparent 32%
),
var(--cr-bg);
font-family:
Inter,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
}
.cr-crosshair *,
.cr-crosshair *::before,
.cr-crosshair *::after {
box-sizing: border-box;
}
/* =========================================
PREVIEW PANEL
========================================= */
.cr-crosshair__panel {
position: relative;
width: min(100%, 920px);
min-height: 540px;
overflow: hidden;
border: 1px solid var(--cr-border);
border-radius: 26px;
background:
linear-gradient(
rgba(255, 255, 255, 0.018) 1px,
transparent 1px
),
linear-gradient(
90deg,
rgba(255, 255, 255, 0.018) 1px,
transparent 1px
),
linear-gradient(
145deg,
rgba(13, 18, 40, 0.96),
rgba(5, 8, 22, 0.98)
);
background-size:
42px 42px,
42px 42px,
auto;
box-shadow:
0 30px 80px rgba(0, 0, 0, 0.38),
inset 0 1px 0 rgba(255, 255, 255, 0.04);
isolation: isolate;
cursor: none;
}
.cr-crosshair__panel::before,
.cr-crosshair__panel::after {
content: "";
position: absolute;
width: 300px;
height: 300px;
border-radius: 50%;
filter: blur(90px);
opacity: 0.14;
pointer-events: none;
z-index: -1;
}
.cr-crosshair__panel::before {
top: -120px;
left: -100px;
background: var(--cr-cyan);
}
.cr-crosshair__panel::after {
right: -120px;
bottom: -130px;
background: var(--cr-purple);
}
/* =========================================
MAIN CONTENT
========================================= */
.cr-crosshair__content {
position: relative;
z-index: 2;
width: min(100%, 680px);
margin: 0 auto;
padding: 78px 36px 110px;
text-align: center;
pointer-events: none;
}
.cr-crosshair__eyebrow {
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 18px;
padding: 7px 12px;
border: 1px solid rgba(34, 211, 238, 0.2);
border-radius: 999px;
background: rgba(34, 211, 238, 0.06);
color: var(--cr-cyan);
font-size: 11px;
font-weight: 700;
line-height: 1;
letter-spacing: 0.16em;
}
.cr-crosshair__content h2 {
margin: 0;
color: var(--cr-text);
font-size: clamp(30px, 5vw, 52px);
line-height: 1.08;
letter-spacing: -0.04em;
}
.cr-crosshair__content > p {
max-width: 560px;
margin: 20px auto 0;
color: var(--cr-muted);
font-size: 15px;
line-height: 1.75;
}
/* =========================================
INTERACTIVE TARGETS
========================================= */
.cr-crosshair__targets {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
margin-top: 44px;
pointer-events: auto;
}
.cr-crosshair__target {
position: relative;
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
padding: 15px;
border: 1px solid rgba(148, 163, 184, 0.15);
border-radius: 15px;
outline: none;
background: rgba(255, 255, 255, 0.025);
color: var(--cr-text);
text-align: left;
font: inherit;
cursor: none;
overflow: hidden;
transition:
transform 180ms ease,
border-color 180ms ease,
background 180ms ease,
box-shadow 180ms ease;
}
.cr-crosshair__target::before {
content: "";
position: absolute;
inset: 0;
background:
linear-gradient(
120deg,
rgba(34, 211, 238, 0.09),
transparent 45%,
rgba(168, 85, 247, 0.09)
);
opacity: 0;
pointer-events: none;
transition: opacity 180ms ease;
}
.cr-crosshair__target:hover,
.cr-crosshair__target:focus-visible {
transform: translateY(-3px);
border-color: rgba(34, 211, 238, 0.5);
background: rgba(34, 211, 238, 0.045);
box-shadow:
0 12px 30px rgba(0, 0, 0, 0.25),
0 0 24px rgba(34, 211, 238, 0.08);
}
.cr-crosshair__target:hover::before,
.cr-crosshair__target:focus-visible::before {
opacity: 1;
}
.cr-crosshair__target-icon {
position: relative;
z-index: 1;
flex: 0 0 38px;
width: 38px;
height: 38px;
display: grid;
place-items: center;
border: 1px solid rgba(34, 211, 238, 0.22);
border-radius: 11px;
background: rgba(34, 211, 238, 0.06);
color: var(--cr-cyan);
font-size: 11px;
font-weight: 800;
letter-spacing: 0.06em;
}
.cr-crosshair__target-text {
position: relative;
z-index: 1;
display: flex;
min-width: 0;
flex-direction: column;
gap: 3px;
}
.cr-crosshair__target-text strong {
color: var(--cr-text);
font-size: 13px;
font-weight: 700;
}
.cr-crosshair__target-text small {
color: var(--cr-muted);
font-size: 10px;
}
/* =========================================
BOTTOM HINT
========================================= */
.cr-crosshair__hint {
position: absolute;
left: 50%;
bottom: 24px;
z-index: 2;
display: flex;
align-items: center;
gap: 8px;
transform: translateX(-50%);
color: rgba(148, 163, 184, 0.72);
font-size: 11px;
letter-spacing: 0.04em;
white-space: nowrap;
pointer-events: none;
}
.cr-crosshair__hint-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--cr-cyan);
box-shadow:
0 0 10px var(--cr-cyan);
animation:
crCrosshairPulse 1.8s ease-in-out infinite;
}
@keyframes crCrosshairPulse {
0%,
100% {
opacity: 0.35;
transform: scale(0.8);
}
50% {
opacity: 1;
transform: scale(1.15);
}
}
/* =========================================
CUSTOM CROSSHAIR CURSOR
========================================= */
.cr-crosshair__cursor {
position: absolute;
top: 0;
left: 0;
z-index: 20;
width: 0;
height: 0;
opacity: 0;
pointer-events: none;
will-change: transform;
transition: opacity 160ms ease;
}
.cr-crosshair__panel:hover
.cr-crosshair__cursor {
opacity: 1;
}
/* CENTER RING */
.cr-crosshair__ring {
position: absolute;
top: 0;
left: 0;
width: 32px;
height: 32px;
border: 1px solid rgba(34, 211, 238, 0.9);
border-radius: 50%;
transform: translate(-50%, -50%);
box-shadow:
0 0 12px rgba(34, 211, 238, 0.28),
inset 0 0 12px rgba(34, 211, 238, 0.06);
transition:
width 180ms ease,
height 180ms ease,
border-color 180ms ease,
box-shadow 180ms ease,
background 180ms ease;
}
.cr-crosshair__dot {
position: absolute;
top: 50%;
left: 50%;
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--cr-cyan);
transform: translate(-50%, -50%);
box-shadow:
0 0 7px var(--cr-cyan),
0 0 15px rgba(34, 211, 238, 0.65);
transition:
transform 180ms ease,
background 180ms ease,
box-shadow 180ms ease;
}
/* =========================================
CROSSHAIR LINES
========================================= */
.cr-crosshair__line {
position: absolute;
display: block;
background:
linear-gradient(
to var(--cr-line-direction),
rgba(34, 211, 238, 0.65),
rgba(34, 211, 238, 0.08)
);
box-shadow:
0 0 7px rgba(34, 211, 238, 0.12);
transition:
background 180ms ease,
opacity 180ms ease;
}
.cr-crosshair__line--top {
--cr-line-direction: top;
left: -0.5px;
bottom: 22px;
width: 1px;
height: 52px;
}
.cr-crosshair__line--bottom {
--cr-line-direction: bottom;
left: -0.5px;
top: 22px;
width: 1px;
height: 52px;
}
.cr-crosshair__line--left {
--cr-line-direction: left;
top: -0.5px;
right: 22px;
width: 52px;
height: 1px;
}
.cr-crosshair__line--right {
--cr-line-direction: right;
top: -0.5px;
left: 22px;
width: 52px;
height: 1px;
}
/* =========================================
COORDINATES
========================================= */
.cr-crosshair__coordinate {
position: absolute;
min-width: 48px;
padding: 4px 7px;
border: 1px solid rgba(34, 211, 238, 0.18);
border-radius: 6px;
background: rgba(5, 8, 22, 0.82);
color: rgba(34, 211, 238, 0.9);
font-family: "Courier New", monospace;
font-size: 9px;
font-weight: 700;
line-height: 1;
letter-spacing: 0.06em;
white-space: nowrap;
backdrop-filter: blur(6px);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.18);
transition:
color 180ms ease,
border-color 180ms ease,
background 180ms ease;
}
.cr-crosshair__coordinate--x {
top: 32px;
left: 34px;
}
.cr-crosshair__coordinate--y {
top: 55px;
left: 34px;
}
/* =========================================
TARGETING STATE
========================================= */
.cr-crosshair__cursor.is-targeting
.cr-crosshair__ring {
width: 46px;
height: 46px;
border-color: var(--cr-purple);
background: rgba(168, 85, 247, 0.06);
box-shadow:
0 0 18px rgba(168, 85, 247, 0.4),
inset 0 0 16px rgba(168, 85, 247, 0.08);
}
.cr-crosshair__cursor.is-targeting
.cr-crosshair__dot {
background: var(--cr-purple);
transform:
translate(-50%, -50%)
scale(1.3);
box-shadow:
0 0 8px var(--cr-purple),
0 0 18px rgba(168, 85, 247, 0.7);
}
.cr-crosshair__cursor.is-targeting
.cr-crosshair__line {
background: rgba(168, 85, 247, 0.65);
}
.cr-crosshair__cursor.is-targeting
.cr-crosshair__coordinate {
color: #c084fc;
border-color: rgba(168, 85, 247, 0.3);
background: rgba(12, 8, 28, 0.88);
}
/* =========================================
RESPONSIVE
========================================= */
@media (max-width: 700px) {
.cr-crosshair {
min-height: 600px;
padding: 14px;
}
.cr-crosshair__panel {
min-height: 550px;
border-radius: 20px;
}
.cr-crosshair__content {
padding: 60px 20px 100px;
}
.cr-crosshair__content h2 {
font-size: clamp(28px, 9vw, 42px);
}
.cr-crosshair__targets {
grid-template-columns: 1fr;
max-width: 340px;
margin: 34px auto 0;
}
}
/* TOUCH DEVICES */
@media (hover: none), (pointer: coarse) {
.cr-crosshair__panel,
.cr-crosshair__target {
cursor: auto;
}
.cr-crosshair__cursor {
display: none;
}
}
/* REDUCED MOTION */
@media (prefers-reduced-motion: reduce) {
.cr-crosshair__target,
.cr-crosshair__ring,
.cr-crosshair__dot,
.cr-crosshair__line,
.cr-crosshair__coordinate {
transition: none;
}
.cr-crosshair__hint-dot {
animation: none;
}
}
(() => {
const component =
document.querySelector("#crCrosshair");
if (!component) return;
const panel =
component.querySelector(
".cr-crosshair__panel"
);
const cursor =
component.querySelector(
".cr-crosshair__cursor"
);
const coordX =
component.querySelector(
".cr-crosshair__coordinate--x"
);
const coordY =
component.querySelector(
".cr-crosshair__coordinate--y"
);
const targets =
component.querySelectorAll(
".cr-crosshair__target"
);
if (
!panel ||
!cursor ||
!coordX ||
!coordY
) {
return;
}
let mouseX = 0;
let mouseY = 0;
let currentX = 0;
let currentY = 0;
let isInside = false;
let animationFrame = null;
/* =========================================
FORMAT COORDINATE
========================================== */
const formatCoordinate = (value) => {
return String(
Math.max(
0,
Math.round(value)
)
).padStart(3, "0");
};
/* =========================================
UPDATE CURSOR
========================================== */
const updateCursor = () => {
if (!isInside) {
animationFrame = null;
return;
}
currentX +=
(mouseX - currentX) * 0.22;
currentY +=
(mouseY - currentY) * 0.22;
cursor.style.transform =
`translate3d(
${currentX}px,
${currentY}px,
0
)`;
animationFrame =
requestAnimationFrame(
updateCursor
);
};
/* =========================================
MOUSE ENTER
========================================== */
panel.addEventListener(
"mouseenter",
(event) => {
const rect =
panel.getBoundingClientRect();
mouseX =
event.clientX - rect.left;
mouseY =
event.clientY - rect.top;
currentX = mouseX;
currentY = mouseY;
isInside = true;
cursor.style.transform =
`translate3d(
${currentX}px,
${currentY}px,
0
)`;
cursor.style.opacity = "1";
coordX.textContent =
`X ${formatCoordinate(mouseX)}`;
coordY.textContent =
`Y ${formatCoordinate(mouseY)}`;
if (!animationFrame) {
animationFrame =
requestAnimationFrame(
updateCursor
);
}
}
);
/* =========================================
MOUSE MOVE
========================================== */
panel.addEventListener(
"mousemove",
(event) => {
const rect =
panel.getBoundingClientRect();
mouseX =
event.clientX - rect.left;
mouseY =
event.clientY - rect.top;
coordX.textContent =
`X ${formatCoordinate(mouseX)}`;
coordY.textContent =
`Y ${formatCoordinate(mouseY)}`;
}
);
/* =========================================
MOUSE LEAVE
========================================== */
panel.addEventListener(
"mouseleave",
() => {
isInside = false;
cursor.style.opacity = "0";
cursor.classList.remove(
"is-targeting"
);
if (animationFrame) {
cancelAnimationFrame(
animationFrame
);
animationFrame = null;
}
}
);
/* =========================================
TARGET HOVER
========================================== */
targets.forEach((target) => {
target.addEventListener(
"mouseenter",
() => {
cursor.classList.add(
"is-targeting"
);
}
);
target.addEventListener(
"mouseleave",
() => {
cursor.classList.remove(
"is-targeting"
);
}
);
target.addEventListener(
"focus",
() => {
cursor.classList.add(
"is-targeting"
);
}
);
target.addEventListener(
"blur",
() => {
cursor.classList.remove(
"is-targeting"
);
}
);
});
/* =========================================
PAGE VISIBILITY
========================================== */
document.addEventListener(
"visibilitychange",
() => {
if (!document.hidden) return;
isInside = false;
cursor.style.opacity = "0";
cursor.classList.remove(
"is-targeting"
);
if (animationFrame) {
cancelAnimationFrame(
animationFrame
);
animationFrame = null;
}
}
);
})();
How This Crosshair Cursor Effect Works
The Crosshair Cursor Effect replaces the normal mouse pointer inside its preview panel with a custom cursor made from HTML elements. The cursor contains four crosshair lines, a circular center ring, a glowing dot, and two small labels that display the current X and Y coordinates. The native pointer is hidden with cursor: none on the panel and interactive target buttons.
The panel uses layered CSS backgrounds to create a subtle grid over a dark gradient surface. Two blurred pseudo-elements add cyan and purple ambient glows around opposite corners. The main content remains centered inside a panel that can grow to 920px wide, while the heading uses clamp() to scale between different viewport sizes.
JavaScript handles the actual cursor tracking. When the pointer enters the panel, getBoundingClientRect() is used to calculate its position relative to the panel rather than the entire browser window. The resulting coordinates are displayed as values such as X 125 and Y 084. Values are rounded, prevented from going below zero, and padded to at least three digits.
Instead of moving the custom cursor directly to every new mouse position, the script stores target and current coordinates separately. On each animation frame, the current position moves 22% of the remaining distance toward the mouse. This produces the smooth trailing movement. The cursor itself is positioned with translate3d(), while requestAnimationFrame() keeps the updates synchronized with browser rendering. For more background on this technique, see the MDN requestAnimationFrame documentation.
The three Explore, Discover, and Launch buttons provide a second interaction state. Hovering or focusing one adds the is-targeting class to the custom cursor. CSS then enlarges the center ring from 32px to 46px and switches the ring, dot, crosshair lines, and coordinate styling from cyan to purple. The target button also moves upward by 3px and receives a brighter border and glow.
The component includes practical fallback behavior as well. On screens below 700px, the three target buttons change from three columns to one column. Devices with coarse pointers or no hover support keep the standard cursor and hide the custom crosshair completely. A prefers-reduced-motion rule removes transitions and the pulsing hint animation for users who request reduced motion.
Customizing the Crosshair Cursor Effect
Most of the visual appearance can be adjusted through the CSS variables and a few dimensions already defined in the component.
- Change
--cr-bgand--cr-panelto adjust the dark base colors, or edit--cr-cyanand--cr-purpleto create a different accent combination. - Adjust the 32px width and height of
.cr-crosshair__ringto change the normal cursor ring size. - Change the targeting ring from 46px to make the active target state larger or smaller.
- Modify the 52px crosshair line dimensions to control how far the four lines extend from the center.
- Edit the
0.22JavaScript interpolation value to change how quickly the custom cursor catches up with the real mouse position. - Adjust the 180ms transitions used by the targets and cursor elements to change how quickly their hover states respond.
- Change the 42px grid background size on the panel to make its background grid finer or wider.
- Edit the 700px media-query breakpoint if the three-column target layout should collapse at a different screen width.
Where to Use This Crosshair Cursor Effect
This component works best in interfaces where pointer movement is part of the visual experience rather than ordinary page navigation.
- Interactive portfolio sections where projects or experiments act as selectable targets.
- Game-inspired landing pages and futuristic interface concepts.
- Product showcases with a small number of prominent interactive controls.
- Creative agency websites that use custom pointer interactions.
- Technology demos where live cursor coordinates support the visual theme.
- Hero or feature sections that need a contained cursor effect without replacing the pointer across the whole website.
Because the effect is scoped to its preview panel, it can sit alongside regular page content without changing the cursor everywhere. For another cursor treatment with a different visual approach, the Cursor Spotlight Effect is a related CodeRipple component worth considering.
Frequently Asked Questions
Does the Crosshair Cursor Effect use JavaScript?
Yes. JavaScript calculates the pointer position inside the panel, updates the X and Y coordinate labels, animates the custom cursor with requestAnimationFrame(), and toggles the targeting state when interactive buttons are hovered or focused.
Why does the crosshair follow the mouse with a slight delay?
The script does not immediately assign the mouse coordinates to the displayed cursor. Instead, it moves the current position 22% closer to the latest mouse position on each animation frame. This interpolation creates the smooth trailing behavior.
What happens on phones and touch devices?
The CSS checks (hover: none) and (pointer: coarse). When either condition applies, the custom crosshair is hidden and the panel and target buttons return to the normal cursor behavior. The target layout also switches to a single column below 700px.
How does the cursor know when it is over a target?
Each .cr-crosshair__target has mouse-enter, mouse-leave, focus, and blur listeners. Entering or focusing a target adds the is-targeting class to the cursor. Leaving or blurring removes it, allowing CSS to control the purple targeting appearance.
The Crosshair Cursor Effect combines a CSS-built crosshair with JavaScript position tracking, live coordinates, smooth frame-based movement, responsive behavior, and a separate targeting state while keeping the interaction contained within one panel.
