Bir açılır menü için kaç satır JavaScript yazdığınızı hatırlıyor musunuz? Dışarı tıklayınca kapanması, Escape tuşuyla kapanması, klavye ile gezinmesi, ekran okuyucuya açık ya da kapalı olduğunu bildirmesi. Her biri küçük bir iş gibi görünür, ama toplamda bir bileşen kütüphanesi kadar kod çıkar. Alpine.js'in çekiciliği bu kodun büyük kısmını HTML öznitelikleri içine taşımasında.

Alpine'in boyutu küçük, sözdizimi tanıdık ve kurulum neredeyse yoktur. Ama asıl değeri, bir bileşenin davranışının o bileşenin HTML'ine bakılarak okunabilmesidir. Bir öğenin durumu, o öğenin yanında yazılıysa bakım maliyeti düşer. Bu metinde Alpine ile sıfır bağımlılıklı, ama yine de erişilebilir ve sağlam etkileşimlerin nasıl kurulduğunu anlatacağım.

Yerel durum, yerel çözüm

Alpine'in temel birimi x-data özniteliğidir. Bu öznitelik, bir öğe ağacının durumunu tanımlar. Durum, o öğenin içinde yaşar ve dışarıdan yalnızca gerekirse erişilir. Bir açılır menü yalnızca kendi açık ya da kapalı durumunu biliyorsa, bu bilgiyi global bir depoya taşımanın bir anlamı yoktur.

<div x-data="{ open: false }">
    <button type="button" @click="open = !open" :aria-expanded="open">
        Menü
    </button>
    <div x-show="open" x-transition @click.outside="open = false">
        <a href="/yazilar">Yazılar</a>
        <a href="/hakkinda">Hakkında</a>
    </div>
</div>

Bu kodda üç Alpine kavramı bir arada: x-data durumu tanımlar, @click olayı dinler, x-show görünürlüğü yönetir. :aria-expanded ise durumu erişilebilirlik özniteliğine bağlar. Menünün açık olup olmadığını ekran okuyucu da bilir.

Az kod, çok niyet: iyi bir arayüz katmanı kendini gizler.

Dört temel direktif

Alpine'in çoğu etkileşimi dört temel direktifle kurulabilir. Hepsini ezberlemek gerekmiyor; hangisinin ne iş yaptığını bilmek yeterli.

DirektifGöreviÖrnek
x-dataBileşenin durumunu tanımlarx-data="{ sekme: 'bir' }"
x-showGörünürlüğü CSS ile açıp kapatırx-show="open"
x-ifÖğeyi DOM'dan tamamen ekler ya da çıkarırtemplate x-if="hata"
x-modelGirdiyi durumla çift yönlü bağlarx-model="arama"

x-show ile x-if arasındaki fark, sık karıştırılan bir ayrımdır. x-show öğeyi sayfada tutar ve yalnızca gizler; bu yüzden odak ve ekran okuyucu ilişkileri korunur, ama gizli öğenin içeriği DOM'da kalır. x-if öğeyi gerçekten kaldırır, ağır içerikler için daha uygundur ama her açılışta yeniden oluşturulur.

Sekmeler: klavyeyi unutmadan

Sekmeler, Alpine ile en çok yazılan bileşenlerdendir ve en çok yanlış yazılanlardan da. Görsel olarak çalışan bir sekme, klavye ile gezilemiyorsa erişilebilir değildir. Ok tuşlarıyla geçiş, doğru role değerleri ve aria-selected bu yüzden birlikte düşünülmelidir.

<div x-data="{ sekme: 'ozet' }">
    <div role="tablist" aria-label="Yazı bilgileri">
        <button role="tab" id="sekme-ozet"
            :aria-selected="sekme === 'ozet'"
            :tabindex="sekme === 'ozet' ? 0 : -1"
            @click="sekme = 'ozet'"
            @keydown.right.prevent="sekme = 'yorumlar'"
        >Özet</button>
        <button role="tab" id="sekme-yorumlar"
            :aria-selected="sekme === 'yorumlar'"
            :tabindex="sekme === 'yorumlar' ? 0 : -1"
            @click="sekme = 'yorumlar'"
            @keydown.left.prevent="sekme = 'ozet'"
        >Yorumlar</button>
    </div>

    <div role="tabpanel" x-show="sekme === 'ozet'">…</div>
    <div role="tabpanel" x-show="sekme === 'yorumlar'" x-cloak>…</div>
</div>

tabindex ayrıntısı önemli. Yalnızca seçili sekme Tab sırasına girer; diğerleri ok tuşlarıyla erişilir. Bu, WAI-ARIA sekme desenine uygun bir davranıştır ve klavye kullanıcıları için sekmeler arasında gezinmeyi kısa tutar.

  • x-cloak ile Alpine yüklenmeden önce görünen içerik gizlenir; bunun için ilgili CSS kuralını da sayfaya eklemeyi unutmayın.
  • Sekme panelleri aria-labelledby ile sekmelere bağlanmalıdır; bu bağlantı ekran okuyucu için öğeyi anlamlı kılar.

Modal, Alpine ile en kolay kurulan ama en çok hata yapılan bileşendir. Açıldığında odak içeri alınmalı, Escape ile kapanmalı ve kapandığında odak onu açan düğmeye dönmelidir. Odak tuzağı için Alpine'in resmi x-trap eklentisi kullanılabilir.

<div x-data="{ acik: false }">
    <button type="button" x-ref="acici" @click="acik = true">Paylaş</button>

    <div x-show="acik" x-trap.noscroll="acik" @keydown.escape.window="acik = false"
         role="dialog" aria-modal="true" aria-labelledby="paylas-baslik">
        <h2 id="paylas-baslik">Yazıyı paylaş</h2>
        <button type="button" @click="acik = false; $refs.acici.focus()">Kapat</button>
    </div>
</div>

Bu örnekte $refs, kapanınca odağı açan düğmeye geri götürür. Bu küçük adımı atlamak, klavye kullanıcılarını sayfanın başına döndürür ve nerede olduklarını kaybettirir.

Modal içinde arka plan kaydırmasını da engellemek gerekir; x-trap.noscroll bunu yapar. Ama bu davranışın kapanma anında geri alındığını kontrol edin, yoksa sayfa kilitli kalabilir.

Olaylar ve bileşenler arası iletişim

Bir bileşenin başka bir bileşeni tetiklemesi gerektiğinde iki yol var: doğrudan erişim ya da olay. Doğrudan erişim, iki bileşen aynı ağaçta olduğunda işe yarar. Olay ise daha gevşek bir bağ kurar. $dispatch ile bir olay yayınlarsınız, başka bir öğe .window değiştiricisiyle bu olayı dinler.

<button type="button" @click="$dispatch('open-modal', 'paylas')">Paylaş</button>

<div x-data="{ acik: false }"
     @open-modal.window="if ($event.detail === 'paylas') acik = true">
    …
</div>

Bu desen, Pines UI gibi Blade ve Alpine tabanlı bileşen kütüphanelerinde de yaygındır. Modal, açılır menü ya da bildirim katmanı, tetikleyici ile içerik arasında bir olayla konuşur ve birbirlerinin iç durumunu bilmek zorunda kalmaz.

Global bir durum gerekirse Alpine.store kullanılabilir. Ama her şeyi global yapmak, yerel durumun getirdiği sadeliği kaybettirir. Kural basit: durum yalnızca bir yerde kullanılıyorsa yerel kalmalı.

Alpine ile HTMX birlikte

Sunucudan gelen HTML parçaları sayfaya eklendiğinde, içindeki Alpine öznitelikleri de çalışmalıdır. Alpine bu iş için bir gözlemci kullanır ve yeni eklenen öğeleri otomatik olarak başlatır. Bu yüzden HTMX ile yerleştirilen bir parçada x-data ayrıca bir yere kaydedilmek zorunda değildir.

Ancak dikkat edilmesi gereken bir nokta var: parça değiştiğinde eski durum yok olur. Bir sekmenin seçili olduğu, bir menünün açık kaldığı durum, yeni parçayla birlikte sıfırlanır. Bu istenmiyorsa durumu parçanın dışındaki bir kapsayıcıya taşımak ya da sunucudan gelen parçanın kendisine varsayılan değeri yazmak gerekir.

  • Parça içinde Alpine öznitelikleri olabilir, ama durum yalnızca parçaya ait olmalıdır.
  • Kalıcı tercihler (örneğin koyu tema) için localStorage kullanın ve okumayı bir try bloğuna alın.
  • HTMX olaylarını (htmx:afterSwap) dinlemek gerekirse Alpine'in x-init öznitelik içinde kalın.

Erişilebilirlik kontrol listesi

Alpine ile yazılan her etkileşim için birkaç temel soruyu sormak yeterlidir. Bu sorular bir kontrol listesi gibi, her bileşende tekrar edilebilir.

SoruKontrol
Klavye ile açılabiliyor mu?Enter ve Space ile tetiklenen düğmeler kullanın, div değil
Durum ekran okuyucuya bildiriliyor mu?aria-expanded, aria-selected gibi öznitelikleri bağlayın
Escape ile kapanıyor mu?@keydown.escape ekleyin
Odak kapanınca nereye dönüyor?Tetikleyici öğeye geri odaklanın
Dışarı tıklamak kapatıyor mu?@click.outside ekleyin

Bu listedeki her madde basit görünür, ama birlikte uygulandığında bileşen gerçekten herkes için çalışır. Ekran okuyucuyla bir kez test etmek, listenin hangi maddelerinin eksik olduğunu hızla gösterir.

Sık yapılan hatalar

Alpine'de en yaygın hata, durumu gereğinden fazla büyütmektir. Her şeyi bir x-data içine koymak bir süre sonra okunmaz hâle gelir. Bir bileşen on alandan fazla durum taşıyorsa, onu daha küçük parçalara ayırmanın zamanı gelmiş olabilir.

İkinci hata, ifadelerin içine mantık yazmaktır. @click="sepet.push(urun); toplam = sepet.reduce((a, b) => a + b.fiyat, 0); bildirimGoster = true" gibi bir satır, okunması ve test edilmesi zor bir yapı kurar. Bu tür bir mantık bir fonksiyona taşınmalı ve x-data içinde tanımlanmalıdır.

Üçüncü hata, erişilebilirliği sona bırakmaktır. Görsel olarak tamamlanmış bir bileşene sonradan aria öznitelikleri eklemek, genellikle yapının yeniden düşünülmesini gerektirir. Rol ve durum öznitelikleri, bileşen ilk yazılırken birlikte düşünülmelidir.

Liste ve filtre: x-for ile çalışmak

Alpine ile bir liste üretmek ve filtrelemek, sunucuya gitmeden de mümkündür. x-for direktifi bir şablonu bir dizinin her elemanı için çoğaltır. Filtrelenmiş bir liste için, filtre metni ve sonuç bir arada tutulur; görünüm yalnızca bu sonucu okur.

<div x-data="{
    arama: '',
    etiketler: ['htmx', 'alpine-js', 'laravel', 'blade', 'tailwind-css'],
    get sonuc() {
        const aranan = this.arama.trim().toLowerCase();
        return aranan === ''
            ? this.etiketler
            : this.etiketler.filter((etiket) => etiket.includes(aranan));
    },
}">
    <label for="etiket-ara">Etiket ara</label>
    <input id="etiket-ara" type="search" x-model="arama">

    <ul>
        <template x-for="etiket in sonuc" :key="etiket">
            <li x-text="etiket"></li>
        </template>
    </ul>

    <p role="status" aria-live="polite">
        <span x-text="sonuc.length"></span> etiket bulundu
    </p>
</div>

Bu örnekte üç ayrıntı önemli. Birincisi, get sonuc() bir hesaplanmış özelliktir ve yalnızca okunduğunda çalışır. İkincisi, :key her elemanı benzersiz tutar; bu olmadan Alpine yeniden çizimde öğeleri yanlış eşleştirebilir. Üçüncüsü, sonuç sayısı canlı bir bölgede duyurulur. Kullanıcı yazarken ekran okuyucu her değişikliği değil, sonuç sayısını duyar.

Büyük listelerde bu yaklaşım bir sınıra gelir. Yüzlerce elemanı her tuş vuruşunda yeniden filtrelemek, tarayıcıda gözle görülür bir gecikme yaratabilir. O noktada sunucu tarafında filtreleme ya da bir HTMX çağrısı daha uygun olur.

Form alanları ve yerel doğrulama

Alpine, bir formun alanlarını anlık olarak izlemek ve kullanıcıya hızlı geri bildirim vermek için de kullanılabilir. Bir e-posta alanının biçimi, kullanıcı yazarken kontrol edilebilir ve hata metni hemen gösterilebilir. Bu, sunucuya gitmeden önce kullanıcıyı yönlendirir.

<div x-data="{ eposta: '', hataliMi() { return this.eposta !== '' && !this.eposta.includes('@'); } }">
    <label for="eposta">E-posta</label>
    <input id="eposta" name="eposta" type="email" x-model="eposta"
           :aria-invalid="hataliMi()" aria-describedby="eposta-hata">
    <p id="eposta-hata" x-show="hataliMi()" x-cloak>Geçerli bir adres yazın.</p>
</div>

Bu kontrol yalnızca bir kolaylıktır. Asıl doğrulama her zaman sunucuda yapılmalıdır; tarayıcıdaki kontrol atlanabilir ve yalnızca kullanıcıya zaman kazandırır. Bu nedenle istemci tarafı doğrulama, sunucu kuralının bir yansıması olarak düşünülmeli, ona alternatif olarak değil.

Geçişler ve hareket tercihi

x-transition ile gelen varsayılan geçişler çoğu durumda yeterlidir. Ama sürelerini ve yumuşatmayı ayarlamak, arayüzün karakterini değiştirir. Bunun için her aşamayı ayrı ayrı tanımlayabilirsiniz:

<div x-show="acik"
     x-transition:enter="transition ease-out duration-200"
     x-transition:enter-start="opacity-0 -translate-y-1"
     x-transition:enter-end="opacity-100 translate-y-0"
     x-transition:leave="transition ease-in duration-150"
     x-transition:leave-start="opacity-100"
     x-transition:leave-end="opacity-0">
    …
</div>

Hareketin bazı kullanıcılar için rahatsız edici olabileceğini unutmayın. İşletim sisteminde hareket azaltma tercihi açıksa, geçiş süresini sıfıra indirmek ya da geçişi tamamen kaldırmak gerekir. Bunu tek bir yerden yönetmek için tercihi bir kez okuyup bir değişkende tutmak yeterlidir:

<body x-data="{ hareketAzalt: window.matchMedia('(prefers-reduced-motion: reduce)').matches }">

Bu değer daha sonra panellerin süresine bağlanabilir. Böylece her bileşen aynı kuralı uygular ve bir bileşeni kopyalayan kişi kuralı kendiliğinden miras alır.

Sayfa yüklenirken görünmeyen içerik

Alpine yüklenmeden önce, x-show ile gizlenmiş öğeler kısa süreliğine görünür olabilir. Bu, sayfanın ilk saniyesinde kafa karıştırıcı bir görüntü yaratır. x-cloak özniteliği bu sorunu çözer; ama CSS'te karşılık gelen kuralın da tanımlı olması gerekir:

[x-cloak] {
    display: none !important;
}

Bu kural yüklenmeden önce öğeleri gizler, Alpine başladığında öznitelik kaldırılır ve öğe normal davranışına döner. Kuralı unutmak, x-cloak özniteliğini tamamen etkisiz bırakır; bu yüzden bu kuralı ana CSS dosyasının başına koymak en güvenli yoldur.

Bileşenleri test etmek

Alpine bileşenlerini test etmenin en gerçekçi yolu, tarayıcı otomasyonu kullanmaktır. Bir modalın açılıp kapandığını, Escape ile kapandığını ve odağın geri döndüğünü bir senaryo olarak yazmak, manuel denetimin yerini tutar. Bu testler yavaş olabilir, bu yüzden yalnızca kritik etkileşimler için kullanılmalıdır.

  1. Açılış ve kapanış davranışını test edin.
  2. Klavye ile tüm öğelerin erişilebilir olduğunu doğrulayın.
  3. Odağın tetikleyiciye döndüğünü kontrol edin.
  4. Hareket azaltma tercihi açıkken geçişin uygulanmadığını görün.

Sonuç

Alpine.js, bir arayüzün sunucuya gitmesi gerekmeyen kısmını sade ve okunabilir biçimde yönetmek için iyi bir araç. Doğru kullanıldığında, JavaScript dosyalarını küçük tutar, davranışı işaretlendiği yere yakın tutar ve erişilebilirliği bileşen tasarımının bir parçası hâline getirir.

Yeni bir bileşen yazarken şu sırayı izleyin:

  1. Durumun yalnızca bu bileşene ait olup olmadığına bakın.
  2. Rol ve durum öznitelikleri olmadan bileşeni yazmaya başlamayın.
  3. Klavye ile iki kez baştan sona gezin.
  4. Ekran okuyucuyla, bileşenin ne olduğunu ve ne durumda olduğunu duyup duymadığınızı kontrol edin.
Alpine'in en iyi özelliği, yanlış yapmanın bile görünür olmasıdır: yanlış bir öznitelik, HTML'de hemen göze çarpar.