Mert Özen Her Satırda Daha İleri
Frontend Sass/SCSS — #08

Sass Built-in Module'leri 1: sass:math, sass:string ve sass:list ile Birim Hesabı, px→rem Function'ı ve List İşlemleri

Mert Özen 15 Kas 2026 13 dk 8 görüntülenme
Sass Built-in Module'leri 1: sass:math, sass:string ve sass:list ile Birim Hesabı, px→rem Function'ı ve List İşlemleri

Sass sayıları birimleriyle taşır: uyumlu birimleri dönüştürür, rem ile px'i karıştırınca hata verir, 1.96.0'dan beri 16px * 2px'i tarayıcının attığı bir calc()'a çevirir; list'ler 1'den sayar. Bookstore'a birimsiz, px ve pt kabul eden bir rem(), 0'dan başlayan ölçeği list'ten okuyan space() ve font-stack() ekliyor, class adlarında escape'i ve unique-id()'nin build'i nasıl deterministik olmaktan çıkardığını gösteriyoruz.

Sürüm notu: Bu yazıdaki örnekler Dart Sass 1.108.2, Angular 22.2 (TypeScript 6.0; Angular CLI'ın içindeki Sass 1.104.1) ve Playwright 1.65'in Chromium build'iyle denenmiştir.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) fiyat etiketinde üç değer bir taban değerden hesaplanıyor:

@use "sass:math";

$base: 16px;

.price-tag {
    padding: $base * 0.5;
    line-height: math.div(24px, $base);
    width: $base * 2px;
}

Üç satırdan hangisi derleme hatası verir? Hiçbiri hata vermiyorsa, tarayıcı hangisini kullanır? Birkaç yıl önce bu sorunun cevabı başkaydı; bugünkü cevap yazının ilk bölümünde.

Önceki yazıda Dart Sass 2.0'a hazırlanırken deprecation'ları temizledik ve global built-in function'ların yerine module'lerin geçtiğini gördük. Seriye eklenen bu yeni blokta Sass'ın her gün kullanılan araç kutusuna geçiyoruz. İlk durak sayı, string ve list işlemleri: sass:math, sass:string ve sass:list. Sonunda Bookstore'un Angular istemcisine (bookstore-web) birim dönüşümü, spacing ölçeği ve font listesi için üç küçük function ekleyeceğiz.

sass:math: Birimler Derleme Anında Nasıl Hesaplanır?

Tahminin cevabı:

npx sass 08/guess.scss
.price-tag {
  padding: 8px;
  line-height: 1.5;
  width: calc(32px * 1px);
}

Derleme hatasız bitiyor. padding beklendiği gibi 8px, line-height birimsiz 1.5: px'i px'e bölünce birimler sadeleşiyor. Üçüncü satır ise ilginç. 16px * 2px'nin sonucu "px kare" birimli bir sayı. Eski Sass sürümleri burada isn't a valid CSS value hatası verirdi; Dart Sass 1.96.0'dan beri bu tür karmaşık birimli sayılar calc() olarak CSS'e yazılıyor. Peki tarayıcı ne yapıyor? Aynı CSS'i Chromium'a verip hangi declaration'ların kaldığını okudum:

node scripts/08-guess.mjs
kept by the browser: padding: 8px; line-height: 1.5;
computed: line-height=24px width=1248px

calc(32px * 1px) bir uzunluk değil, uzunluğun karesi; width için geçersiz ve tarayıcı declaration'ı parse ederken atıyor. Genişlik auto'ya düşüyor. Yani eskiden derleme anında yakalanan bir hata artık tarayıcıya sessizce gidiyor. Bu, birimlerle çalışan her function'ın kendi kontrolünü yapması için iyi bir sebep.

Sass sayıları birimleriyle birlikte taşıyor ve aritmetiği birimlere göre yapıyor. Kuralları bir dosyada topladım:

@use "sass:math";

.units {
    // uyumlu birimler dönüştürülür: sonuç soldaki birimi alır
    a: 1in + 6px;
    b: 6px + 1in;
    // birimsiz sayı diğer tarafın birimini alır
    c: 16px * 2;
    // px / px: birimler sadeleşir
    d: math.div(24px, 16px);
    // px * px CSS değeri değil ama geçerli bir Sass sayısı
    e: math.unit(16px * 2px);
    f: math.is-unitless(math.div(24px, 16px));
    g: math.compatible(12pt, 1px);
    h: math.compatible(1rem, 1px);
    i: math.div(1, 3);
    j: math.percentage(math.div(1, 3));
    k: math.round(2.5) math.ceil(2.1) math.floor(2.9);
    l: math.clamp(4px, 12px, 8px);
    m: math.pow(1.25, 3) math.sqrt(2);
}
.units {
  a: 1.0625in;
  b: 102px;
  c: 32px;
  d: 1.5;
  e: "px*px";
  f: true;
  g: true;
  h: false;
  i: 0.3333333333;
  j: 33.3333333333%;
  k: 3 3 2;
  l: 8px;
  m: 1.953125 1.4142135624;
}

Sass yalnız sabit oranla birbirine çevrilebilen birimleri dönüştürüyor: in, px, pt, cm gibi mutlak uzunluklar, açılar, süreler. rem ile px uyumlu değil, çünkü bir rem'in kaç piksel olduğu kullanıcının tarayıcı ayarına bağlı ve derleme anında bilinmiyor. 1rem + 4px bu yüzden hata:

Error: 1rem and 4px have incompatible units.
  ╷
1 │ .x { width: 1rem + 4px; }
  │             ^^^^^^^^^^
  ╵
  08/incompatible.scss 1:13  root stylesheet

Bölme için tek yol math.div(); eğik çizginin Dart Sass 2.0'da neden ayraç olacağını serinin üçüncü ve yedinci yazılarında gördük. Sonuçlar 10 ondalık basamağa kadar tutuluyor (0.3333333333). math.clamp() ise CSS'teki clamp() ile aynı sırayla (min, değer, max) çalışıyor ama derleme anında ve yalnız uyumlu birimlerle.

px→rem Function'ı: Birimsiz, px ve Uyumlu Birimler

Serinin üçüncü yazısında rem() function'ını yazmıştık; yalnız px kabul ediyordu ve math.unit($px) != "px" ile kontrol ediyordu. Tasarımcıdan gelen ölçüler bazen birimsiz (rem(6)), bazen pt oluyor. math.compatible() ile kontrolü birim adından değil, birimin px'e çevrilebilmesinden yapmak daha doğru:

@use "sass:math";

$root-font-size: 16px !default;

// px (ya da px'e çevrilebilen bir birim) -> rem; birimsiz sayı px sayılır
@function rem($value) {
    @if math.is-unitless($value) {
        $value: $value * 1px;
    }
    @if not math.compatible($value, 1px) {
        @error "rem() expects px or a unitless number, got #{$value}";
    }
    @return math.div($value, $root-font-size) * 1rem;
}
@use "units" as *;

.book-card__title {
    font-size: rem(18px);
    margin-block-end: rem(6);
    letter-spacing: rem(0.75pt);
}
.book-card__title {
  font-size: 1.125rem;
  margin-block-end: 0.375rem;
  letter-spacing: 0.0625rem;
}

0.75pt, math.div() sırasında 1px'e çevrildi; bölme işleminin uyumlu birimleri kendisi dönüştürmesi sayesinde ayrı bir dönüşüm kodu yazmadık. rem(1.5rem) ise build'i kendi mesajımızla kırıyor:

Error: "rem() expects px or a unitless number, got 1.5rem"
  ╷
2 │ .x { a: rem(1.5rem); }
  │         ^^^^^^^^^^^
  ╵
  08/rem-error.scss 2:9  root stylesheet

Biri rem("18px") gibi bir string verirse hata bizim mesajımız değil, built-in function'ın mesajı oluyor ve function'ın içini gösteriyor: Error: $number: "18px" is not a number. ve altında 08/_units.scss 7:9 rem(). Çalışıyor ama kullanıcıya en iyi mesaj değil; argüman tipini sass:meta ile kontrol edip anlamlı hata vermeyi kendi kütüphanemizi yazarken ayrıntılı göreceğiz.

Ölçek Üretmek: math.pow, Yuvarlama ve Ondalık Basamaklar

Tipografide sık kullanılan bir yöntem modular scale: her yazı boyutu bir öncekinin sabit bir oranla çarpımı. Oran 1.25 ise ölçek 0.8, 1, 1.25, 1.5625… diye gidiyor. math.pow() bunu üretiyor ama sonuçlar uzun ondalıklı çıkıyor. Sass 1.x'te math.round() yalnız tam sayıya yuvarlıyor; basamak sayısı için küçük bir yardımcı gerekiyor:

@use "sass:list";
@use "sass:math";

// modular scale: her adım bir öncekinin 1.25 katı
$ratio: 1.25;
$steps: (-1, 0, 1, 2, 3);

@function round-to($number, $digits: 3) {
    $factor: math.pow(10, $digits);
    @return math.div(math.round($number * $factor), $factor);
}

$type-scale: ();
@each $step in $steps {
    $type-scale: list.append($type-scale, round-to(math.pow($ratio, $step)) * 1rem);
}

@for $i from 1 through list.length($type-scale) {
    .text-#{$i} {
        font-size: list.nth($type-scale, $i);
    }
}

.raw {
    font-size: math.pow($ratio, 3) * 1rem;
}
.text-1 {
  font-size: 0.8rem;
}
...
.text-4 {
  font-size: 1.563rem;
}

.text-5 {
  font-size: 1.953rem;
}

.raw {
  font-size: 1.953125rem;
}

Burada list.append()'in yeni bir list döndürdüğüne dikkat et: Sass'ta list'ler değiştirilemiyor (immutable), bu yüzden döngüde sonucu aynı değişkene geri atıyoruz. Yuvarlama tarayıcı için şart değil; ama derlenmiş CSS'i okuyan biri için 1.563rem, 1.953125rem'den daha anlaşılır ve tasarım dokümanındaki değerle karşılaştırması kolay.

Sass Hesabı mı, calc() mi?

Dart Sass calc()'ı tanıyor ve içindeki işlemi derleme anında yapabiliyorsa yapıyor. Yapamıyorsa ifadeyi tarayıcıya bırakıyor:

@use "units" as *;

$header-height: 64px;

.catalog {
    // her şey derleme anında biliniyor: Sass hesaplar
    padding-block-start: calc($header-height + 16px);
    // 100vh ancak tarayıcıda bilinir: Sass calc()'ı korur
    min-height: calc(100vh - $header-height);
    // akışkan yazı boyutu: rem ve vw karışık, tarayıcı karar verir
    font-size: clamp(rem(16px), 0.875rem + 0.5vw, rem(20px));
}
.catalog {
  padding-block-start: 80px;
  min-height: calc(100vh - 64px);
  font-size: clamp(1rem, 0.875rem + 0.5vw, 1.25rem);
}

Benim kuralım şu: değer viewport'a, kullanıcının yazı boyutuna, container'a ya da custom property'ye bağlıysa calc()/clamp() yaz ve tarayıcıya bırak; yalnız tasarım değerlerinden türüyorsa Sass'ta hesapla. Son satırdaki clamp() Sass'ın değil CSS'in function'ı: math.clamp() birimleri uyumsuz olduğu için bunu hesaplayamazdı. Sass yalnız rem() çağrılarını çözüp gerisini olduğu gibi yazdı.

sass:string: Class Adı Üretirken String İşlemleri

Stil kodunda string işlemleri çoğunlukla selector ve class adı üretirken gerekiyor. Module'deki function'ların çoğunu tek dosyada denedim:

@use "sass:string";

$block: "stock-badge--low";

.string-demo {
    a: string.index($block, "--");
    b: string.slice($block, 1, string.index($block, "--") - 1);
    c: string.slice($block, -3);
    d: string.insert("book-card", "__title", -1);
    e: string.split("Inter, Segoe UI, system-ui", ", ");
    f: string.split("catalog.book-card.title", ".", 1);
    g: string.to-upper-case("isbn");
    h: string.unquote("Segoe UI") string.quote(Inter);
    i: string.length("Dune");
}
.string-demo {
  a: 12;
  b: "stock-badge";
  c: "low";
  d: "book-card__title";
  e: ["Inter", "Segoe UI", "system-ui"];
  f: ["catalog", "book-card.title"];
  g: "ISBN";
  h: Segoe UI "Inter";
  i: 4;
}

Index'ler 1'den başlıyor ve negatif index sondan sayıyor (-3 son üç karakter). string.split() 1.57.0'da geldi; köşeli parantezli (bracketed) bir list döndürüyor ve üçüncü parametre en fazla kaç kez bölüneceğini söylüyor. string.unquote("Segoe UI") tırnakları kaldırınca font-family için iki ayrı kelimeye dönüşüyor; boşluklu font adlarını tırnaklı tutmak gerekiyor. to-upper-case() yalnız ASCII (İngilizce alfabenin temel karakter seti) harflerini değiştiriyor; Türkçe karakterlerde tarayıcının text-transform'u ve lang attribute'u daha doğru sonuç veriyor.

Class adında CSS'te özel anlamı olan bir karakter varsa escape gerekiyor. Yüzde genişlik utility'lerini .w-1/3 gibi isimlerle üretmek isteyince:

Error: expected selector.
  ╷
1 │ .w-1/2 { width: 50%; }
  │     ^
  ╵
  08/escape-error.scss 1:5  root stylesheet
@use "sass:math";

$fractions: (2, 3);

@each $d in $fractions {
    @for $n from 1 to $d {
        // class adında eğik çizgiye izin yok: escape et
        .w-#{$n}\/#{$d} {
            width: math.percentage(math.div($n, $d));
        }
    }
}
.w-1\/2 {
  width: 50%;
}

.w-1\/3 {
  width: 33.3333333333%;
}

.w-2\/3 {
  width: 66.6666666667%;
}

HTML'de class adı escape'siz yazılıyor (class="w-1/3"); ters eğik çizgi yalnız CSS selector'ünde. Chromium'da 600px'lik bir kutu içinde ölçtüm: w-1/3 200px, w-2/3 400px geldi. Rakamla başlayan class adları da aynı sorunu yaşıyor; .2xl yazınca Sass Expected identifier. diyor, doğru yazım .\32 xl. Responsive variant'lardaki : karakteri için aynı escape'i utility üretirken kullanacağız.

string.unique-id() her çağrıda benzersiz bir isim üretiyor; @keyframes adları çakışmasın diye kullanılıyor. Yan etkisi var: aynı dosyayı iki kez derlediğimde bir seferinde @keyframes uuqgxbs, diğerinde @keyframes utx9nn3 çıktı. Çıktı her build'de değişince dosya adındaki içerik hash'i de değişiyor ve tarayıcı cache'i hiçbir şey değişmemiş olsa bile her deploy'da yeniden indiriyor. Bookstore'da keyframe adlarını elle veriyoruz.

sass:list: 1'den Başlayan Index'ler, Separator'lar ve slash

Sass'ta 4px 8px de, Inter, system-ui de bir list; tek fark separator. Bookstore'un spacing ölçeği ve font listesiyle:

@use "sass:list";

$space-scale: 0 4px 8px 16px 24px 32px;
$fonts: "Inter", "Segoe UI", system-ui;

.list-demo {
    a: list.nth($space-scale, 1);
    b: list.nth($space-scale, -1);
    c: list.length($space-scale);
    d: list.index($space-scale, 16px);
    e: list.separator($space-scale) list.separator($fonts);
    f: list.append($fonts, sans-serif);
    g: list.join($fonts, (Arial, sans-serif));
    h: list.join(4px 8px, 16px 24px, $separator: comma);
    i: list.separator(16px) list.separator((16px,));
    j: list.zip(color background-color, 150ms 300ms);
    k: list.slash(1, -1);
}
.list-demo {
  a: 0;
  b: 32px;
  c: 6;
  d: 4;
  e: space comma;
  f: "Inter", "Segoe UI", system-ui, sans-serif;
  g: "Inter", "Segoe UI", system-ui, Arial, sans-serif;
  h: 4px, 8px, 16px, 24px;
  i: space comma;
  j: color 150ms, background-color 300ms;
  k: 1 / -1;
}

Akılda tutulacak birkaç şey: list.nth() 1'den sayıyor, -1 son element. list.append() ve list.join() varsayılan olarak ilk list'in separator'ünü koruyor; farklı bir şey istiyorsan $separator ver. Tek bir değer de bir list (separator'ü space); tek elemanlı bir virgüllü list için sondaki virgül şart: (16px,). list.zip() iki list'i eşleştirip transition gibi virgüllü shorthand'ler üretiyor. list.slash() ise grid-column: 1 / -1 gibi eğik çizgili değerleri bölme sanılma riski olmadan kuruyor; /'ın 2.0'da ayraç olmasının list tarafı bu.

Index hataları derleme anında yakalanıyor:

Error: $n: List index may not be 0.
  ╷
3 │ .x { margin: list.nth($space-scale, 0); }
  │              ^^^^^^^^^^^^^^^^^^^^^^^^^
Error: $n: Invalid index 5 for a list with 4 elements.

Bookstore'da: rem(), space() ve font-stack()

bookstore-web'de üç function'ı src/styles/abstracts/_units.scss'e koydum ve abstracts/_index.scss'ten @forward ettim. Dosya yalnız değişken ve function içeriyor; abstracts'ın CSS üretmediği kuralı korunuyor (npx sass --load-path=src/styles src/styles/abstracts/_index.scss çıktısı 0 byte).

@use "sass:list";
@use "sass:math";

$root-font-size: 16px;

// spacing ölçeği: space(0) … space(5)
$space-scale: 0 4px 8px 16px 24px 32px;

$font-system: system-ui, "Segoe UI", Roboto, sans-serif;

@function rem($value) {
    @if math.is-unitless($value) {
        $value: $value * 1px;
    }
    @if not math.compatible($value, 1px) {
        @error "rem() expects px or a unitless number, got #{$value}";
    }
    @return math.div($value, $root-font-size) * 1rem;
}

// adımlar 0'dan, Sass list'leri 1'den başlar
@function space($step) {
    $last: list.length($space-scale) - 1;
    @if not math.is-unitless($step) or $step < 0 or $step > $last {
        @error "space() expects a step from 0 to #{$last}, got #{$step}";
    }
    @return list.nth($space-scale, $step + 1);
}

// önce verilen font'lar, sonra sistem font'ları; her zaman virgüllü
@function font-stack($fonts...) {
    @return list.join($fonts, $font-system, $separator: comma);
}

space()'in varlık sebebi off-by-one hatası: tasarım dokümanında ölçek 0'dan başlıyor, Sass list'i 1'den. Dönüşümü tek yerde yapınca kimse list.nth($space-scale, 2)'nin 4px mi 8px mi olduğunu düşünmek zorunda kalmıyor. font-stack() rest argüman ($fonts...) alıyor; parametresiz çağrılınca yalnız sistem font'larını döndürüyor. Kullanımı:

// base/_typography.scss
h1,
h2,
h3 {
    font-family: font-stack("Iowan Old Style", Georgia);
    line-height: 1.2;
}

// app/book-card/book-card.scss
.book-card__title {
    margin: 0 0 space(1);
    font-size: rem(18px);

    @include from("lg") {
        font-size: rem(20px);
    }
}

Build'den sonra kitap başlıklarını 400px ve 1280px genişlikte ölçtüm:

node scripts/web-probe.mjs ".book-card__title" font-size,font-family,margin-bottom light 400
Dune                     font-size=18px font-family="Iowan Old Style", Georgia, system-ui, "Segoe UI", Roboto, sans-serif margin-bottom=4px
...
Dune                     font-size=20px

Bir component'te yanlışlıkla space(6) yazınca ng build kendi mesajımızla duruyor:

✘ [ERROR] Error: "space() expects a step from 0 to 5, got 6"
   ╷
18 │         margin: 0 0 space(6);
   │                     ^^^^^^^^
   ╵
  ../../../../src/app/book-card/book-card.scss 18:21  root stylesheet [plugin angular-sass]

Global stil dosyası 10,65 kB'tan 10,74 kB'a çıktı; fark başlıklardaki yeni font listesi. npm run check:sass2 hâlâ 3 files, 0 failed diyor.

Üretimde Dikkat

$root-font-size'ın bir varsayım olduğunu unutma. rem() 16px'i kök boyutu kabul ediyor; bu, tarayıcının varsayılanı. Biri html { font-size: 62.5%; } gibi bir kural eklerse bütün rem değerleri küçülür ama Sass bunu bilmez. Kök font boyutunu değiştirmek yerine rem() kullanmak bu yüzden daha güvenli: kullanıcının tarayıcı ayarı korunuyor.

Ondalıkları kontrol et. Sass 10 basamak tutuyor; 33.3333333333% zararsız ama ölçek üreten bir döngüde yüzlerce uzun sayı derlenmiş CSS'i okumayı zorlaştırıyor. Yuvarlamayı function'ın içinde, tek yerde yap.

Build'i deterministik tut. string.unique-id() gibi her derlemede farklı çıktı üreten function'lar, içerik hash'li dosya adlarını ve CDN (Content Delivery Network; dosyaları kullanıcıya yakın sunuculardan dağıtan ağ) cache'ini bozuyor. Aynı kaynak her zaman aynı CSS'i üretmeli; bunu CI'da iki derlemenin diff'iyle kontrol etmek kolay.

Sık Düşülen Tuzaklar

Birimli iki sayıyı çarpmak. Yazının başındaki $base * 2px eskiden derleme hatasıydı, 1.96.0'dan beri tarayıcının attığı bir calc(32px * 1px). Çarpmalarda bir taraf her zaman birimsiz olmalı; birim eklemek için * 1px, birim kaldırmak için math.div($value, 1px).

List index'ini 0'dan saymak. JavaScript ve C# alışkanlığıyla list.nth($list, 0) yazmak derleme hatası veriyor; daha kötüsü list.nth($space-scale, 2)'nin hata vermeden ikinci elemanı, yani 4px'i döndürmesi. space() gibi bir sarmalayıcı bu karışıklığı tek yerde çözüyor.

Global isimleri kullanmaya devam etmek. nth() ve percentage() hâlâ çalışıyor ama her kullanım bir uyarı:

DEPRECATION WARNING [global-builtin]: Global built-in functions are deprecated and will be removed in Dart Sass 3.0.0.
Use list.nth instead.

More info and automated migrator: https://sass-lang.com/d/import

  ╷
4 │     padding: nth($space-scale, 3);
  │              ^^^^^^^^^^^^^^^^^^^^

Bu uyarılar Dart Sass 2.0'da da uyarı olarak kalıyor ama 3.0'da kaldırılacak. sass-migrator module bunları list.nth() ve math.percentage()'a çeviriyor; yeni kodda baştan module'lü isimleri kullan.

Escape'i unutmak. .w-1/2 gibi bir class adı derleme hatası veriyor, bu iyi; ama interpolation ile üretilen bir isimde escape eksikse ve karakter selector'de başka bir anlama geliyorsa (. ya da : gibi) derleme geçiyor ve kural yanlış element'leri hedefliyor.

Küçük Bir Deneme

★☆☆ 08/type-scale.scss'te oranı 1.2 yap ve $steps'e 4 ekle. round-to()'ya $digits: 2 verdiğinde hangi iki adımın aynı değere yuvarlandığını kontrol et.

★★☆ rem()'e ikinci bir parametre ekle: $unit: rem. em verildiğinde sonucu em olarak döndürsün, başka bir değerde @error versin. rem(12px, em)'in 0.75em ürettiğini derleyerek doğrula.

★★★ bookstore-web'de $space-scale ile custom property üreten bir mixin yaz: :root altında --space-0 … --space-5. Sonra space()'e $var: true parametresi ekle ve var(--space-2) döndürsün. Hangi durumda derleme anında değer, hangi durumda custom property döndürmenin daha doğru olduğunu yaz.

Bir sonraki yazıda built-in module'lerin ikinci yarısına geçiyoruz: sass:map ile iç içe token map'leri ve deep-merge, sass:color ile color space farkında renk türetme ve Bookstore'un kurumsal portal teması.