Ders 07 / 23
Duyarlı Tasarım İlkesi
Duyarlılığın üç bileşeni, kesme noktalarının içerikten hesapla türetilmesi, iç boyutlandırmayla kendiliğinden çözülen eşikler ve şerit tanımının eşiği nasıl kaydırdığı.
İçindekiler
Çok sütunlu düzen dersinde bir karar sessizce verildi: sütun genişliği kapsayıcıya sığmadığında sütun sayısı düşürüldü. Eşik, bir ekran genişliği listesinden değil, metnin okunabilir kaldığı en dar ölçüden geldi.
Bu ders aynı akıl yürütmeyi sayfanın bütününe taşır. Bir düzenin hangi genişlikte değişmesi gerektiği hesaplanabilir bir sorudur; yanıtı aygıt adlarında değil, içeriğin kendi ölçülerindedir.
Duyarlılığın Üç Bileşeni
Duyarlı tasarım (responsive design) tek bir bildirimin adı değildir; üç davranışın birlikte bulunmasıdır.
Akışkan ızgara (fluid grid). Düzenin ölçüleri sabit uzunluklarla değil, oranlarla ve
kalan alanla verilir. fr, yüzde, minmax() ve auto bu işi görür; sabit piksel genişlikler
yalnız gerçekten sabit olması gereken yerlerde kalır.
Esnek medya. Görsel ve gömülü içerik kapsayıcısını aşmaz; ölçüsü kendi doğal boyutu değil, kapsayıcının bıraktığı yerdir.
Koşullu biçem. Yukarıdaki ikisi tek başına yetmediğinde — düzenin yapısı değişmek zorunda olduğunda — biçem bir koşula bağlanır.
Sıralama önemlidir. İlk iki bileşen ne kadar iş görürse, üçüncüsüne o kadar az iş kalır. Sorguyla yazılan her koşul, biçem dosyasında bakımı gereken bir daldır; iç boyutlandırmayla çözülen her eşik ise dosyada hiç görünmez.
Kesme Noktası Bir Ölçüdür
Kesme noktası (breakpoint), düzenin yapısının değiştiği genişliktir. Sorunun doğru sorulması şöyledir: bu bileşen, anlamını yitirmeden ne kadar daralabilir? Yanıt bir sayıdır ve bileşenin içeriğinden gelir.
Aşağıdaki program, ölçüm istasyonu sayfasının üç bileşeni için bu sayıyı hesaplıyor.
// kesme.mjs — kesme noktalarinin icerikten turetilmesi // Sayfanin yatay ic bosluklari her iki yanda 24px. const IC_BOSLUK = 24 * 2; // 1) Metin sutunu: govde yazisi 18px, ortalama karakter genisligi 0.5em -> 9px/karakter const KARAKTER = 9; const OLCU_ALT = 45; // okunabilir en az karakter const OLCU_UST = 75; // okunabilir en cok karakter const metinAlt = OLCU_ALT * KARAKTER; const metinUst = OLCU_UST * KARAKTER; // 2) Olcum kartlari: bir kartin anlamli kaldigi en dar olcu ve aralik const KART = 200; const KART_ARALIK = 16; const kartEsik = (n) => n * (KART + KART_ARALIK) - KART_ARALIK; // 3) Sayfa duzeni: ana bolum ve yan bilgi yan yana durabilmek icin kendi en dar olculeri const ANA_EN_DAR = 480; const YAN_EN_DAR = 260; const SAYFA_ARALIK = 24; const ikiSutunEsik = ANA_EN_DAR + YAN_EN_DAR + SAYFA_ARALIK; console.log("--- 1) metin sutunu ---"); console.log(`govde yazisi 18px, 1 karakter ~ ${KARAKTER}px`); console.log(`olcu ${OLCU_ALT}-${OLCU_UST} karakter -> sutun ${metinAlt}-${metinUst}px`); console.log(`gorus alani karsiligi: ${metinAlt + IC_BOSLUK}-${metinUst + IC_BOSLUK}px`); console.log(`ust sinir asildiginda satir uzar: esik ${metinUst + IC_BOSLUK}px`); console.log("\n--- 2) olcum kartlari ---"); console.log(`kart en dar ${KART}px, aralik ${KART_ARALIK}px`); console.log("sutun gereken icerik alani gorus alani esigi"); for (let n = 1; n <= 5; n++) { console.log( `${String(n).padStart(5)} ${String(kartEsik(n)).padStart(20)} ${String(kartEsik(n) + IC_BOSLUK).padStart(18)}`, ); } console.log("\n--- 3) sayfa duzeni ---"); console.log(`ana bolum en dar ${ANA_EN_DAR}px + yan bilgi en dar ${YAN_EN_DAR}px + aralik ${SAYFA_ARALIK}px`); console.log(`iki sutunun mumkun oldugu en dar icerik alani: ${ikiSutunEsik}px`); console.log(`gorus alani esigi (alt sinir): ${ikiSutunEsik + IC_BOSLUK}px`); console.log("\n--- turetilen esiklerin tamami ---"); const esikler = [ { ad: "kart 2 sutun", px: kartEsik(2) + IC_BOSLUK, sorgu: false }, { ad: "kart 3 sutun", px: kartEsik(3) + IC_BOSLUK, sorgu: false }, { ad: "metin ust siniri", px: metinUst + IC_BOSLUK, sorgu: false }, { ad: "sayfa iki sutun*", px: ikiSutunEsik + IC_BOSLUK, sorgu: true }, { ad: "kart 4 sutun", px: kartEsik(4) + IC_BOSLUK, sorgu: false }, { ad: "kart 5 sutun", px: kartEsik(5) + IC_BOSLUK, sorgu: false }, ]; esikler.sort((a, b) => a.px - b.px); console.log("esik(px) bilesen sorgu gerekiyor mu"); for (const e of esikler) { console.log( `${String(e.px).padStart(8)} ${e.ad.padEnd(20)} ${e.sorgu ? "evet" : "hayir (ic boyutlandirma yeter)"}`, ); } const yazilacak = esikler.filter((e) => e.sorgu); console.log(`\ntoplam esik: ${esikler.length}, yazilacak sorgu: ${yazilacak.length} (${yazilacak.map((e) => e.px + "px").join(", ")})`); console.log("* en dar olculerin toplamindan gelen alt sinir; serit tanimina gore yukari cikar");
--- 1) metin sutunu ---
govde yazisi 18px, 1 karakter ~ 9px
olcu 45-75 karakter -> sutun 405-675px
gorus alani karsiligi: 453-723px
ust sinir asildiginda satir uzar: esik 723px
--- 2) olcum kartlari ---
kart en dar 200px, aralik 16px
sutun gereken icerik alani gorus alani esigi
1 200 248
2 416 464
3 632 680
4 848 896
5 1064 1112
--- 3) sayfa duzeni ---
ana bolum en dar 480px + yan bilgi en dar 260px + aralik 24px
iki sutunun mumkun oldugu en dar icerik alani: 764px
gorus alani esigi (alt sinir): 812px
--- turetilen esiklerin tamami ---
esik(px) bilesen sorgu gerekiyor mu
464 kart 2 sutun hayir (ic boyutlandirma yeter)
680 kart 3 sutun hayir (ic boyutlandirma yeter)
723 metin ust siniri hayir (ic boyutlandirma yeter)
812 sayfa iki sutun* evet
896 kart 4 sutun hayir (ic boyutlandirma yeter)
1112 kart 5 sutun hayir (ic boyutlandirma yeter)
toplam esik: 6, yazilacak sorgu: 1 (812px)
* en dar olculerin toplamindan gelen alt sinir; serit tanimina gore yukari cikar
Üç bileşen, üç ayrı ölçü kaynağı.
Metin sütununun ölçüsü tipografiden gelir. Bir satırda 45 karakterin altına inildiğinde göz çok sık satır atlar, 75 karakterin üstüne çıkıldığında satır sonundan sonrakinin başına dönmek zorlaşır. On sekiz taban ölçüsündeki bir gövde yazısında karakter genişliği yaklaşık dokuz birimdir; iki sınır 405 ve 675 birime karşılık gelir.
Kartların ölçüsü içeriğin kendisinden gelir: bir ölçüm kartında başlık, değer ve birim yan yana okunabilmelidir ve bu 200 birimin altında bozulur. Kaç sütun sığdığı buradan çıkar.
Sayfa düzeninin ölçüsü iki bölümün ölçülerinin toplamıdır. Yan yana durabilmeleri için gereken en dar alan, ikisinin en dar ölçüsü ile aralarındaki boşluğun toplamıdır.
Elde edilen liste altı eşik içeriyor ve hiçbiri bir aygıt adıyla anılmıyor. Sayılar tuhaf: 464, 680, 723, 812. Bu tuhaflık bir kusur değil, ölçünün gerçekten içerikten geldiğinin işaretidir.
Yazılmayan Eşikler
Altı eşikten yalnız biri koşullu biçem gerektiriyor. Kalan beşi, önceki derslerde kurulan iç boyutlandırma (intrinsic sizing) araçlarıyla kendiliğinden karşılanır.
Kart ızgarası repeat(auto-fill, minmax(200px, 1fr)) yazımıyla tanımlıdır. Sütun sayısı
kapsayıcı genişliğinden hesaplanır ve 464, 680, 896, 1112 eşikleri hesabın doğal sonucu olarak
geçilir. Bu eşikleri sorguyla yazmak aynı hesabı ikinci kez, elle yapmak olurdu.
Metin sütununun üst sınırı max-inline-size ile verilir; ölçü 675 birimi aşmaz, kalan alan dış
boşluğa gider. Bunun için de koşul gerekmez.
Geriye sayfa düzeninin kendisi kalır. grid-template-areas bildirimi bir yapıdır, bir ölçü
değil; iki sütunlu yerleşimden tek sütunluya geçmek bildirimin başka bir bildirimle
değiştirilmesini gerektirir. Iç boyutlandırma bir ölçüyü hesaplayabilir, bir yapıyı
seçemez.
Ölçüt buradan çıkar: ölçü değişiyorsa iç boyutlandırma, yapı değişiyorsa koşullu biçem.
Şerit Tanımı Eşiği Kaydırır
Yukarıdaki 812 sayısı en dar ölçülerin toplamıdır ve bir alt sınırdır. Gerçek eşik, şeritlerin nasıl tanımlandığına bağlıdır.
// liste.mjs — esigin nereden geldigi: serit tanimina gore turetme const IC_BOSLUK = 48; // sayfanin iki yanindaki ic bosluk toplami const ARALIK = 24; // iki sutun arasindaki aralik const ANA_EN_DAR = 480; // ana bolumun anlamli kaldigi en dar olcu const YAN_EN_DAR = 260; // yan bilginin anlamli kaldigi en dar olcu // minmax(alt, Nfr) seritlerinin olculmesi: pay alt sinirin altinda kalirsa // serit alt sinirda dondurulur, kalan alan yeniden bolusturulur. function seritler(gorusAlani, tanim) { let kalan = gorusAlani - IC_BOSLUK - ARALIK; const sonuc = tanim.map((t) => ({ ...t, olcu: null })); let degisti = true; while (degisti) { degisti = false; const acik = sonuc.filter((t) => t.olcu === null); const frToplam = acik.reduce((s, t) => s + t.fr, 0); for (const t of acik) { const pay = (kalan * t.fr) / frToplam; if (pay < t.alt) { t.olcu = t.alt; kalan -= t.alt; degisti = true; break; } } if (!degisti) for (const t of acik) t.olcu = (kalan * t.fr) / frToplam; } return sonuc; } const tanimlar = [ ["minmax(0, 2fr) minmax(0, 1fr)", [{ ad: "ana", fr: 2, alt: 0 }, { ad: "yan", fr: 1, alt: 0 }]], ["minmax(480px, 2fr) minmax(260px, 1fr)", [{ ad: "ana", fr: 2, alt: ANA_EN_DAR }, { ad: "yan", fr: 1, alt: YAN_EN_DAR }]], ]; for (const [ad, tanim] of tanimlar) { console.log(`--- ${ad} ---`); console.log("gorus alani ana sutun yan sutun ana yeterli yan yeterli toplam tasma"); for (const W of [768, 812, 852, 900, 1024]) { const s = seritler(W, tanim); const ana = s[0].olcu, yan = s[1].olcu; const tasma = ana + yan + ARALIK - (W - IC_BOSLUK); console.log( `${String(W).padStart(11)} ${ana.toFixed(1).padStart(9)} ${yan.toFixed(1).padStart(9)} ` + `${(ana >= ANA_EN_DAR ? "evet" : "HAYIR").padStart(11)} ` + `${(yan >= YAN_EN_DAR ? "evet" : "HAYIR").padStart(11)} ${tasma.toFixed(1).padStart(12)}`, ); } // her iki serit de en dar olcusunu tasmasiz karsilayan ilk genislik let esik = 300; while (esik < 2000) { const s = seritler(esik, tanim); const tasma = s[0].olcu + s[1].olcu + ARALIK - (esik - IC_BOSLUK); if (s[0].olcu >= ANA_EN_DAR && s[1].olcu >= YAN_EN_DAR && tasma <= 0.001) break; esik += 1; } console.log(`turetilen kesme noktasi: ${esik}px\n`); } console.log("--- aygit adlarindan gelen bir esik ile karsilastirma ---"); const HAZIR = 768; for (const [ad, tanim] of tanimlar) { const s = seritler(HAZIR, tanim); const tasma = s[0].olcu + s[1].olcu + ARALIK - (HAZIR - IC_BOSLUK); const dar = s[0].olcu < ANA_EN_DAR || s[1].olcu < YAN_EN_DAR; const tani = dar ? "seritler en dar olcunun altinda" : `seritler yeterli ama ${tasma.toFixed(0)}px tasma var`; console.log(`${HAZIR}px, ${ad}`); console.log(` ana ${s[0].olcu.toFixed(1)} yan ${s[1].olcu.toFixed(1)} -> ${tani}`); }
--- minmax(0, 2fr) minmax(0, 1fr) ---
gorus alani ana sutun yan sutun ana yeterli yan yeterli toplam tasma
768 464.0 232.0 HAYIR HAYIR 0.0
812 493.3 246.7 evet HAYIR 0.0
852 520.0 260.0 evet evet 0.0
900 552.0 276.0 evet evet 0.0
1024 634.7 317.3 evet evet 0.0
turetilen kesme noktasi: 852px
--- minmax(480px, 2fr) minmax(260px, 1fr) ---
gorus alani ana sutun yan sutun ana yeterli yan yeterli toplam tasma
768 480.0 260.0 evet evet 44.0
812 480.0 260.0 evet evet 0.0
852 520.0 260.0 evet evet 0.0
900 552.0 276.0 evet evet 0.0
1024 634.7 317.3 evet evet 0.0
turetilen kesme noktasi: 812px
--- aygit adlarindan gelen bir esik ile karsilastirma ---
768px, minmax(0, 2fr) minmax(0, 1fr)
ana 464.0 yan 232.0 -> seritler en dar olcunun altinda
768px, minmax(480px, 2fr) minmax(260px, 1fr)
ana 480.0 yan 260.0 -> seritler yeterli ama 44px tasma var
İki tanım, iki farklı eşik.
Birinci tanımda oran kilitlidir: yan bilgi her genişlikte kalanın üçte birini alır. Yan bilginin 260 birime ulaşması için üçte birin 260 olması gerekir; eşik 852 birime çıkar. Alt sınırların toplamı olan 812 burada yetmez, çünkü ana bölüm ihtiyacından fazlasını almaktadır.
İkinci tanımda alt sınırlar şeride yazılıdır. Şeritler önce en dar ölçülerine oturur, artan alan orana göre bölüşülür. Eşik alt sınırların toplamına, yani 812 birime iner. Bu tanım sayfaya 40 birim genişlik kazandırır ve kesme noktasını hesabın ilk sonucuyla aynı yere getirir.
Son blok, eşiğin dışarıdan seçilmesinin iki ayrı biçimde nasıl kırıldığını gösteriyor. Aygıt adlarından gelen 768 birimlik eşikte birinci tanımda şeritler en dar ölçünün altında kalır: düzen uygulanır ama içerik sıkışır. İkinci tanımda şeritler alt sınırlarını korur, bu kez toplamları kapsayıcıyı 44 birim aşar: yatay taşma oluşur.
İki sonuç da aynı nedenden doğar. Eşik içerikten türetilmediğinde, ölçüler ile eşik arasındaki bağ kopar ve hata ya sıkışma ya taşma olarak görünür.
Dar Taraftan Başlamak
Kesme noktası bulunduktan sonra bir yazım kararı kalır: taban biçem hangi düzeni tarif etsin?
Taban biçem dar düzeni tarif ediyorsa, koşul genişledikçe eklenir. Tek sütunlu yerleşim zaten yazılıdır; koşul yalnız ikinci sütunu kurar.
Taban biçem geniş düzeni tarif ediyorsa, koşul daraldıkça eklenir ve her koşulun içinde tabanın kurduğu şeyi geri alan bildirimler bulunur: alan adlarını sıfırlamak, şerit sayısını bire indirmek, dış boşlukları küçültmek.
Dar taraftan başlamak, geri alma bildirimlerini ortadan kaldırır. İkinci bir gerekçe daha vardır: içerik dar bir alanda önceliklendirilmek zorundadır. Hangi bölümün önce geleceği, hangi bilginin taşınabilir olduğu dar düzende karara bağlanır; geniş düzen bu kararların üzerine kurulur. Ters yönde çalışıldığında dar düzen, geniş düzenden arta kalan olur.
/* duzen.css — 7. adım: taban dar düzen, tek eşik */ .istasyon-duzeni { display: grid; grid-template-columns: minmax(0, 1fr); grid-template-areas: "kunye" "olcumler" "yan-bilgi" "konum"; gap: var(--aralik-1); } .olcum-kartlari { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: var(--aralik-0); } .istasyon-notlari { max-inline-size: 675px; }
Taban biçemde tek bir sorgu yoktur. Kart ızgarası genişlikle birlikte sütun kazanır, metin sütunu üst sınırında durur, sayfa tek sütunludur. Yalnız 812 birimlik eşikte yapı değişir; o bildirim sonraki derste yazılacaktır.
Görüntü Alanı Bildirimi
Bir ön koşul, bütün bu hesabın geçerli olması için gereklidir. Küçük ekranlı aygıtlarda tarayıcı, dar ekrana göre yazılmamış sayfaları okunur kılmak için geniş bir sanal görüntü alanı varsayıp sonucu küçülterek gösterebilir. Bu durumda CSS’in gördüğü genişlik ekranın gerçek genişliği olmaz ve türetilen eşikler hiç tetiklenmez.
Web’in Temelleri ve HTML kursunda tanıtılan görüntü alanı meta etiketi bu varsayımı kapatır:
<meta name="viewport" content="width=device-width, initial-scale=1">
Bildirim, CSS’in gördüğü genişliği aygıtın kendi genişliğine eşitler. Duyarlı biçemin ilk satırı biçem dosyasında değil, belgenin baş bölümündedir.
Özet
- Duyarlılık üç bileşenin birlikte bulunmasıdır: akışkan ölçüler, kapsayıcısını aşmayan medya ve gerektiğinde koşullu biçem.
- Kesme noktası bir aygıt genişliği değil, bileşenin anlamını yitirmeden inebildiği en dar ölçüden türetilen bir sayıdır; türetilen sayılar yuvarlak olmaz.
- Ölçü değişiyorsa iç boyutlandırma yeter; yapı değişiyorsa koşullu biçem gerekir. Istasyon sayfasında altı eşikten yalnız biri sorguya dönüşür.
- Şerit tanımı eşiği kaydırır: oran kilitli tanımda eşik 852, alt sınırları yazılı tanımda 812 birimdir.
- Eşik dışarıdan seçildiğinde hata iki biçimde görünür — şeritler en dar ölçünün altına iner ya da toplamları kapsayıcıyı aşarak taşma üretir.
- Taban biçem dar düzeni tarif ettiğinde koşullar ekleme yapar; geniş düzenden başlandığında her koşul geri alma bildirimleri taşır.
Sonraki Adım
Türetilen eşik elde var: 812 birim. Geriye bu sayının biçem dosyasında nasıl yazılacağı kalıyor. Koşul yalnız genişlikle kurulmaz; yüksekliğe, ekranın yönüne, çıktının kâğıda mı ekrana mı gideceğine göre de biçem ayrılabilir. Sonraki ders koşullu biçemin yazımını ele alır: bir koşul hangi özelliklere bakabilir, iki koşul nasıl birleştirilir ve iki aralığın ucundaki genişlikte hangi kural kazanır?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.