6 CSS Custom Cursor Styles01 / 06
Custom SVG Brand Pointer with the CSS cursor url() Property and a Keyword Fallback
The lightest possible custom cursor: no script, no image file, no request. A single inline SVG data-URI replaces the default arrow through cursor: url(...) with a hotspot offset, and a native keyword (auto / pointer) always follows the comma as a guaranteed fallback. Clickable links get their own distinct pointer, so affordance stays obvious. This is the performance-friendly baseline every brand site should reach for first.
Published
The code
<section class="ccs-01">
<div class="ccs-01__stage">
<span class="ccs-01__hint" aria-hidden="true">Move your pointer across this panel</span>
<article class="ccs-01__card">
<span class="ccs-01__avatar" aria-hidden="true">CF</span>
<h2>Custom brand pointer</h2>
<p>The default arrow is replaced by an inline SVG through <code>cursor: url()</code> — no image file, no request, always with a keyword fallback.</p>
<div class="ccs-01__actions">
<a class="ccs-01__btn" href="#">Links get their own cursor</a>
<a class="ccs-01__btn ccs-01__btn--ghost" href="#">Secondary</a>
</div>
</article>
</div>
</section><section class="ccs-01">
<div class="ccs-01__stage">
<span class="ccs-01__hint" aria-hidden="true">Move your pointer across this panel</span>
<article class="ccs-01__card">
<span class="ccs-01__avatar" aria-hidden="true">CF</span>
<h2>Custom brand pointer</h2>
<p>The default arrow is replaced by an inline SVG through <code>cursor: url()</code> — no image file, no request, always with a keyword fallback.</p>
<div class="ccs-01__actions">
<a class="ccs-01__btn" href="#">Links get their own cursor</a>
<a class="ccs-01__btn ccs-01__btn--ghost" href="#">Secondary</a>
</div>
</article>
</div>
</section>.ccs-01,
.ccs-01 *,
.ccs-01 *::before,
.ccs-01 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccs-01 {
--bg: oklch(0.16 0.02 265);
--surface: oklch(0.21 0.02 265);
--text: oklch(0.95 0.005 265);
--border: oklch(0.32 0.02 265);
--accent: oklch(0.72 0.17 264);
--muted: color-mix(in oklab,var(--text) 58%,var(--bg));
--hover: color-mix(in oklab,var(--text) 8%,transparent);
color-scheme: dark;
display: grid;
place-items: center;
width: 100%;
min-height: 100vh;
min-height: 100dvh;
padding: 28px;
font-family: system-ui,'Segoe UI',sans-serif;
background: var(--bg);
color: var(--text);
}
.ccs-01__stage {
position: relative;
width: min(460px,100%);
display: grid;
place-items: center;
padding: 40px 24px;
border-radius: 22px;
background: radial-gradient(120% 120% at 50% 0%,var(--surface),var(--bg));
border: 1px solid var(--border);
}
.ccs-01__hint {
position: absolute;
top: 14px;
left: 16px;
font: 600 11px/1 ui-monospace,Menlo,monospace;
letter-spacing: .02em;
color: var(--muted);
}
.ccs-01__card {
width: 100%;
display: flex;
flex-direction: column;
gap: 14px;
padding: 26px;
border-radius: 16px;
background: var(--surface);
border: 1px solid var(--border);
box-shadow: 0 20px 50px -28px oklch(0 0 0/.7);
}
.ccs-01__avatar {
display: grid;
place-items: center;
width: 46px;
height: 46px;
border-radius: 14px;
font: 700 15px/1 system-ui,sans-serif;
color: #fff;
background: linear-gradient(140deg,var(--accent),oklch(from var(--accent) l calc(c*0.85) calc(h + 45)));
}
.ccs-01__card h2 {
font: 700 20px/1.15 system-ui,sans-serif;
letter-spacing: -.01em;
}
.ccs-01__card p {
font: 400 14px/1.6 system-ui,sans-serif;
color: var(--muted);
text-wrap: pretty;
}
.ccs-01__card code {
font: 600 12px/1 ui-monospace,Menlo,monospace;
padding: 2px 5px;
border-radius: 5px;
background: var(--hover);
color: var(--text);
}
.ccs-01__actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 4px;
}
.ccs-01__btn {
display: inline-flex;
align-items: center;
min-height: 44px;
padding: 0 18px;
border-radius: 11px;
font: 600 13.5px/1 system-ui,sans-serif;
text-decoration: none;
color: #fff;
background: var(--accent);
}
.ccs-01__btn--ghost {
color: var(--text);
background: transparent;
border: 1px solid var(--border);
}
.ccs-01__btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
/* ── the custom cursors: pointer-only so touch users are untouched ── */
@media (hover: hover) and (pointer: fine) {
.ccs-01__stage {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 28 28'%3E%3Cpath d='M4 3 12 24l3.1-8.6L24 12z' fill='%230b0b12' stroke='%23ffffff' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E") 4 3, auto;
}
.ccs-01__btn,
.ccs-01__hint {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3E%3Ccircle cx='15' cy='15' r='9' fill='none' stroke='%23ffffff' stroke-width='2'/%3E%3Ccircle cx='15' cy='15' r='3' fill='%237c9cff'/%3E%3C/svg%3E") 15 15, pointer;
}
}.ccs-01,
.ccs-01 *,
.ccs-01 *::before,
.ccs-01 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccs-01 {
--bg: oklch(0.16 0.02 265);
--surface: oklch(0.21 0.02 265);
--text: oklch(0.95 0.005 265);
--border: oklch(0.32 0.02 265);
--accent: oklch(0.72 0.17 264);
--muted: color-mix(in oklab,var(--text) 58%,var(--bg));
--hover: color-mix(in oklab,var(--text) 8%,transparent);
color-scheme: dark;
display: grid;
place-items: center;
width: 100%;
min-height: 100vh;
min-height: 100dvh;
padding: 28px;
font-family: system-ui,'Segoe UI',sans-serif;
background: var(--bg);
color: var(--text);
}
.ccs-01__stage {
position: relative;
width: min(460px,100%);
display: grid;
place-items: center;
padding: 40px 24px;
border-radius: 22px;
background: radial-gradient(120% 120% at 50% 0%,var(--surface),var(--bg));
border: 1px solid var(--border);
}
.ccs-01__hint {
position: absolute;
top: 14px;
left: 16px;
font: 600 11px/1 ui-monospace,Menlo,monospace;
letter-spacing: .02em;
color: var(--muted);
}
.ccs-01__card {
width: 100%;
display: flex;
flex-direction: column;
gap: 14px;
padding: 26px;
border-radius: 16px;
background: var(--surface);
border: 1px solid var(--border);
box-shadow: 0 20px 50px -28px oklch(0 0 0/.7);
}
.ccs-01__avatar {
display: grid;
place-items: center;
width: 46px;
height: 46px;
border-radius: 14px;
font: 700 15px/1 system-ui,sans-serif;
color: #fff;
background: linear-gradient(140deg,var(--accent),oklch(from var(--accent) l calc(c*0.85) calc(h + 45)));
}
.ccs-01__card h2 {
font: 700 20px/1.15 system-ui,sans-serif;
letter-spacing: -.01em;
}
.ccs-01__card p {
font: 400 14px/1.6 system-ui,sans-serif;
color: var(--muted);
text-wrap: pretty;
}
.ccs-01__card code {
font: 600 12px/1 ui-monospace,Menlo,monospace;
padding: 2px 5px;
border-radius: 5px;
background: var(--hover);
color: var(--text);
}
.ccs-01__actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 4px;
}
.ccs-01__btn {
display: inline-flex;
align-items: center;
min-height: 44px;
padding: 0 18px;
border-radius: 11px;
font: 600 13.5px/1 system-ui,sans-serif;
text-decoration: none;
color: #fff;
background: var(--accent);
}
.ccs-01__btn--ghost {
color: var(--text);
background: transparent;
border: 1px solid var(--border);
}
.ccs-01__btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
/* ── the custom cursors: pointer-only so touch users are untouched ── */
@media (hover: hover) and (pointer: fine) {
.ccs-01__stage {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 28 28'%3E%3Cpath d='M4 3 12 24l3.1-8.6L24 12z' fill='%230b0b12' stroke='%23ffffff' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E") 4 3, auto;
}
.ccs-01__btn,
.ccs-01__hint {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3E%3Ccircle cx='15' cy='15' r='9' fill='none' stroke='%23ffffff' stroke-width='2'/%3E%3Ccircle cx='15' cy='15' r='3' fill='%237c9cff'/%3E%3C/svg%3E") 15 15, pointer;
}
}/* No JavaScript by design — cursor: url() is a pure-CSS property. */
/* No JavaScript by design — cursor: url() is a pure-CSS property. */Here's a working CSS Custom Cursor Style from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Custom SVG Brand Pointer with the CSS cursor url() Property and a Keyword Fallback
Source: https://codefronts.com/snippets/css-custom-cursor/svg-brand-pointer/
The lightest possible custom cursor: no script, no image file, no request. A single inline SVG data-URI replaces the default arrow through cursor: url(...) with a hotspot offset, and a native keyword (auto / pointer) always follows the comma as a guaranteed fallback. Clickable links get their own distinct pointer, so affordance stays obvious. This is the performance-friendly baseline every brand site should reach for first.
## HTML
```html
<section class="ccs-01">
<div class="ccs-01__stage">
<span class="ccs-01__hint" aria-hidden="true">Move your pointer across this panel</span>
<article class="ccs-01__card">
<span class="ccs-01__avatar" aria-hidden="true">CF</span>
<h2>Custom brand pointer</h2>
<p>The default arrow is replaced by an inline SVG through <code>cursor: url()</code> — no image file, no request, always with a keyword fallback.</p>
<div class="ccs-01__actions">
<a class="ccs-01__btn" href="#">Links get their own cursor</a>
<a class="ccs-01__btn ccs-01__btn--ghost" href="#">Secondary</a>
</div>
</article>
</div>
</section>
```
## CSS
```css
.ccs-01,
.ccs-01 *,
.ccs-01 *::before,
.ccs-01 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccs-01 {
--bg: oklch(0.16 0.02 265);
--surface: oklch(0.21 0.02 265);
--text: oklch(0.95 0.005 265);
--border: oklch(0.32 0.02 265);
--accent: oklch(0.72 0.17 264);
--muted: color-mix(in oklab,var(--text) 58%,var(--bg));
--hover: color-mix(in oklab,var(--text) 8%,transparent);
color-scheme: dark;
display: grid;
place-items: center;
width: 100%;
min-height: 100vh;
min-height: 100dvh;
padding: 28px;
font-family: system-ui,'Segoe UI',sans-serif;
background: var(--bg);
color: var(--text);
}
.ccs-01__stage {
position: relative;
width: min(460px,100%);
display: grid;
place-items: center;
padding: 40px 24px;
border-radius: 22px;
background: radial-gradient(120% 120% at 50% 0%,var(--surface),var(--bg));
border: 1px solid var(--border);
}
.ccs-01__hint {
position: absolute;
top: 14px;
left: 16px;
font: 600 11px/1 ui-monospace,Menlo,monospace;
letter-spacing: .02em;
color: var(--muted);
}
.ccs-01__card {
width: 100%;
display: flex;
flex-direction: column;
gap: 14px;
padding: 26px;
border-radius: 16px;
background: var(--surface);
border: 1px solid var(--border);
box-shadow: 0 20px 50px -28px oklch(0 0 0/.7);
}
.ccs-01__avatar {
display: grid;
place-items: center;
width: 46px;
height: 46px;
border-radius: 14px;
font: 700 15px/1 system-ui,sans-serif;
color: #fff;
background: linear-gradient(140deg,var(--accent),oklch(from var(--accent) l calc(c*0.85) calc(h + 45)));
}
.ccs-01__card h2 {
font: 700 20px/1.15 system-ui,sans-serif;
letter-spacing: -.01em;
}
.ccs-01__card p {
font: 400 14px/1.6 system-ui,sans-serif;
color: var(--muted);
text-wrap: pretty;
}
.ccs-01__card code {
font: 600 12px/1 ui-monospace,Menlo,monospace;
padding: 2px 5px;
border-radius: 5px;
background: var(--hover);
color: var(--text);
}
.ccs-01__actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 4px;
}
.ccs-01__btn {
display: inline-flex;
align-items: center;
min-height: 44px;
padding: 0 18px;
border-radius: 11px;
font: 600 13.5px/1 system-ui,sans-serif;
text-decoration: none;
color: #fff;
background: var(--accent);
}
.ccs-01__btn--ghost {
color: var(--text);
background: transparent;
border: 1px solid var(--border);
}
.ccs-01__btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
/* ── the custom cursors: pointer-only so touch users are untouched ── */
@media (hover: hover) and (pointer: fine) {
.ccs-01__stage {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 28 28'%3E%3Cpath d='M4 3 12 24l3.1-8.6L24 12z' fill='%230b0b12' stroke='%23ffffff' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E") 4 3, auto;
}
.ccs-01__btn,
.ccs-01__hint {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3E%3Ccircle cx='15' cy='15' r='9' fill='none' stroke='%23ffffff' stroke-width='2'/%3E%3Ccircle cx='15' cy='15' r='3' fill='%237c9cff'/%3E%3C/svg%3E") 15 15, pointer;
}
}
```
## JavaScript
```js
/* No JavaScript by design — cursor: url() is a pure-CSS property. */
```Here's a working CSS Custom Cursor Style from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Custom SVG Brand Pointer with the CSS cursor url() Property and a Keyword Fallback
Source: https://codefronts.com/snippets/css-custom-cursor/svg-brand-pointer/
The lightest possible custom cursor: no script, no image file, no request. A single inline SVG data-URI replaces the default arrow through cursor: url(...) with a hotspot offset, and a native keyword (auto / pointer) always follows the comma as a guaranteed fallback. Clickable links get their own distinct pointer, so affordance stays obvious. This is the performance-friendly baseline every brand site should reach for first.
## HTML
```html
<section class="ccs-01">
<div class="ccs-01__stage">
<span class="ccs-01__hint" aria-hidden="true">Move your pointer across this panel</span>
<article class="ccs-01__card">
<span class="ccs-01__avatar" aria-hidden="true">CF</span>
<h2>Custom brand pointer</h2>
<p>The default arrow is replaced by an inline SVG through <code>cursor: url()</code> — no image file, no request, always with a keyword fallback.</p>
<div class="ccs-01__actions">
<a class="ccs-01__btn" href="#">Links get their own cursor</a>
<a class="ccs-01__btn ccs-01__btn--ghost" href="#">Secondary</a>
</div>
</article>
</div>
</section>
```
## CSS
```css
.ccs-01,
.ccs-01 *,
.ccs-01 *::before,
.ccs-01 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccs-01 {
--bg: oklch(0.16 0.02 265);
--surface: oklch(0.21 0.02 265);
--text: oklch(0.95 0.005 265);
--border: oklch(0.32 0.02 265);
--accent: oklch(0.72 0.17 264);
--muted: color-mix(in oklab,var(--text) 58%,var(--bg));
--hover: color-mix(in oklab,var(--text) 8%,transparent);
color-scheme: dark;
display: grid;
place-items: center;
width: 100%;
min-height: 100vh;
min-height: 100dvh;
padding: 28px;
font-family: system-ui,'Segoe UI',sans-serif;
background: var(--bg);
color: var(--text);
}
.ccs-01__stage {
position: relative;
width: min(460px,100%);
display: grid;
place-items: center;
padding: 40px 24px;
border-radius: 22px;
background: radial-gradient(120% 120% at 50% 0%,var(--surface),var(--bg));
border: 1px solid var(--border);
}
.ccs-01__hint {
position: absolute;
top: 14px;
left: 16px;
font: 600 11px/1 ui-monospace,Menlo,monospace;
letter-spacing: .02em;
color: var(--muted);
}
.ccs-01__card {
width: 100%;
display: flex;
flex-direction: column;
gap: 14px;
padding: 26px;
border-radius: 16px;
background: var(--surface);
border: 1px solid var(--border);
box-shadow: 0 20px 50px -28px oklch(0 0 0/.7);
}
.ccs-01__avatar {
display: grid;
place-items: center;
width: 46px;
height: 46px;
border-radius: 14px;
font: 700 15px/1 system-ui,sans-serif;
color: #fff;
background: linear-gradient(140deg,var(--accent),oklch(from var(--accent) l calc(c*0.85) calc(h + 45)));
}
.ccs-01__card h2 {
font: 700 20px/1.15 system-ui,sans-serif;
letter-spacing: -.01em;
}
.ccs-01__card p {
font: 400 14px/1.6 system-ui,sans-serif;
color: var(--muted);
text-wrap: pretty;
}
.ccs-01__card code {
font: 600 12px/1 ui-monospace,Menlo,monospace;
padding: 2px 5px;
border-radius: 5px;
background: var(--hover);
color: var(--text);
}
.ccs-01__actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 4px;
}
.ccs-01__btn {
display: inline-flex;
align-items: center;
min-height: 44px;
padding: 0 18px;
border-radius: 11px;
font: 600 13.5px/1 system-ui,sans-serif;
text-decoration: none;
color: #fff;
background: var(--accent);
}
.ccs-01__btn--ghost {
color: var(--text);
background: transparent;
border: 1px solid var(--border);
}
.ccs-01__btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 3px;
}
/* ── the custom cursors: pointer-only so touch users are untouched ── */
@media (hover: hover) and (pointer: fine) {
.ccs-01__stage {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 28 28'%3E%3Cpath d='M4 3 12 24l3.1-8.6L24 12z' fill='%230b0b12' stroke='%23ffffff' stroke-width='2' stroke-linejoin='round'/%3E%3C/svg%3E") 4 3, auto;
}
.ccs-01__btn,
.ccs-01__hint {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3E%3Ccircle cx='15' cy='15' r='9' fill='none' stroke='%23ffffff' stroke-width='2'/%3E%3Ccircle cx='15' cy='15' r='3' fill='%237c9cff'/%3E%3C/svg%3E") 15 15, pointer;
}
}
```
## JavaScript
```js
/* No JavaScript by design — cursor: url() is a pure-CSS property. */
```How this works
The cursor property accepts a comma-separated list: one or more url() images followed by a mandatory keyword fallback. The browser walks the list and uses the first cursor it can load, so cursor: url("…") 4 3, auto means "use my SVG, but if it fails, fall back to the system arrow." The two trailing numbers are the hotspot — the exact pixel inside the image that counts as the click point (here the arrow's tip at 4 3). Omit them and the browser guesses, which makes clicks feel misaligned.
The art is an inline SVG data-URI, so there is zero network request and the cursor ships inside your CSS. Only three characters must be percent-encoded for a data-URI to be valid — <→%3C, >→%3E, and #→%23 in hex colors. The whole override is wrapped in @media (hover:hover) and (pointer:fine) so touch and stylus users — who have no pointer to restyle — keep the native behavior and never get a stuck cursor.
Techniques used: cursor: url() with inline SVG data-URI · hotspot offset (the two trailing numbers) · mandatory keyword fallback after the comma · distinct cursor for links vs. surface · @media (hover:hover) and (pointer:fine) touch guard · percent-encoding SVG for a valid data-URI
/* image hotspot-x hotspot-y , REQUIRED keyword fallback */
.stage { cursor: url("data:image/svg+xml,%3Csvg…%3E") 4 3, auto; }
.stage a,
.stage button{ cursor: url("data:image/svg+xml,%3Csvg…%3E") 12 4, pointer; }Make it yours
- Swap the SVG paths for your logo mark — keep it ≤ 32×32px; most browsers cap cursor images around 128px and ignore anything larger.
- Need a raster? Use
cursor: url('cursor.png') 4 3, url('cursor.svg') 4 3, auto— list PNG first for ancient engines, SVG next, keyword last. - Give inputs and text areas
cursor: textback explicitly so a global custom cursor never breaks editing affordance. - Add a
:activevariant with a 'pressed' SVG for a tactile click, still pure CSS.
Gotchas — read before shipping
- The keyword fallback is REQUIRED —
cursor: url("…")with no trailing keyword is invalid and the whole declaration is dropped. - Forgetting the hotspot makes clicks land off-target; always set it to the visual tip/point of your art.
- SVG cursors must be a valid standalone document with
xmlns; a missing namespace silently fails to load. - Don't set a giant custom cursor globally — oversized images are ignored and huge ones hurt precision and accessibility.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 114+ | 17.5+ | 121+ | 114+ |
Floor set by oklch(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 1+.
cursor: url() is ancient. SVG cursors: Chrome/Firefox yes; Safari historically prefers PNG/ICO for url() cursors, so ship a PNG first in the list if Safari coverage matters.