Sass Temelleri: Variable, Nesting, Parent Selector (&), Interpolation ve Placeholder
Sass değişkeni derleme anında bir değerdir ve derleme bitince yok olur; custom property ise runtime'da yaşar, bu yüzden ikisini birlikte kullanırken değeri #{} ile aktarmak gerekir, yoksa tarayıcıya "$brand" metni gider. Bookstore kartını & ile BEM'e çeviriyor, Sass nesting'inin native nesting'den farklı specificity ürettiğini iki motorda ölçüyor ve derin nesting'in çıktıyı nasıl şişirdiğini gösteriyoruz.
Sürüm notu: Bu yazıdaki örnekler Dart Sass 1.108.2, Angular 22.2 (TypeScript 6.0) ve Playwright 1.65'in Chromium ve WebKit build'leriyle denenmiştir.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için renkleri Sass değişkeni olarak tutuyoruz ama tarayıcının da onları görmesini istiyoruz. Akla ilk gelen yazım şu:
$brand: #b45309;
:root {
--color-brand-raw: $brand;
}
Derlenmiş CSS'te --color-brand-raw'un değeri ne olur? #b45309 mu, yoksa başka bir şey mi? Cevap Sass'ın değişken ile custom property arasındaki sınırı nasıl çizdiğini gösteriyor; bu yazının ilk bölümü o sınır üzerine.
Serinin ilk yazısında Sass'ın bir derleme adımı olduğunu ve native CSS'in hangi alanları kapattığını gördük. Bu yazıda Sass'ın her gün kullanılan temel syntax'ına geçiyoruz: değişkenler, nesting ve &, interpolation, placeholder'lar ve yorumlar. Bookstore'un Angular istemcisinde (bookstore-web) kart stilini BEM (Block, Element, Modifier; class isimlendirme yöntemi) ile yeniden yazacak, sonunda da fazla nesting'in derlenmiş CSS'te neye dönüştüğünü ölçeceğiz.
$variable ve Custom Property: Derleme Zamanı ve Runtime
Tahminin cevabı derleyicide:
$brand: #b45309;
$radius: 8px;
$gutter: 16px;
:root {
// #{} olmadan değer yazıldığı gibi kopyalanır
--color-brand-raw: $brand;
--color-brand: #{$brand};
--radius: #{$radius};
}
.book-list {
// Sass calc()'ı tanıyor: interpolation gerekmez
width: calc(100% - $gutter * 2);
padding-inline: $gutter;
}
npx sass 02/tokens.scss
:root {
--color-brand-raw: $brand;
--color-brand: #b45309;
--radius: 8px;
}
.book-list {
width: calc(100% - 32px);
padding-inline: 16px;
}
--color-brand-raw'un değeri tarayıcıya $brand metni olarak gidiyor. Sebep custom property'lerin CSS'teki tanımı: değerleri neredeyse her şey olabilir, bir JavaScript parçası bile. Sass bu yüzden custom property değerine dokunmuyor ve içinde Sass ifadesi görmek istiyorsan bunu #{} ile açıkça söylemen gerekiyor. Tarayıcı hata vermiyor; var(--color-brand-raw) kullanan declaration sessizce çalışmıyor.
İkinci kural ters yönü gösteriyor. calc() içinde Sass değişkeni doğrudan kullanılabiliyor; Dart Sass calc()'ı tanıyor, içindeki işlemleri yapabildiği kadar derleme anında yapıyor ($gutter * 2, 32px oldu) ve 100% gibi derleme anında bilinemeyen kısmı tarayıcıya bırakıyor. Eski kodlarda gördüğün calc(100% - #{$gutter}) yazımı hâlâ çalışıyor ama artık gerekmiyor.
İki değişken türü arasındaki fark kısaca şu: $brand derleme zamanında bir değerdir, derleme bitince yok olur; --color-brand runtime'da bir property'dir, cascade'e katılır, inherit edilir, media query'ye ya da bir class'a göre değişebilir ve JavaScript'ten okunup yazılabilir. Hangisinin ne zaman kullanılacağı CSS3 serisinin custom property yazısında ayrıntılı anlatıldı. Bookstore'da kuralım şu: tasarım değerleri Sass'ta tanımlanır, component'ler yalnız custom property kullanır. Global styles.scss:
// tasarım değerleri Sass'ta; tarayıcı custom property alır
$color-brand: #b45309;
$color-border: #d6d3d1;
$color-text: #1c1917;
$color-muted: #57534e;
$radius: 8px;
$space: 16px;
:root {
--color-brand: #{$color-brand};
--color-border: #{$color-border};
--color-text: #{$color-text};
--color-muted: #{$color-muted};
--radius: #{$radius};
--space: #{$space};
}
body {
margin: 0;
font-family: system-ui, sans-serif;
color: var(--color-text);
}
ng build'in ürettiği global stil dosyası:
:root{--color-brand: #b45309;--color-border: #d6d3d1;--color-text: #1c1917;--color-muted: #57534e;--radius: 8px;--space: 16px}body{margin:0;font-family:system-ui,sans-serif;color:var(--color-text)}
Bu ayrımın pratik bir faydası var: component dosyaları global Sass değişkenlerini görmüyor (her .scss dosyası ayrı derleniyor, değişkenleri paylaşmanın yolu serinin dördüncü yazısındaki module sistemi), ama custom property'ler DOM (sayfanın element ağacı) üzerinden her component'e ulaşıyor. Tema değiştirmek de yalnız custom property'leri değiştirmek demek; bunu beşinci yazıda yapacağız.
Sass değişkenlerinin bir de scope'u var. Bir blok içinde tanımlanan değişken yalnız o blokta görünür; en üst seviyede tanımlanan değişken dosya genelinde. Bir blok içinden global değişkeni değiştirmek için !global flag'i var ama kullandığım her yerde sonradan kafa karıştırdı; değeri bir fonksiyonun sonucu olarak üretmek hemen her zaman daha okunur.
Nesting ve &: BEM ile Kitap Kartı
Sass nesting'inin en çok kullanılan tarafı &. Sass'ta & ebeveyn selector'ün metnidir; önüne ya da arkasına başka bir metin yapıştırılabilir. Bookstore kartının lab'deki hâli:
.book-card {
padding: var(--space, 16px);
border: 1px solid #d6d3d1;
// BEM element: .book-card__title
&__title {
margin: 0;
font-size: 1.125rem;
}
// BEM modifier: .book-card--featured
&--featured {
border-color: var(--color-brand);
// modifier içinde &, .book-card--featured demek
.book-card__title {
color: var(--color-brand);
}
}
&:hover,
&:focus-within {
border-color: #a8a29e;
}
// sonda &: önce bağlam gelir
.catalog--compact & {
padding: 8px;
}
// yan yana kartlar
& + & {
margin-top: 0;
}
}
.book-card {
padding: var(--space, 16px);
border: 1px solid #d6d3d1;
}
.book-card__title {
margin: 0;
font-size: 1.125rem;
}
.book-card--featured {
border-color: var(--color-brand);
}
.book-card--featured .book-card__title {
color: var(--color-brand);
}
.book-card:hover, .book-card:focus-within {
border-color: #a8a29e;
}
.catalog--compact .book-card {
padding: 8px;
}
.book-card + .book-card {
margin-top: 0;
}
Çıktıda dikkat edilecek üç şey var. &__title ve &--featured iç içe yazılmalarına rağmen çıktıda tek class'lı, düz selector'ler: BEM'in amacı da bu, her element'in specificity'si (0,1,0). .catalog--compact & ise &'ı sona koyarak "bu kart şu bağlamın içindeyse" diyor; Sass bunu .catalog--compact .book-card'a çeviriyor. Bir de selector listesi (&:hover, &:focus-within) her parçası için ayrı ayrı genişletiliyor.
BEM'de element'leri modifier'ın içine iç içe yazmak (&--featured .book-card__title) bilinçli bir istisna: modifier bir durumun bütün kartı nasıl etkilediğini söylüyor ve bunun tek bir yerde durması okumayı kolaylaştırıyor. Bunun bedeli o kuralın specificity'sinin (0,2,0) olması.
Bookstore'un BookCard component'i artık bu yapıyı kullanıyor. Template'e [class.book-card--featured]="book().featured" binding'ini ekledim ve Dune'u öne çıkan kitap yaptım. Component'in stil dosyası:
.book-card {
padding: var(--space);
border: 1px solid var(--color-border);
border-radius: var(--radius);
&:hover {
border-color: var(--color-brand);
}
&--featured {
border-width: 2px;
border-color: var(--color-brand);
}
&__title {
margin: 0 0 4px;
color: var(--color-brand);
}
&__author {
margin: 0;
color: var(--color-muted);
}
&__price {
font-weight: 700;
}
}
Build'den sonra sayfayı küçük bir Playwright script'iyle açıp kartların kenarlığını okudum. Script dist klasörünü kendi içindeki küçük bir HTTP sunucusuyla servis ediyor ve verilen selector için computed değerleri yazıyor:
node scripts/web-probe.mjs ".book-card" border-top-color,border-top-width
Dune border-top-color=rgb(180, 83, 9) border-top-width=2px
Emma border-top-color=rgb(214, 211, 209) border-top-width=1px
The Hobbit border-top-color=rgb(214, 211, 209) border-top-width=1px
Middlemarch border-top-color=rgb(214, 211, 209) border-top-width=1px
Component içindeki var(--color-brand) global :root'tan geliyor; Angular'ın emulated encapsulation'ı selector'lere attribute ekliyor ama custom property'lere dokunmuyor.
Sass Nesting'i Native Nesting Değil
Sass nesting'i her zaman düzleştirir: çıktıda iç içe kural kalmaz. Bu, aynı dosyayı derlemeden tarayıcıya verdiğinde farklı sonuç alabileceğin anlamına geliyor. Birinci yazıda &__title'ın native CSS'te çalışmadığını gördük. Daha az bilinen fark, ebeveyn bir selector listesi olduğunda ortaya çıkıyor:
#featured,
.book-card {
.price {
font-weight: 700;
}
}
.book-card .price {
font-weight: 400;
}
Bu dosya hem geçerli SCSS (Sass'ın CSS'e benzeyen syntax'ı) hem geçerli native CSS. Script'i aynı dosyayı bir kez olduğu gibi, bir kez de Sass'tan geçirip iki motora verdim:
node scripts/02-list-parent.mjs
chromium native nesting .book-card .price → font-weight 700
chromium compiled by Sass .book-card .price → font-weight 400
webkit native nesting .book-card .price → font-weight 700
webkit compiled by Sass .book-card .price → font-weight 400
Sass ebeveyn listesini iki ayrı selector'e açıyor: #featured .price, .book-card .price. Kart içindeki fiyat için eşleşen selector .book-card .price, specificity'si (0,2,0); aşağıdaki aynı specificity'li kural sonra geldiği için kazanıyor. Tarayıcı ise ebeveyn listesini :is(#featured, .book-card) olarak yorumluyor ve :is() listedeki en yüksek specificity'yi, yani id'ninkini alıyor: (1,1,0). Sonuç bir motorda 400, ötekinde 700. Sass'tan native nesting'e geçen bir projede bu tür satırlar hiçbir hata vermeden görünümü değiştirir; geçişten önce derlenmiş CSS'teki selector listelerini aramak gerekiyor. :is() davranışının ayrıntısı CSS3 serisinin nesting yazısında.
Interpolation: #{} ile Selector, Property ve String Üretmek
#{} bir Sass ifadesinin sonucunu metin olarak CSS'e yerleştiriyor. Değişkenin normalde kullanılamayacağı her yerde işe yarıyor: selector adı, property adı, URL ve string içi:
$block: "order-summary";
$edge: "inline-start";
$asset-base: "/assets/covers";
.#{$block} {
border-#{$edge}: 4px solid #b45309;
&__row {
display: flex;
}
}
.book-cover--placeholder {
background-image: url("#{$asset-base}/placeholder.webp");
}
// tırnaklı string içinde de çalışır
.debug::after {
content: "#{$block}";
}
.order-summary {
border-inline-start: 4px solid #b45309;
}
.order-summary__row {
display: flex;
}
.book-cover--placeholder {
background-image: url("/assets/covers/placeholder.webp");
}
.debug::after {
content: "order-summary";
}
Interpolation'ın sonucu her zaman tırnaksız bir string; $block tırnaklı tanımlanmış olsa da selector'de tırnaksız çıkıyor. Bu yüzden sayıyla iş yaparken interpolation'dan kaçınmak gerekiyor: #{$space}px gibi bir yazım 16px görünen ama artık sayı olmayan bir string üretir; üzerinde işlem yapmaya kalkınca Sass Undefined operation "16px * 2" hatası veriyor. Birim eklemek için $space * 1px yazılır. Interpolation'ı en çok döngüyle class üretirken kullanacağız (serinin üçüncü yazısı).
Placeholder ve @extend'e Giriş
% ile başlayan bir selector placeholder'dır: kendi başına hiçbir CSS üretmez, yalnız @extend edildiğinde çıktıya girer. Ekran okuyucuya görünen ama ekranda yer kaplamayan metin için Bookstore'da iki yerde aynı kurala ihtiyaç var:
%visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
// hiç extend edilmiyor: CSS üretmez
%unused-card-shadow {
box-shadow: 0 1px 2px rgb(0 0 0 / 0.2);
}
.skip-link:not(:focus) {
@extend %visually-hidden;
}
.cart-icon__label {
@extend %visually-hidden;
}
.cart-icon__label, .skip-link:not(:focus) {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
@extend declaration'ları kopyalamıyor; extend eden selector'leri placeholder'ın kuralına ekliyor. Çıktıda tek kural ve iki selector var, %unused-card-shadow ise hiç yok. Bu, mixin'den farklı bir davranış: mixin her @include'da declaration'ları yeniden yazar. Hangisinin ne zaman seçileceği ve @extend'in media query içinde neden çalışmadığı bir sonraki yazının konusu.
Yorumlar ve Çıktıya Etkisi
Sass'ta üç tür yorum var ve çıktıya etkileri farklı:
// sessiz yorum: çıktıya hiç girmez
/* normal yorum: expanded çıktıda var, compressed siler */
/*! korunan yorum: compressed çıktıda da kalır */
.book-card__price::before {
content: "₺"; // satır sonundaki sessiz yorum
}
npx sass 02/comments.scss
@charset "UTF-8";
/* normal yorum: expanded çıktıda var, compressed siler */
/*! korunan yorum: compressed çıktıda da kalır */
.book-card__price::before {
content: "₺";
}
// yorumları hiçbir zaman çıktıya girmiyor; ekip içi notlar için doğru yer orası. /* */ yorumları expanded çıktıda kalıyor, --style=compressed'da siliniyor. /*! */ ise compressed çıktıda da korunuyor; lisans başlıkları için kullanılıyor. Angular production build'i CSS'i ayrıca minify ettiği için oradaki davranışı da kendi build'inde kontrol et.
Çıktının ilk satırı beklenmedik: @charset "UTF-8";. Sass, çıktıda ASCII (temel 128 karakterlik İngilizce karakter seti) dışında bir karakter görünce (burada ₺; Türkçe yazılmış bir /* */ yorumu da aynı etkiyi yapar) expanded modda bu satırı, compressed modda ise dosyanın başına bir BOM (byte order mark; dosyanın kodlamasını belirten üç görünmez byte) ekliyor:
npx sass --style=compressed 02/comments.scss | head -c 3 | od -An -tx1
ef bb bf
Tek başına zararsız; ama CSS'i başka bir dosyanın ortasına ekleyen ya da byte byte karşılaştıran bir araç zinciri varsa sorun çıkarır. Sass CLI'ında --no-charset bu davranışı kapatıyor.
Üretimde Dikkat
Sass değişkeninden custom property üretirken interpolation'ı unutmak build'i kırmıyor, sayfayı kırıyor. Bunu yakalamanın en ucuz yolu derlenmiş CSS'te $ karakterini aramak: grep -n '\$' dist/**/*.css boş dönmeli. Sass bu durumu hata saymadığı için bu kontrol CI'a eklenmeye değer.
Sass ile native nesting'i aynı codebase'te karıştırma. Angular'da .scss dosyalarının hepsi Sass'tan geçtiği için native nesting zaten düzleştirilmiş olarak çıkıyor; düz .css dosyalarındaki nesting'i ise Sass değil tarayıcı yorumluyor. Aynı kuralın bir dosyada :is(), ötekinde düz selector specificity'siyle çalışması hata ayıklamayı zorlaştırıyor.
Derlenmiş CSS'i oku. Sass'ın ne ürettiğini görmenin yolu npx sass dosya.scss çıktısına ya da Angular'da development build'deki stil dosyasına bakmak. Code review'da yalnız kaynağa değil, en az bir kez çıktıya bakmak bu yazıdaki sürprizlerin hepsini yakalar.
Sık Düşülen Tuzaklar
En yaygın tuzak HTML ağacını Sass'ta birebir kopyalamak. Ödeme sayfasının sipariş özeti şöyle yazılmıştı:
.checkout {
.order-summary {
.order-summary__items {
li {
display: flex;
justify-content: space-between;
.title {
font-weight: 600;
a {
color: inherit;
&:hover {
text-decoration: underline;
}
}
}
.price {
font-variant-numeric: tabular-nums;
}
}
}
}
}
.checkout .order-summary .order-summary__items li {
display: flex;
justify-content: space-between;
}
.checkout .order-summary .order-summary__items li .title {
font-weight: 600;
}
.checkout .order-summary .order-summary__items li .title a {
color: inherit;
}
.checkout .order-summary .order-summary__items li .title a:hover {
text-decoration: underline;
}
.checkout .order-summary .order-summary__items li .price {
font-variant-numeric: tabular-nums;
}
Kaynakta zararsız görünen girinti, çıktıda her kurala dört selector'lük bir önek ekliyor. Son kuralın specificity'si (0,5,2): dört class, bir :hover, iki element. Aynı stilleri BEM ile düz yazınca (.order-summary__item, .order-summary__title, .order-summary__link, .order-summary__price) compressed çıktı 422 byte'tan 247 byte'a iniyor. Bir component için küçük bir fark, ama bu alışkanlık yüzlerce dosyaya yayıldığında stil dosyası gözle görülür şekilde büyüyor.
Asıl maliyet boyutta değil, override'da. Sonradan bir başlığı soluk göstermek için .is-muted { font-weight: 400; } eklendiğini düşün:
node scripts/02-deep-override.mjs
02/deep.scss .is-muted → font-weight 600
02/flat.scss .is-muted → font-weight 400
Derin nesting'li sürümde tek class'lı bir kural (0,1,0) asla kazanamıyor; geliştirici ya selector'ü aynı uzunlukta yazıyor ya da !important'a uzanıyor ve specificity yarışı başlıyor. Benim kuralım: kaynakta en fazla üç seviye ve çıktıda en fazla iki class. Pseudo-class (&:hover), modifier (&--featured) ve media query iç içe yazılmaya değer; DOM yapısı değil.
İkinci tuzak & ile class adı üretmenin aranabilirliği bozması. &__title yazınca projede book-card__title araması bu satırı bulmuyor. Bookstore'da buna element ve modifier seviyesinde izin veriyoruz, daha derin birleştirmelere (&__title &__icon) vermiyoruz.
Küçük Bir Deneme
★☆☆ 02/tokens.scss'te --color-brand-raw: $brand; satırını kullanan bir .price { color: var(--color-brand-raw, green); } kuralı ekle ve tarayıcıda fiyatın hangi renge boyandığını kontrol et. Fallback'in neden devreye girmediğini açıkla.
★★☆ 02/deep.scss'i BEM ile en fazla iki seviye nesting kullanarak yeniden yaz. Compressed çıktı boyutunu ve en uzun selector'ün specificity'sini öncesi ve sonrasıyla karşılaştır.
★★★ bookstore-web'de BookCard'a book-card--out-of-stock modifier'ını ekle: stok sıfırsa fiyatın üstü çizilsin ve başlık soluk görünsün. Kuralı modifier içinde nesting ile yaz, sonra web-probe.mjs ile The Hobbit kartının computed değerlerini doğrula.
Bir sonraki yazıda tekrarı kaldıran araçlara geçiyoruz: @mixin ve @content ile breakpoint mixin'i, @function ve sass:math, map ve @each ile utility class üretmek, mixin ile @extend arasındaki seçim ve her şeyi mixin yapmanın CSS çıktısına maliyeti.