---
title: 'Başsız Bileşenler'
source: 'https://academia.sh/tr/kurslar/bilesen-tabanli-gelistirme/bassiz-bilesenler'
course: 'Bileşen Tabanlı Arayüz Geliştirme'
language: tr
updated: '2026-08-17T18:09:13+00:00'
license: 'CC BY-SA 4.0'
---

# Başsız Bileşenler

Davranışın görünümden koparılması; seçenek listesinin saf durum makinesi olarak yazılması, erişilebilirlik özniteliklerinin durumdan türetilmesi, olay dizisiyle sınanması ve aynı davranışın iki görünümle kullanılması.

Süzgeç panelinde istasyon seçmek için bir liste var. Listenin davranışı ayrıntılıdır: ok
tuşlarıyla gezinme, kapalı öğeyi atlama, listenin ucunda başa dönme, ilk ve son öğeye
atlama, harfe basınca eşleşen öğeye gitme, seçim, iptal. Bu davranış aynı zamanda bir
erişilebilirlik sözleşmesi taşır — hangi öğenin etkin olduğu, listenin açık olup olmadığı ve
hangi öğenin seçili olduğu yardımcı teknolojiye bildirilmek zorundadır.

Görünüm ise her yerde farklı. Süzgeç panelinde düğmeye bağlı açılır bir liste, geniş
ekranda yan panelde her zaman açık duran bir liste, ölçüm tablosunun başlığında satır içi
bir seçici. Davranışı bir bileşene gömmek, bu üç görünümü tek bir görünüme indirmek demektir.

## Davranış Bir Bileşen Değildir

**Başsız bileşen (headless component)**, davranışı ve erişilebilirlik sözleşmesini taşıyan
ama hiçbir görünüm üretmeyen birimdir. Dışarıya iki şey verir: okunacak bir durum ve
işaretlemeye takılacak öznitelikler. Ne öge seçer, ne sınıf yazar, ne biçem tanımlar.

Önceki dersteki ayrımın bir adım ilerisidir. Sunum ve konteyner ayrımı veriyi görünümden
ayırmıştı; burada ayrılan şey davranıştır. Sonuçta üç bağımsız katman kalır: veriyi elde
eden, davranışı yürüten ve görünümü üreten.

Davranışın taşıyıcısı bir durum makinesidir. Programlama Temelleri kursundaki saf fonksiyon
tanımına uyan bir indirgeyici — bir durum ve bir olay alır, yeni durum döndürür — davranışın
tamamını taşıyabilir. Zamanlayıcı, DOM erişimi ve ağ çağrısı bu işlevin dışında kalır.

```js
// bassiz.mjs — secenek listesinin davranisi: saf durum makinesi, gorunum yok
export const SECENEKLER = [
  { deger: "ust", metin: "Üst yamaç" },
  { deger: "orta", metin: "Orta teras" },
  { deger: "alt", metin: "Alt yamaç", pasif: true },
  { deger: "zirve", metin: "Zirve" },
];

export const baslangic = (secili = null) =>
  ({ acik: false, etkin: -1, secili, arabellek: "", sonHarfZamani: -Infinity });

const secilebilir = (s, i) => i >= 0 && i < s.length && !s[i].pasif;
const uc = (s, bastan) =>
  (bastan ? s.map((_, i) => i) : s.map((_, i) => s.length - 1 - i)).find((i) => secilebilir(s, i)) ?? -1;
const ilerle = (s, baslangicDizini, yon) => {
  if (baslangicDizini < 0) return uc(s, yon > 0);
  for (let adim = 1; adim <= s.length; adim++) {
    const i = (baslangicDizini + yon * adim + s.length * adim) % s.length;
    if (secilebilir(s, i)) return i;
  }
  return -1;
};

// Saf indirgeyici: (durum, olay) -> yeni durum. Yan etki, zamanlayici ve DOM yok.
export function indirge(durum, olay, secenekler = SECENEKLER) {
  const ac = () => {
    const seciliDizin = secenekler.findIndex((o) => o.deger === durum.secili);
    return {
      ...durum, acik: true, arabellek: "",
      etkin: secilebilir(secenekler, seciliDizin) ? seciliDizin : uc(secenekler, true),
    };
  };
  const kapat = () => ({ ...durum, acik: false, etkin: -1, arabellek: "" });

  switch (olay.t) {
    case "ac": return durum.acik ? durum : ac();
    case "kapat": return kapat();
    case "isaretle":
      return durum.acik && secilebilir(secenekler, olay.dizin) ? { ...durum, etkin: olay.dizin } : durum;
    case "tus":
      switch (olay.tus) {
        case "ArrowDown": return durum.acik ? { ...durum, etkin: ilerle(secenekler, durum.etkin, +1) } : ac();
        case "ArrowUp": return durum.acik ? { ...durum, etkin: ilerle(secenekler, durum.etkin, -1) } : ac();
        case "Home": return durum.acik ? { ...durum, etkin: uc(secenekler, true) } : durum;
        case "End": return durum.acik ? { ...durum, etkin: uc(secenekler, false) } : durum;
        case "Escape": return kapat();
        case "Enter": case " ":
          if (!durum.acik) return ac();
          if (!secilebilir(secenekler, durum.etkin)) return durum;
          return { ...kapat(), secili: secenekler[durum.etkin].deger };
        default: return durum;
      }
    case "harf": {
      const yeni = olay.zaman - durum.sonHarfZamani > 500 ? olay.harf : durum.arabellek + olay.harf;
      const kucuk = (m) => m.toLocaleLowerCase("tr");
      const bulunan = secenekler.findIndex((o, i) => secilebilir(secenekler, i) && kucuk(o.metin).startsWith(kucuk(yeni)));
      return {
        ...durum, acik: true, arabellek: yeni, sonHarfZamani: olay.zaman,
        etkin: bulunan === -1 ? durum.etkin : bulunan,
      };
    }
    default: return durum;
  }
}

// Durumdan turetilen erisilebilirlik oznitelikleri: yine gorunum degil, sozlesme.
export const aria = (durum, kok, secenekler = SECENEKLER) => ({
  tetikleyici: {
    "aria-haspopup": "listbox",
    "aria-expanded": String(durum.acik),
    "aria-controls": `${kok}-liste`,
  },
  liste: {
    id: `${kok}-liste`,
    role: "listbox",
    tabindex: -1,
    "aria-activedescendant": durum.acik && durum.etkin >= 0 ? `${kok}-secenek-${durum.etkin}` : "",
  },
  secenek: (i) => ({
    id: `${kok}-secenek-${i}`,
    role: "option",
    "aria-selected": String(secenekler[i].deger === durum.secili),
    ...(secenekler[i].pasif ? { "aria-disabled": "true" } : {}),
  }),
});
```

Durumda beş alan var ve hiçbiri görünüme ait değil: liste açık mı, hangi öğe etkin, hangi
değer seçili, harfle arama arabelleği ne ve son harf ne zaman girildi. Zaman bile bir
alandır — indirgeyici saati okumaz, olayın içinde alır. Bu, sınamayı zamandan bağımsız kılar.

Erişilebilirlik öznitelikleri durumun bir türevi olarak yazılır. Etkin öğe odağı almaz;
liste odaklı kalır ve etkin öğenin kimliği `aria-activedescendant` ile bildirilir. Tarayıcı
ve Web Platformu kursundaki dönen sekme durağı düzeninin bir başka biçimi budur: klavye
sırasında tek durak, listenin içinde ok tuşlarıyla gezinme.

## Aynı Davranış, İki Görünüm

Aşağıdaki dosya, önceki bloktaki `bassiz.mjs` ile aynı dizine konur.

```js
// bassiz-izle.mjs — ayni durum makinesi, iki ayri gorunum
import { SECENEKLER, baslangic, indirge, aria } from "./bassiz.mjs";

const dizi = [
  { t: "tus", tus: "ArrowDown" },
  { t: "tus", tus: "ArrowDown" },
  { t: "tus", tus: "ArrowDown" },
  { t: "harf", harf: "z", zaman: 1000 },
  { t: "tus", tus: "Home" },
  { t: "tus", tus: "ArrowUp" },
  { t: "tus", tus: "Enter" },
  { t: "tus", tus: "ArrowDown" },
  { t: "tus", tus: "Escape" },
];

let durum = baslangic();
console.log("olay                 açık  etkin  etkin metin   seçili  arabellek");
console.log(`${"(başlangıç)".padEnd(20)} ${String(durum.acik).padEnd(5)} ${String(durum.etkin).padStart(5)}  ${"-".padEnd(12)} ${String(durum.secili).padEnd(7)} "${durum.arabellek}"`);
for (const olay of dizi) {
  durum = indirge(durum, olay);
  const ad = olay.t === "tus" ? `tuş ${olay.tus}` : olay.t === "harf" ? `harf "${olay.harf}"` : olay.t;
  const etkinMetin = durum.etkin >= 0 ? SECENEKLER[durum.etkin].metin : "-";
  console.log(`${ad.padEnd(20)} ${String(durum.acik).padEnd(5)} ${String(durum.etkin).padStart(5)}  ${etkinMetin.padEnd(12)} ${String(durum.secili).padEnd(7)} "${durum.arabellek}"`);
}

// --- Ayni durum, iki gorunum ---
const oge = (ad, ozn = {}, ...cocuklar) => ({ ad, ozn, cocuklar: cocuklar.flat().filter(Boolean) });
const metin = (d) => ({ ad: "#metin", ozn: {}, cocuklar: [], metin: d });
const yaz = (d, g = 0) => {
  if (d.metin !== undefined) return [`${"  ".repeat(g)}"${d.metin}"`];
  const o = Object.entries(d.ozn).filter(([, v]) => v !== "").map(([a, v]) => ` ${a}="${v}"`).join("");
  return [`${"  ".repeat(g)}<${d.ad}${o}>`, ...d.cocuklar.flatMap((c) => yaz(c, g + 1))];
};

// Gorunum A: dugmeye bagli acilir liste.
const acilirListe = (durum, kok) => {
  const a = aria(durum, kok);
  const secili = SECENEKLER.find((o) => o.deger === durum.secili);
  return oge("div", { class: "acilir" },
    oge("button", { type: "button", ...a.tetikleyici }, metin(secili?.metin ?? "İstasyon seçin")),
    durum.acik && oge("ul", { ...a.liste, class: "acilir-liste" },
      ...SECENEKLER.map((s, i) => oge("li", { ...a.secenek(i) }, metin(s.metin)))));
};

// Gorunum B: yan panelde her zaman acik liste.
const yanPanel = (durum, kok) => {
  const a = aria({ ...durum, acik: true }, kok);
  return oge("nav", { class: "yan-panel" },
    oge("h2", { id: `${kok}-baslik` }, metin("İstasyon")),
    oge("ul", { ...a.liste, "aria-labelledby": `${kok}-baslik`, class: "panel-liste" },
      ...SECENEKLER.map((s, i) => oge("li", { ...a.secenek(i) }, metin(s.metin)))));
};

let d2 = baslangic();
for (const olay of [{ t: "ac" }, { t: "tus", tus: "ArrowDown" }]) d2 = indirge(d2, olay);

console.log("\ngörünüm A (açılır liste), durum: açık, etkin =", d2.etkin);
console.log(yaz(acilirListe(d2, "istasyon")).join("\n"));

// Iki gorunumu karsilastir: rol ve durum oznitelikleri ayni, oge adlari ve siniflar farkli.
const dolas = (d, c = []) => { if (d.metin === undefined) { c.push(d); d.cocuklar.forEach((x) => dolas(x, c)); } return c; };
const imza = (kok, on) => dolas(kok).map((d) => {
  const rol = d.ozn.role ?? d.ad;
  const durumlar = Object.entries(d.ozn)
    .filter(([a]) => a.startsWith("aria-"))
    .map(([a, v]) => `${a}=${String(v).replace(on, "*")}`).sort().join(",");
  return `${rol}${durumlar ? "[" + durumlar + "]" : ""}`;
});
const a = imza(acilirListe(d2, "istasyon"), /istasyon/g);
const b = imza(yanPanel(d2, "istasyon-yan"), /istasyon-yan/g);
console.log("\ngörünüm A rol/durum imzası:", JSON.stringify(a));
console.log("görünüm B rol/durum imzası:", JSON.stringify(b));
console.log("öge adları:", JSON.stringify(dolas(acilirListe(d2, "istasyon")).map((d) => d.ad)),
  "/", JSON.stringify(dolas(yanPanel(d2, "istasyon-yan")).map((d) => d.ad)));
```

```
olay                 açık  etkin  etkin metin   seçili  arabellek
(başlangıç)          false    -1  -            null    ""
tuş ArrowDown        true      0  Üst yamaç    null    ""
tuş ArrowDown        true      1  Orta teras   null    ""
tuş ArrowDown        true      3  Zirve        null    ""
harf "z"             true      3  Zirve        null    "z"
tuş Home             true      0  Üst yamaç    null    "z"
tuş ArrowUp          true      3  Zirve        null    "z"
tuş Enter            false    -1  -            zirve   ""
tuş ArrowDown        true      3  Zirve        zirve   ""
tuş Escape           false    -1  -            zirve   ""

görünüm A (açılır liste), durum: açık, etkin = 1
<div class="acilir">
  <button type="button" aria-haspopup="listbox" aria-expanded="true" aria-controls="istasyon-liste">
    "İstasyon seçin"
  <ul id="istasyon-liste" role="listbox" tabindex="-1" aria-activedescendant="istasyon-secenek-1" class="acilir-liste">
    <li id="istasyon-secenek-0" role="option" aria-selected="false">
      "Üst yamaç"
    <li id="istasyon-secenek-1" role="option" aria-selected="false">
      "Orta teras"
    <li id="istasyon-secenek-2" role="option" aria-selected="false" aria-disabled="true">
      "Alt yamaç"
    <li id="istasyon-secenek-3" role="option" aria-selected="false">
      "Zirve"

görünüm A rol/durum imzası: ["div","button[aria-controls=*-liste,aria-expanded=true,aria-haspopup=listbox]","listbox[aria-activedescendant=*-secenek-1]","option[aria-selected=false]","option[aria-selected=false]","option[aria-disabled=true,aria-selected=false]","option[aria-selected=false]"]
görünüm B rol/durum imzası: ["nav","h2","listbox[aria-activedescendant=*-secenek-1,aria-labelledby=*-baslik]","option[aria-selected=false]","option[aria-selected=false]","option[aria-disabled=true,aria-selected=false]","option[aria-selected=false]"]
öge adları: ["div","button","ul","li","li","li","li"] / ["nav","h2","ul","li","li","li","li"]
```

İzleme tablosu davranışın tamamını on satırda gösteriyor. Üçüncü ok tuşunda etkin dizin
1'den 3'e atlıyor: kapalı öğe atlanmış. `Home` tuşu başa gidiyor, oradan yukarı ok yine
kapalı öğeyi atlayarak sona sarıyor. `Enter` seçimi yapıp listeyi kapatıyor; ardından gelen
ok tuşu listeyi seçili öğe etkin olacak biçimde açıyor. Son satırda `Escape` listeyi
kapatıyor ama seçimi bozmuyor.

İki görünümün rol imzaları öğe düzeyinde birebir aynı: dört seçenek, aynı seçim durumları,
aynı kapalılık bildirimi, aynı etkin öğe. Ayrım yalnızca kapsayan yapıda — biri düğmeli bir
açılır liste, diğeri başlığa bağlanmış bir yan panel. Öge adları da farklı. Davranış aynı
kaldığı hâlde görünüm bağımsız değişebiliyor.

## Davranışın Görünümsüz Sınanması

Başsız bileşenin en somut kazancı burada görünür: davranış, hiçbir görünüm kurulmadan
sınanır.

```js
// bassiz.test.mjs — davranisin sinanmasi; hicbir gorunum, hicbir DOM kurulmuyor
import { test } from "node:test";
import { strictEqual, deepStrictEqual } from "node:assert";
import { SECENEKLER, baslangic, indirge, aria } from "./bassiz.mjs";

const oynat = (olaylar, durum = baslangic()) => olaylar.reduce((d, o) => indirge(d, o), durum);
const tus = (t) => ({ t: "tus", tus: t });

test("ok tuşu pasif seçeneği atlar", () => {
  const d = oynat([tus("ArrowDown"), tus("ArrowDown"), tus("ArrowDown")]);
  strictEqual(SECENEKLER[d.etkin].deger, "zirve");
});

test("son seçenekten sonra başa döner, başta yukarı sona gider", () => {
  const son = oynat([tus("End")], { ...baslangic(), acik: true });
  strictEqual(son.etkin, 3);
  strictEqual(oynat([tus("ArrowDown")], son).etkin, 0);
  strictEqual(oynat([tus("ArrowUp")], { ...son, etkin: 0 }).etkin, 3);
});

test("Enter seçer ve kapatır, Escape seçimi değiştirmez", () => {
  const secilmis = oynat([tus("ArrowDown"), tus("ArrowDown"), tus("Enter")]);
  strictEqual(secilmis.secili, "orta");
  strictEqual(secilmis.acik, false);
  const kacilmis = oynat([tus("ArrowDown"), tus("Escape")], secilmis);
  strictEqual(kacilmis.secili, "orta");
  strictEqual(kacilmis.acik, false);
});

test("açılışta seçili olan seçenek etkin olur", () => {
  strictEqual(oynat([{ t: "ac" }], baslangic("zirve")).etkin, 3);
  strictEqual(oynat([{ t: "ac" }], baslangic(null)).etkin, 0);
});

test("harf arabelleği 500 ms içinde birikir, sonra sıfırlanır", () => {
  const a = oynat([{ t: "harf", harf: "o", zaman: 1000 }]);
  strictEqual(SECENEKLER[a.etkin].deger, "orta");
  const b = indirge(a, { t: "harf", harf: "r", zaman: 1200 });   // "or" -> Orta teras
  strictEqual(b.arabellek, "or");
  const c = indirge(b, { t: "harf", harf: "z", zaman: 2000 });   // arabellek sifirlandi
  strictEqual(c.arabellek, "z");
  strictEqual(SECENEKLER[c.etkin].deger, "zirve");
});

test("pasif seçenek işaretlenemez ve seçilemez", () => {
  const acik = oynat([{ t: "ac" }]);
  strictEqual(indirge(acik, { t: "isaretle", dizin: 2 }).etkin, acik.etkin);
  const zorlanmis = { ...acik, etkin: 2 };
  strictEqual(indirge(zorlanmis, tus("Enter")).secili, null);
});

test("indirgeyici verilen durumu değiştirmez", () => {
  const once = oynat([{ t: "ac" }]);
  const kopya = structuredClone(once);
  indirge(once, tus("ArrowDown"));
  deepStrictEqual(once, kopya);
});

test("etkin betimleyici yalnızca liste açıkken bildirilir", () => {
  const kapali = baslangic("orta");
  strictEqual(aria(kapali, "k").liste["aria-activedescendant"], "");
  strictEqual(aria(kapali, "k").tetikleyici["aria-expanded"], "false");
  const acik = indirge(kapali, { t: "ac" });
  strictEqual(aria(acik, "k").liste["aria-activedescendant"], "k-secenek-1");
  strictEqual(aria(acik, "k").secenek(1)["aria-selected"], "true");
  strictEqual(aria(acik, "k").secenek(2)["aria-disabled"], "true");
});
```

Dosya `node --test bassiz.test.mjs` ile çalıştırılır. Aşağıdaki çıktıda süre değerleri
makineye göre değişir; sınama sonuçları değişmez.

```
✔ ok tuşu pasif seçeneği atlar (0.43125ms)
✔ son seçenekten sonra başa döner, başta yukarı sona gider (0.064375ms)
✔ Enter seçer ve kapatır, Escape seçimi değiştirmez (0.05875ms)
✔ açılışta seçili olan seçenek etkin olur (0.349ms)
✔ harf arabelleği 500 ms içinde birikir, sonra sıfırlanır (0.123ms)
✔ pasif seçenek işaretlenemez ve seçilemez (0.059625ms)
✔ indirgeyici verilen durumu değiştirmez (0.343333ms)
✔ etkin betimleyici yalnızca liste açıkken bildirilir (0.093542ms)
ℹ tests 8
ℹ suites 0
ℹ pass 8
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 40.331
```

Sekiz sınama, hiçbir tarayıcı ve hiçbir sahte DOM olmadan çalışıyor. Harfle arama sınaması
zamanı bir sayı olarak veriyor; gerçek bir zamanlayıcı beklenmediği için sınama
milisaniyeler yerine mikrosaniyeler sürüyor. Yedinci sınama durumun değiştirilemezliğini
denetliyor: indirgeyici verilen nesneyi değiştirirse, çerçevenin değişikliği saptama
düzeneği de bozulur.

## Başsız Bileşenin Sınırları

Başsız yaklaşımın bedeli, görünümü kuran tarafın işinin artmasıdır. Öznitelikleri
işaretlemeye takmak, olayları doğru öğeye bağlamak ve etkin öğeyi görünür alana kaydırmak
çağıranın sorumluluğuna geçer. Bu iş her kullanımda tekrar edilir ve bir yerde eksik
yapılır.

Sınır şuradan geçer: davranış birden çok görünümle kullanılacaksa başsız yazmak
karşılığını verir; tek bir görünümle kullanılacaksa aynı davranışı bir bileşenin içinde
tutmak daha az yüzey demektir. Ara bir yol, başsız çekirdeğin üzerine varsayılan bir görünüm
koymaktır: yaygın durumda hazır bileşen kullanılır, gerektiğinde çekirdeğe inilir.

İkinci sınır, bazı davranışların saf kalamamasıdır. Etkin öğenin görünür alana kaydırılması,
açılır listenin ekran kenarına göre konumlandırılması ve odağın liste kapanınca tetikleyiciye
dönmesi ölçüm ve DOM erişimi gerektirir. Bunlar durum makinesinin dışında, ayrı ve açıkça
işaretlenmiş bir katmanda durur; makinenin saflığı korunur.

## Özet

- Başsız bileşen davranışı ve erişilebilirlik sözleşmesini taşır, görünüm üretmez; dışarıya
  okunacak bir durum ve işaretlemeye takılacak öznitelikler verir.
- Davranışın taşıyıcısı saf bir indirgeyicidir; zaman bile olayın içinde verilir, böylece
  sınama zamandan bağımsız kalır.
- Erişilebilirlik öznitelikleri durumun türevidir; etkin öğe odağı almaz, kimliği
  `aria-activedescendant` ile bildirilir.
- Aynı durumdan iki farklı görünüm üretildiğinde rol ve durum imzaları aynı kalır, öge
  adları ve kapsayan yapı değişir.
- Davranış hiçbir görünüm kurulmadan olay dizileriyle sınanır; indirgeyicinin durumu
  değiştirmemesi de sınanacak bir özelliktir.
- Ölçüm ve odak taşıma gerektiren davranışlar makinenin dışında, ayrı bir katmanda kalır;
  başsız yaklaşım tek görünümlü kullanımda karşılığını vermez.

## Sonraki Adım

Bu konuda kurulan her şey bileşenin dış yüzeyine dayanıyordu: hangi özellikleri aldığı,
hangi değerleri verdiği, hangi deliği bıraktığı. Bu yüzeyin kendisi henüz tasarlanmadı.
Çağıran bileşene tanımadığı bir öznitelik verdiğinde ne olacak? Bileşenin kendi sınıf adı ile
çağıranın verdiği sınıf adı çakıştığında hangisi kazanacak? Bileşen bir olayı hem kendisi
dinleyip hem çağıranın dinleyicisini çağırabilecek mi? Değerin sahibi bileşen mi, çağıran
mı? Sonraki ders bu soruların yanıtını özellik yayılması, varsayılan birleştirmesi ve
denetim sahipliği başlıkları altında toplar.
