Bu siteyi Yaparken: Grid Sistemleri, Kodla Çizilen Görseller ve Kısıtın Gücü
Şubat 2026’da kişisel bir site yapmaya karar verdim. Bir template değil, bir tema değil — her üründe uyguladığım tasarım felsefesini somutlaştıracak bir site. Kısıt, modülerlik ve görünmez detaylara takıntılı bir dikkat. Bu yazı o sitenin nasıl bir araya geldiğinin teknik hikayesi: grid sistemi, kodla üretilen görseller, design token mimarisi ve bütün bu kararların arkasındaki düşünce.
Site Astro üzerinde çalışıyor. Ana sayfadaki her görsel tamamen kodla yapılmış. Harici bir görsel yok, illüstrasyon kütüphanesi yok, ilk render’da canvas yok. Sadece HTML, CSS ve SVG — server-side render edilmiş, hızlı, tam anlamıyla responsive.
Grid Sistemi
En kritik mimari karar grid’di. CSS Grid spec’i değil — o zaten verilmiş bir şey — üzerine kurduğum component soyutlaması. Vercel ekibinin ana sayfa craft yazısında anlattığı yaklaşımdan ilham alarak, içinde kaç eleman olursa olsun kendi kılavuz çizgilerini çizebilen bir grid sistemi istedim.
Border Kullanmanın Sorunu
Grid çizgisi çizmenin en yaygın yolu her child elemana iki dik kenarda border eklemek — örneğin border-right ve border-bottom. Grid’deki her hücre doluysa sonuç temiz görünür. Container’a border-top ve border-left eklersen illüzyon tamamlanır.
Ama boş hücre olduğu an, birden fazla sütuna yayılan bir eleman olduğu an ya da child sayısı grid’i tam doldurmadığı an illüzyon bozulur. Olmayan elemanlara border çizmeye ya da olması gereken yerde border’ı kaçırmaya başlarsın.
İstediğim şey şuydu: dolu ya da boş, tüm hücreler için kılavuz çizgisi çizen, içeriği herhangi bir satır-sütun kesişimine özgürce yerleştirmeye izin veren bir grid.
display: contents — Saklı Mücevher
Çözüm, çoğu developer’ın hiç kullanmadığı bir CSS property’den geldi: display: contents.
.grid-guides {
display: contents;
}
Bu property bir container’a uygulandığında, o elementi box model’dan tamamen siliyor. Child’lar, elementin parent’ının doğrudan child’ıymış gibi render ediliyor. Yani bir CSS Grid container’ının içine, layout’u hiç bozmadan bir rehber katmanı ekleyebiliyorsun.
Grid component’inin özü şöyle çalışıyor:
---
const { rows = 3, columns = 3 } = Astro.props;
const guides = Array.from(
{ length: rows * columns },
(_, index) => ({
x: (index % columns) + 1,
y: Math.floor(index / columns) + 1,
})
);
---
<div
class="relative grid border-t border-l border-grid-line"
style={`
grid-template-columns: repeat(${columns}, minmax(0, 1fr));
grid-template-rows: repeat(${rows}, minmax(0, 1fr));
`}
>
<!-- Rehber katmanı: display: contents -->
<div class="contents pointer-events-none">
{guides.map(({ x, y }) => (
<div
class="absolute inset-0 border-b border-r border-grid-line"
style={`grid-column-start: ${x}; grid-row-start: ${y};`}
/>
))}
</div>
<!-- Asıl içerik buraya -->
<slot />
</div>
guides dizisi rows × columns kadar placeholder eleman üretiyor. Her biri grid koordinatına yerleştirilip border-bottom ve border-right çiziyor. display: contents wrapper’ının içinde oturdukları için parent grid’in layout’una katılıyorlar, kendi box’larını oluşturmadan. Parent grid görsel çerçeveyi tamamlamak için border-top ve border-left ekliyor.
Sonuç: içinde kaç ya da ne kadar az child olursa olsun, her zaman tam bir kılavuz çizgisi seti çizen bir grid. Gördüğünde “zaten böyle olması gerekiyordu” dedirten türden bir çözüm — ama varmak için şaşırtıcı miktarda iterasyon gerekti.
GridCell ve GridCross
Grid temeli oturdu, iki yardımcı component sistemi tamamlıyor:
GridCell — İsteğe bağlı colSpan ve rowSpan ile herhangi bir satır/sütun kesişimine yerleştirilebilen content container:
---
const {
row = 'auto', column = 'auto',
rowSpan = 1, colSpan = 1,
solid = false
} = Astro.props;
const gridRow = row === 'auto'
? 'auto' : `${row} / span ${rowSpan}`;
const gridColumn = column === 'auto'
? 'auto' : `${column} / span ${colSpan}`;
---
<div
class={`relative ${solid
? 'z-20 bg-white dark:bg-black border-r border-b'
: 'z-10'}`}
style={`grid-row: ${gridRow}; grid-column: ${gridColumn};`}
>
<slot />
</div>
solid prop’u, grid çizgilerinin üzerinde oturan (z-20 vs z-10) opak beyaz/siyah hücreleri yaratıyor. İçeriğin rehber grid’in üzerinde yüzüyormuş gibi görünmesini sağlayan şey bu z-index katmanlama stratejisi. Sitenin görsel kimliğinin merkezinde bu var.
GridCross — Geleneksel baskı registration mark’larından ilham alan küçük artı işaretleri. Grid kesişimlerine mutlak konumlandırılmış bu işaretler, ince bir navigasyon ipucu olarak işlev görüyor:
<div
class="absolute z-30 w-3 h-3 text-grid-cross pointer-events-none"
style={`
grid-row-start: ${row};
grid-column-start: ${column};
transform: translate(-50%, -50%);
`}
>
<svg viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path d="M12 4.5v15m7.5-7.5h-15" />
</svg>
</div>
z-30 ile artı işaretleri her şeyin üzerinde oturuyor — solid hücrelerin bile. Küçükler (12px), pointer-events: none kullanıyorlar ve renklerini --grid-cross CSS variable’ından alıyorlar. Baskı işareti metaforu dijital arayüzü tasarım zanaatinin fiziksel dünyasıyla buluşturuyor — bu projeyi derinden etkileyen Müller-Brockmann ve İsviçre grafik tasarım geleneğine bir selam.
Z-Index Katmanlama Modeli
Tüm görsel sistem hassas bir katmanlama stratejisine dayanıyor. Aşağıdan yukarıya stack:
| Katman | z-index | Ne var |
|---|---|---|
| Grid Guides | z-10 | Kılavuz çizgiler |
| Beam (ışık geçişi) | z-15 | Kayan radial-gradient |
| GridCell (solid) | z-20 | Opak hücre içerikleri |
| GridCross | z-30 | Artı işaretleri |
Beam z-index: 15’te yaşıyor — grid çizgilerinin (z-10) önünde, solid hücre içeriklerinin (z-20) arkasında görünür şekilde hareket ediyor. Atmosferik bir derinlik yaratıyor. Etki tamamen CSS — JavaScript yok, canvas yok, WebGL yok.
Hero Composition
Hero bölümü grid’i çok daha büyük bir ölçekte kullanıyor — masaüstünde 12 sütun × 8 satır, mobilde 4 × 6. solid etkin tek bir GridCell, satır 2’den 7’ye, sütun 2’den 11’e kadar yayılarak başlık ve CTA butonları için ortalanmış bir sahne yaratıyor.
Beam — kayan bir radial-gradient — hero bölümüne o uhrevi kaliteyi veren şey:
.beam {
position: absolute;
z-index: 15;
pointer-events: none;
width: 60%;
height: 60%;
background: radial-gradient(
ellipse at center,
rgba(161, 161, 170, 0.25) 0%,
rgba(161, 161, 170, 0.08) 40%,
transparent 70%
);
animation: beam-drift 14s ease-in-out infinite;
will-change: transform;
}
Hareket hassasiyeti olan kullanıcılar için prefers-reduced-motion ile kendini gizliyor.
Kodla Üretilen Görseller
Ana sayfadaki her görsel component, sadece HTML, CSS ve inline SVG kullanılarak Astro component’i olarak yapılmış. Harici görsel yok. Icon font yok. Render için runtime JavaScript yok. Bu bilinçli bir karardı ve birkaç nedeni var:
- Her ölçekte kusursuz kalite — Vektör tabanlı görseller 4K ekranda da mobilde de hiç piksellenmez.
- Design token entegrasyonu — Her görsel sitenin geri kalanıyla aynı CSS variable’larını (
--grid-line,--text-muted,--boyut-primary) kullanıyor, yani otomatik olarak açık/koyu modda uyum sağlıyor. - Gerçek anlamda responsive — Görsellerden farklı olarak, kodla üretilen görseller container boyutuna göre kendilerini yeniden yapılandırabilir.
- Sıfır ekstra network isteği — Her şey inline ve server-side render edilmiş.
BoyutVisual: Design System Önizlemesi
Boyut görseli, component önizlemelerinin yanında design token’ları gösteren bir kod editörü penceresini simüle ediyor. Görsel, bir miniatur bölünmüş panel editörü olarak yapılandırılmış. Sol panel color.primary, radius.full, spacing.md, font.sans gibi design token anahtar-değer çiftlerini gösteriyor. Sağ panel ise CSS variable sisteminden gelen gerçek design token değerlerini kullanan butonlar ve bir input içeren gerçek UI component’larını render ediyor.
.boyut-btn-primary {
background: var(--boyut-primary);
color: var(--boyut-primary-fg);
font-size: 8px;
padding: 4px 10px;
border-radius: 9999px;
}
Görselin içindeki minyatür buton var(--boyut-primary) kullanıyor — sitenin gerçek butonlarına güç veren değişkenin aynısı. Kullanıcı dark mode’a geçtiğinde, görselin butonları diğer her şeyle birlikte renk değiştiriyor. Ayrı bir “dark mode görseli” yok — aynı component cascade’e tepki veriyor.
PromptVisual: AI Canvas Önizlemesi
Promptheon görseli, değişkenler paneli ve output akışıyla bir prompt yönetim canvas’ını temsil ediyor. Output panelinde farklı genişliklerde ince, yuvarlak kenar köşeli dikdörtgenler var ve AI output akışını simüle etmek için staggered bir CSS animasyonuyla titreşiyor. Alttaki yanıp sönen imleç, gerçek bir terminal imleci gibi davranmak için step-end zamanlama kullanıyor. Her iki animasyon da prefers-reduced-motion’a saygı gösteriyor.
EmiVisual: SVG Çizim Animasyonu
Emi görseli belki de en sinematik component. Bir çizim animasyonu yaratmak için stroke-dasharray ve stroke-dashoffset kullanarak SVG portresi render ediyor:
.emi-stroke {
stroke: #8b6c4f;
fill: none;
stroke-dasharray: 400;
stroke-dashoffset: 400;
animation: draw 2.5s ease forwards;
}
@keyframes draw {
to { stroke-dashoffset: 0; }
}
Her SVG path elemanına staggered bir animation-delay veriliyor — yüz konturu önce çiziliyor (0s), sonra gözler (0.4s, 0.7s), burun (1.0s), ağız (1.2s), kaşlar (1.4s) ve son olarak saçlar (1.6s). İmza 2.2s’de fade in oluyor. Bütün sekans, sadece CSS ve inline SVG kullanarak 2.5 saniyede bir yaratım hikayesi anlatıyor.
Sıcak, parşömen benzeri arka plan rengi (açık modda #fdf6ee, koyu modda #1c1710) sitenin geri kalanından farklı, dokunsal bir kalite katıyor — bu hücrenin farklı bir yaratıcı disiplini temsil ettiğini söylüyor.
GalerionVisual: Mobil Mockup
Galerion, çentikli, uygulama başlıklı, Van Gogh’un Yıldızlı Gece’sinin SVG soyutlamasını içeren bir sanat kartı ve alt navigasyon çubuğu olan minyatür bir telefon mockup’ı olarak render ediliyor. Telefon çerçevesinin kendisi CSS border ve border-radius ile yapılmış — telefon görseli yok, cihaz frame kütüphanesi yok. Her şey donanım gibi görünmesi için stillendirilmiş yapısal HTML.
UtilityVisual: Ayarlar Widget’ı
Utility hücresi, gerçek anlamda interaktif olan tek görsel. Çalışan bir tema geçişi ve çalışan bir dil değiştirici içeriyor — her ikisi de sitenin gerçek state’ine bağlı. Alttaki küçük “Designed by The Emi.” atfı serif italic bir font kullanıyor — üzerindeki monospace etiketlerle kasıtlı bir tipografik kontrast, mekanikten kişisele geçişi işaret ediyor.
Bento Layout
Ana sayfa beş görseli bir bento grid’de bir araya getiriyor — masaüstünde 4 satır × 3 sütunluk bir layout, mobilde tek sütunlu 8 satıra daralan. Masaüstü bento’nun yapısal wireframe’i:
Staggered colSpan değerleri görsel ritim yaratıyor: Boyut 2 sütuna yayılıyor, sonra writing hücresi 1 sütun alıyor. Stack hücresi 1 sütun, sonra Promptheon 2 sütuna yayılıyor. Bu dönüşüm sıkı hizalamayı korurken layout’un tek düze hissettirmesini engelliyor.
hideTopBorder prop’u grid’in üst border’ını kaldırarak yukarıdaki hero bölümüyle görsel bir bağlantı kuruyor. Artı işaretleri masaüstünde her iç kesişime yerleştiriliyor — layout’un yapısal eklemlerini işaretleyen 12 artı işareti. Görsel karmaşıklığı önlemek için mobilde kaldırılıyorlar.
Design Token Mimarisi
Renk sisteminin tamamı :root ve html.dark’ta tanımlanan CSS custom property’lerinden akıyor:
:root {
--grid-line: #e5e7eb;
--grid-cross: #9ca3af;
--text-muted: #555862;
--boyut-primary: #000000;
--boyut-primary-fg: #ffffff;
--brand-accent: #a90432;
}
html.dark {
--grid-line: #27272a;
--grid-cross: #71717a;
--text-muted: #a1a1aa;
--boyut-primary: #ffffff;
--boyut-primary-fg: #000000;
--brand-accent: #d1064a;
}
Sistemdeki her component — grid çizgilerinden buton arka planlarına, görsel component’lara kadar — renklerini bu yedi variable’dan alıyor. Component ağacında hiçbir yerde hard-coded renk değeri yok. Tema değişikliği <html> üzerindeki class’ı değiştirmek demek, ve sayfadaki her element uyum içinde tepki veriyor.
--brand-accent özel bir yerde duruyor. Açık modda koyu bordo (#a90432), koyu modda daha canlı kırmızımsı (#d1064a). Tam olarak iki yerde görünüyor: hero başlığındaki “Güney.” den sonraki noktada ve metin seçim vurgusunda. Sadece %10 sıklıkta kullanılması önemini koruyor — 60-30-10 ilkesi tam bir disiplinle uygulanmış.
Reduced Motion ve Erişilebilirlik
Sitedeki her animasyon bir prefers-reduced-motion media query içeriyor:
@media (prefers-reduced-motion: reduce) {
.beam { animation: none; opacity: 0; }
.emi-stroke { animation: none; stroke-dashoffset: 0; }
.canvas-cursor { animation: none; opacity: 0.6; }
}
Yaklaşım animasyon türüne göre değişiyor. Tamamen dekoratif olan beam tamamen gizleniyor. SVG çizim animasyonu tamamlanmış haline snap ediyor, böylece illüstrasyon hâlâ görünür. İmleç kısmi opacity’de sabit kalıyor. Her kural, içeriğe neyin hizmet ettiğini ve neyin sadece eğlendirdiğini düşünerek verilmiş bilinçli bir karar.
Tüm görsel component’lar uygun role="img" ve aria-label attribute’larına sahip. Tema geçişi gibi interaktif elementler açıklayıcı aria-label metni taşıyor. Dil değiştirici aktif state’ini opacity ve border değişiklikleriyle iletişim kuruyor.
Stack
- Framework: Astro 5 — varsayılan olarak sıfır JavaScript felsefesiyle seçildi
- Styling: Tailwind CSS 4,
@variant darkve design token override’larıyla - Tipografi: Astro’nun
<Font>component’i üzerinden yüklenen Geist Sans ve Geist Mono - i18n: Düz bir çeviri haritasıyla custom utility fonksiyonları — ağır i18n kütüphanesi yok
- İçerik: Glob loader ve Zod ile doğrulanmış frontmatter schema’sıyla MDX tabanlı blog
- Deploy: Static build
Son Düşünceler
Bu siteyi yapmak bir kısıt egzersiziydi. Z-index katmanlama stratejisinden yedi variable’lık renk sistemine, display: contents numarasına kadar her karar tek bir inançla alındı: en güçlü arayüzler, kaçınılmaz hissettirenledir.
Bu sitede sırf gösteri için var olan bir animasyon yok. Compositional bir amaca hizmet etmeyen bir gradient yok. En az iki kez tartışılıp soyutlanıp yeniden yapılmamış bir element yok.
Grid dekorasyon değil. O mimari. Görseller illüstrasyon değil. Onlar component. Dark mode bir feature değil. İlk satırdan itibaren var olan temel bir tasarım kısıtı.