Tasarım sistemini Figma'da değil kodda tutmak: belirteçler ve testler
Tasarım sistemi denince çoğu kişinin aklına özenle hazırlanmış bir Figma dosyası geliyor. O dosya güzel bir başlangıç ama tek başına bir sistem değil. Üç ay sonra tasarımcı birincil rengi bir ton koyulaştırıyor, geliştirici bundan habersiz, uygulamada eski renk yaşamaya devam ediyor. Altı ay sonra kodda on dört farklı gri tonu var ve hiçbiri Figma'dakiyle aynı değil.
Portföyümdeki mobil bankacılık çalışmasında bu sorunu tersinden çözdüm: tasarım sisteminin tek doğruluk kaynağı src/theme/tokens.ts adlı bir TypeScript dosyası. Uygulama değerleri doğrudan buradan okuyor, testler buradan doğruluyor, web ve tasarım araçları için dışa aktarım da buradan üretiliyor.
Design token nedir, neden katmanlı olmalı
Design token (Türkçesiyle tasarım belirteci), bir tasarım kararına verilmiş isimdir. #4f46e5 bir değer; primary bir karar. Kodda değeri değil kararı kullanırsanız, karar değiştiğinde tek bir satır değişir.
Belirteçleri iki katmanda tutmak işi çok kolaylaştırıyor:
- İlkel (primitive) belirteçler: ham renk skalaları. Bu projede indigo, slate, green, red, amber ve sky skalaları var. Bunlar "hangi renkler elimizde" sorusunu cevaplar.
- Anlamsal (semantic) belirteçler: renklerin rolleri.
text,surface,primary,dangerSoft,onDangerSoftgibi. Uygulamada açık ve koyu tema için 30 anlamsal renk rolü tanımlı.
Bileşenler yalnızca ikinci katmanı görür. Böylece koyu tema, yalnızca rollere başka ilkel değerler bağlamaktan ibaret kalır:
export const light: Theme = {
bg: palette.slate[50], surface: palette.slate[0],
text: palette.slate[900], textMuted: palette.slate[500],
primary: palette.indigo[600], textOnPrimary: palette.slate[0],
// ...
};
export const dark: Theme = {
bg: palette.slate[950], surface: palette.slate[850],
text: palette.indigo[50], textMuted: palette.slate[400],
primary: palette.indigo[400], textOnPrimary: palette.slate[950],
// ...
};
Dikkat ederseniz koyu temada textOnPrimary beyaz değil, neredeyse siyah. Koyu temada birincil renk açıldığı için üzerindeki yazının da koyulaşması gerekiyor. Bu tür kararları renk renk hatırlamak zor, bu yüzden bir sonraki bölümdeki sözleşmeye ihtiyaç var.
Renklerin yanında aynı dosyada şunlar da duruyor: 4 pt boşluk ızgarası (4, 8, 12, 16…), köşe yarıçapları, 9 kademeli tipografi ölçeği (her kademede boyut, satır yüksekliği, kalınlık ve harf aralığı), hareket süreleri ve 44 pt minimum dokunma hedefi.
Kontrast kurallarını teste çevirmek
Erişilebilirlik genelde projenin sonunda bir denetimle kontrol edilir ve o noktada düzeltmek pahalıdır. Mobil bankacılık uygulamasında kontrast kuralları belirteç dosyasının bir parçası. contrastContract adlı dizide 16 metin/zemin çifti ve her biri için beklenen minimum WCAG oranı var:
export const contrastContract = [
{ fg: "text", bg: "bg", min: 7, note: "gövde metni / sayfa (AAA)" },
{ fg: "textMuted", bg: "surface", min: 4.5, note: "soluk metin / kart (AA)" },
{ fg: "onDangerSoft", bg: "dangerSoft", min: 4.5, note: "hata rozeti (AA)" },
{ fg: "borderStrong", bg: "surface", min: 3, note: "giriş alanı kenarlığı" },
{ fg: "focus", bg: "bg", min: 3, note: "odak halkası" },
// toplam 16 çift
];
Test dosyası bu diziyi iki tema için döngüyle dolaşıyor, yani 16 çift 2 temada 32 ayrı test olarak çalışıyor:
for (const name of ["light", "dark"] as const) {
const t = themes[name];
for (const c of contrastContract) {
it(`${name}: ${c.note}`, () => {
expect(contrast(t[c.fg], t[c.bg], t.bg)).toBeGreaterThanOrEqual(c.min);
});
}
}
Biri soluk metin rengini "biraz daha zarif dursun" diye açarsa test kırılıyor ve hangi çiftin hangi oranla kaldığını söylüyor. Yarı saydam renkler (koyu temadaki rgba(248,113,113,0.16) gibi yumuşak rozet zeminleri) önce sayfa zeminine bindirilip sonra ölçülüyor; aksi hâlde sonuç yanıltıcı olurdu.
Aynı dosyada birkaç kural daha testle korunuyor: tipografi ölçeği küçükten büyüğe sıralı mı, satır yükseklikleri yeterli mi, tüm boşluklar 4'ün katı mı, dokunma hedefi 44 pt'nin altına inmiş mi, en büyük yazı ölçeğinde gövde metni 20 pt'ye ulaşıyor mu. Uygulamanın 54 birim testinin önemli bir kısmı bu sözleşmeden geliyor. Ayrıca uygulamanın içinde bir Belirteçler sayfası var; aynı oranlar orada seçili tema için canlı hesaplanıp gösteriliyor.
Bileşen kütüphanesi: sabit renk yasak
Belirteçler tek başına yetmez; kimse her ekranda theme.colors.primary yazıp düğme çizmek istemez. Bu projede src/components/ui.tsx içinde Button, Input, Chip, Toggle, Keypad, ListItem, Sheet, Toast, Banner, Skeleton gibi temel bileşenler ve AccountCard, BankCard, TxRow gibi alana özel bileşenler var. Button örneğin 4 varyant ve 3 boyutla geliyor, yükleniyor ve devre dışı durumlarını kendisi yönetiyor.
Kütüphanenin tek kuralı var: hiçbir bileşende sabit renk yok. Hepsi useTheme() ile tema ve yazı ölçeğini alıyor. Yazı ölçeği normal, büyük ve çok büyük olmak üzere üç kademe; kullanıcı ayarlardan büyük yazıyı seçtiğinde tüm uygulama buna uyuyor.
Erişilebilirlik de bileşenin sorumluluğu. Her etkileşimli öğe accessibilityRole, accessibilityLabel ve gerektiğinde accessibilityState taşıyor. Hata mesajları role=alert ile duyuruluyor, bakiye gizlendiğinde ekran okuyucuya ayrı bir etiket gidiyor. Uçtan uca testler de bütün düğme ve anahtarların erişilebilir bir adı olup olmadığını ve dokunma hedefi boyutunu denetliyor.
Son olarak uygulamanın içinde bir bileşen galerisi var: Daha → Tasarım sistemi → Bileşenler. Tüm bileşenler seçili tema ve yazı ölçeğiyle tek sayfada. Yeni bir bileşen eklendiğinde önce oraya konup iki temada ve üç yazı ölçeğinde nasıl durduğuna bakılıyor. Storybook kurmadan aynı fayda elde ediliyor.
Tasarımla kodu senkron tutmak
Kaynak koddaysa tasarımcı nasıl çalışacak? Burada standart bir biçim devreye giriyor. W3C bünyesindeki Design Tokens Community Group, belirteçlerin JSON olarak nasıl yazılacağını tanımlayan biçimin ilk kararlı sürümünü (2025.10) Ekim 2025'te yayımladı. Bu biçime kısaca DTCG deniyor.
Aynı çalışmada npm run tokens komutu, TypeScript dosyasını okuyup iki çıktı üretiyor:
design-tokens/tokens.json: DTCG biçiminde renkler, boşluklar, tipografi, hareket süreleri.design-tokens/tokens.css: web için CSS değişkenleri, koyu tema[data-theme="dark"]altında.
:root {
--color-primary: #4f46e5;
--color-textMuted: #646c82;
--space-4: 16px;
}
[data-theme="dark"] {
--color-primary: #818cf8;
}
Bu sayede aynı marka için yazılan bir web sitesi, mobil uygulamayla birebir aynı renkleri kullanıyor. DTCG JSON'unu içe alabilen tasarım araçları da aynı dosyayı okuyabiliyor.
Akış şöyle kurulu: tasarımcı yeni bir renk ya da boşluk önerisini tasarım dosyasında deniyor, karar netleşince değişiklik belirteç dosyasına bir çekme isteğiyle giriyor. Testler kontrastı ve kuralları doğruluyor, dışa aktarım yeniden üretiliyor. Yön her zaman koddan dışarı doğru. İki yönlü otomatik senkron kurmayı denemedim; iki kaynağın birbirini ezdiği durumları çözmek, tek yönlü akışın getirdiği küçük zahmetten daha pahalı geliyor bana.
Sistemi başka projeye taşımak
Kod olarak tutulan bir sistemin güzel yanı taşınabilir olması. Yemek sipariş uygulaması çalışmamda mobil bankacılık uygulamasının belirteç mimarisini aynen aldım, ilkel renkleri sıcak ve kırmızı bir paletle değiştirdim. Kontrast sözleşmesi bu projede 11 çift; açık ve koyu tema, 3 kademeli yazı ölçeği ve 44 pt dokunma hedefi aynı şekilde geldi. Renkler değişti, kurallar ve testler olduğu gibi kaldı. Yeni paletteki bir ton sözleşmeyi bozsaydı bunu ekrana bakarak değil, kırılan testten öğrenilecekti; asıl kazanç bu.
Kendi projeniz için kısa kontrol listesi
- Renkleri ilkel ve anlamsal olarak iki katmana ayırın; bileşenler yalnızca anlamsal rolleri görsün.
- Boşluk, köşe, tipografi ve dokunma hedeflerini de belirteç yapın, yalnızca renkleri değil.
- Kontrast kurallarını bir dizi olarak yazın ve her tema için testte dolaşın.
- Bileşenlerde sabit renk ve sabit yazı boyutu kullanmayın; hepsi temadan ve yazı ölçeğinden gelsin.
- Tüm bileşenleri gösteren bir galeri sayfası tutun, yeni bileşeni önce orada görün.
- Belirteçleri DTCG JSON ve CSS değişkeni olarak dışa aktarın, web ve tasarım tarafı aynı kaynağı kullansın.
- Senkronun yönünü baştan belirleyin ve tek yönlü tutun.