Tüm yazılar
09 · Tasarım ve etkileşim · 2026.10 · 6 dk

İmleci kovalamayı bıraktım: CSS cursor ile yerel SVG imleç

Özel imleç çoğu sitede fareyi kovalayan bir elemandır ve hep biraz geride kalır. Bu sitede imleci tarayıcının kendisine çizdirdim: cursor özelliğinde SVG data URI, iki CSS değişkeni ve gizli mod için bir x. JavaScript'le çalışan disk artık yalnızca etiket taşıyor.

01Eski yaklaşım

İlk sürümde sistem imlecinin etrafında onu takip eden bir halka vardı. Fixed konumlu bir eleman pointermove ile hedef koordinatı alıyor, bir requestAnimationFrame döngüsü onu her karede bu hedefe biraz daha yaklaştırıyordu. Etkileşimli bir elemanın üzerine gelince büyüyor, data-cur özniteliği olan elemanlarda içinde bir etiket gösteriyordu.

Göze hoş geliyordu ama iki sorunu vardı. Birincisi yapısal gecikme: eleman olay geldikten sonra çiziliyor, yumuşatma eklenince halka gerçek imlecin hep gerisinde kalıyor. İkincisi maliyet: döngü ilk satırında bir sonraki kareyi istiyordu, yani fare hareketsizken bile her karede çalışıyordu. Ana thread meşgul olduğunda da ilk takılan şey bu halka oluyordu.

İşletim sistemi imleci ise sayfanın JavaScript yükünden bağımsız çiziliyor. CSS'in cursor özelliği url() ile özel bir görsel kabul ediyor. Yani imlecin hareketini tarayıcıya bırakıp sadece görünüşünü ben verebilirdim.

02İki değişken, iki imleç

tokens.css'te iki değişken var: --c-def ve --c-ptr. Varsayılan değerleri düz anahtar kelimeler: default ve pointer. Yalnızca (hover: hover) and (pointer: fine) sorgusu eşleşirse, yani hassas bir işaretçi varsa, bu değerler SVG data URI'leriyle değiştiriliyor. Varsayılan imleç dört kısa koldan oluşan bir artı ve ortasında Iris renkli küçük bir daire; tıklanabilir elemanlardaki imleç ise bir halka ve ortasında Soft renkli bir nokta.

src/styles/tokens.css (kısaltıldı)
:root { --c-def: default; --c-ptr: pointer; }
@media (hover: hover) and (pointer: fine) {
  :root {
    --c-def: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'><path d='M16 4v7M16 21v7M4 16h7M21 16h7' stroke='%230B0C11' stroke-width='3.5'/><path d='M16 4v7M16 21v7M4 16h7M21 16h7' stroke='%23EDECF2' stroke-width='1.5'/><circle cx='16' cy='16' r='3.2' fill='%23A8A1F1' stroke='%230B0C11' stroke-width='1.5'/></svg>") 16 16, default;
    --c-ptr: url("data:image/svg+xml,...") 16 16, pointer;
  }
  .app.mv { --c-def: url("data:image/svg+xml,...") 16 16, default; /* the x */ }
  html, .app { cursor: var(--c-def); }
  .app a, .app button, .app label, .app summary, .app [role='button'], .app input[type='range'] { cursor: var(--c-ptr); }
  .app input[type='text'], .app input[type='search'], .app textarea { cursor: text; }
}

Her SVG'de çizgiler iki kez çiziliyor: önce kalın bir Void (#0B0C11) kontur, üstüne ince asıl renk. Bu sayede imleç hem koyu zeminde hem Iris ve Soft yüzeylerde okunuyor. Renkler data URI içinde olduğu için # karakteri %23 olarak yazılıyor.

Görseli ayrı bir dosya yerine data URI olarak CSS'in içine yazdım. Böylece imleç stil dosyasıyla birlikte geliyor, ayrı bir isteği beklemiyor ve renklerin tanımlandığı aynı dosyada, tokenların hemen altında duruyor. Bir rengi değiştirmek istediğimde nereye bakacağım belli. Bedeli okunabilirlik: tek satırlık bir SVG'yi düzenlemek, ayrı bir dosyayı düzenlemek kadar rahat değil. Bunu kabul ettim; imleçler sık değişen şeyler değil.

03Hotspot ve yedek

url() değerinden sonraki 16 16, imlecin tıklama noktası. SVG 32×32 olduğu için hotspot tam merkezde; artının ortası ile tıklanan nokta aynı. Bunu yazmasaydım tarayıcı sol üst köşeyi kullanırdı ve imleç, tıkladığı yerin yarım ikon yanında duruyormuş gibi görünürdü.

Virgülden sonraki default ve pointer ise yedek. Tarayıcı görseli kullanamazsa bu anahtar kelimeye düşüyor; zaten cursor sözdiziminde url() listesinin bir anahtar kelimeyle bitmesi gerekiyor. Kullanım tarafı sade: html ve .app --c-def alıyor; .app içindeki a, button, label, summary, [role='button'] ve range input'lar --c-ptr. Metin alanları text imlecini koruyor, çünkü orada okurun gerçekten imleç konumuna ihtiyacı var. Bileşen stilleri de doğrudan cursor: var(--c-ptr) kullanıyor; playground'daki bazı sahneler ew-resize gibi kendi imlecini seçiyor.

İmleci tarayıcı çizdiğinde geride kalacak bir döngü de kalmıyor.

04Dokunmatik cihazlar ve gizli mod

Dokunmatik ekranlarda zaten imleç yok. Coarse işaretçili cihazlarda medya sorgusu eşleşmiyor ve değişkenler düz anahtar kelimelerde kalıyor. Ayrı bir cihaz kontrolü yazmama gerek olmadı.

Değişken kullanmanın asıl faydası gizli modda ortaya çıktı. Mauve modu açıkken .app elemanına mv sınıfı ekleniyor ve aynı medya sorgusu içinde .app.mv iki değişkeni yeniden tanımlıyor: varsayılan imleç Mauve renkli bir x oluyor, tıklanabilir elemanlardaki imleç içinde x olan bir halka. Hiçbir bileşen değişmiyor, cursor kuralları aynı kalıyor; yalnızca değişkenin değeri değişiyor ve bütün sayfa yeni imleci kullanıyor.

05Geriye kalan disk

CustomCursor.vue hâlâ duruyor ama görevi değişti. Bazı elemanlarda data-cur özniteliği var; shell.css'teki yorumun örnekleriyle Aç ya da Kopyala gibi bir etiket. Disk yalnızca böyle bir elemanın üzerindeyken görünüyor ve etiketi taşıyor. pointermove dinleyicisi etiketi arıyor; etiket yoksa döngüyü hiç başlatmıyor.

src/components/layout/CustomCursor.vue
function loop() {
  raf = 0
  if (!vis || !el.value) return
  rx += (cx - rx) * 0.22
  ry += (cy - ry) * 0.22
  el.value.style.transform = `translate3d(${rx.toFixed(1)}px,${ry.toFixed(1)}px,0)`
  if (Math.abs(cx - rx) > 0.2 || Math.abs(cy - ry) > 0.2) raf = requestAnimationFrame(loop)
}

Etiket varsa disk her karede kalan mesafenin bir kısmını kapatıyor, fark 0.2 pikselin altına inince döngü kendini durduruyor. Dokunma olayları yok sayılıyor; hareket azaltma açıksa ya da işaretçi coarse ise dinleyiciler hiç eklenmiyor. Buradaki gecikme kabul edilebilir, çünkü disk imleç değil, imlecin yanında duran bir etiket. Gerçek imleç her zaman yerinde. Bedeli de belli: imleç bir görsel olduğu için hover'da büyüme gibi geçişler yapamıyorum, durum değişimi anlık.

İlgili yazılar
  1. 10Görmediğini durdur: döngüleri ekran dışında ve perde arkasında uyutmakArayüz2026.106 dk
  2. 11Tasarım prototipinden üretim koduna: kontrollü kaosu Vue'ya taşımakTasarım ve etkileşim2026.105 dk