6 CSS Custom Cursor Styles01 / 06

Pure CSSMIT licensed

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

Live Demo
Try it

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>
.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. */
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
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: text back explicitly so a global custom cursor never breaks editing affordance.
  • Add a :active variant 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

ChromeSafariFirefoxEdge
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.

Techniques used in this demo

Search CodeFronts

Loading…