kodingup.camp

Beranda/HTML, CSS & SCSS Mastery

Pengembangan Antarmuka Adaptif dengan Sass

Modul 10 dari 114 menit bacaAdaptive Interface Development

Isi modul

    Kenapa ini penting

    Di modul sebelumnya kamu menyimpan nilai dalam variabel dan map. Sekarang kamu akan membuat Sass bekerja untukmu: menulis pola sekali lalu memakainya berkali-kali. Contohnya, media query mobile-first yang tadinya ditulis manual di 30 tempat cukup ditulis @include naik(md) { ... }. Puluhan class utility seperti .mt-1 sampai .mt-8 atau .teks-utama, .teks-bahaya bisa dihasilkan dari satu loop. Ibaratnya, kamu berhenti memotong kue satu per satu dengan pisau, lalu mulai memakai cetakan.

    Konsep inti

    Mixin dan @include

    Mixin adalah blok deklarasi yang bisa dipakai ulang, dan bisa menerima argumen:

    @mixin tombol-varian($latar, $teks: white) {
      color: $teks;
      background: $latar;
    }
    
    .tombol-utama { @include tombol-varian(#2563eb); }
    .tombol-kuning { @include tombol-varian(#facc15, #1e293b); }
    

    Mixin breakpoint

    Gabungan map breakpoint, @content, dan @error menghasilkan alat responsif yang aman:

    @use "sass:map";
    
    $breakpoints: ("sm": 40rem, "md": 48rem, "lg": 64rem);
    
    @mixin naik($nama) {
      @if not map.has-key($breakpoints, $nama) {
        @error "Breakpoint '#{$nama}' tidak dikenal. Pilihan: #{map.keys($breakpoints)}";
      }
      @media (min-width: map.get($breakpoints, $nama)) {
        @content;
      }
    }
    
    .grid {
      display: grid;
      @include naik(md) { grid-template-columns: 1fr 1fr; }
    }
    

    Kalau ada yang salah ketik @include naik(tablet), compile langsung gagal dengan pesan yang jelas, bukan diam-diam menghasilkan CSS yang salah. #{...} disebut interpolasi, yaitu cara menyisipkan nilai Sass ke dalam string, nama selector, atau nama properti.

    Function

    Fungsi Sass mengembalikan sebuah nilai (bukan blok deklarasi) dengan @return:

    @use "sass:math";
    
    @function rem($px, $dasar: 16px) {
      @return math.div($px, $dasar) * 1rem;
    }
    
    h1 { font-size: rem(40px); } // → font-size: 2.5rem;
    

    Perhatikan math.div(): pembagian dengan tanda / di Sass sudah deprecated karena / juga dipakai sebagai pemisah di CSS (misalnya grid-area: 1 / 3). Modul sass:color menyediakan fungsi warna seperti color.adjust($warna, $lightness: -10%) dan color.scale($warna, $lightness: 30%) untuk membuat variasi lebih gelap atau terang.

    Mixin vs function: mixin menghasilkan deklarasi CSS (beberapa baris), function menghasilkan satu nilai. Kalau kamu ingin menulis properti: fungsi(...), itu function. Kalau ingin menyisipkan sekumpulan baris, itu mixin.

    @extend dan placeholder

    @extend membuat satu selector "mewarisi" semua gaya selector lain. Sass menggabungkan selector-selector itu dalam satu aturan:

    %teks-terpotong {
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
    }
    .kartu__judul { @extend %teks-terpotong; }
    .tabel__sel   { @extend %teks-terpotong; }
    // → .tabel__sel, .kartu__judul { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
    

    Selector yang diawali % disebut placeholder: ia tidak pernah muncul di CSS hasil kecuali di-extend. Batasan dan risiko @extend:

    Aturan praktis: pakai mixin sebagai pilihan utama; pakai @extend dengan placeholder hanya untuk gaya yang benar-benar identik dan statis. Banyak tim memilih tidak memakai @extend sama sekali.

    Kontrol alur: @if, @each, @for

    Kombinasi loop dan interpolasi adalah cara menghasilkan utility class secara otomatis.

    Contoh kode

    src/scss/abstracts/_mixins.scss:

    @use "sass:map";
    @use "sass:math";
    @use "tokens";
    
    // Konversi px ke rem
    @function rem($px, $dasar: 16px) {
      @return math.div($px, $dasar) * 1rem;
    }
    
    // Mobile-first: berlaku mulai breakpoint ke atas
    @mixin naik($nama) {
      @if not map.has-key(tokens.$breakpoints, $nama) {
        @error "Breakpoint '#{$nama}' tidak dikenal. Pilihan: #{map.keys(tokens.$breakpoints)}";
      }
      @media (min-width: map.get(tokens.$breakpoints, $nama)) {
        @content;
      }
    }
    
    // Fokus yang konsisten di semua elemen interaktif
    @mixin cincin-fokus($warna: map.get(tokens.$warna, "utama")) {
      &:focus-visible {
        outline: 3px solid $warna;
        outline-offset: 2px;
      }
    }
    

    src/scss/abstracts/_tokens.scss:

    $warna: (
      "utama": #2563eb,
      "sukses": #16a34a,
      "bahaya": #dc2626,
      "netral": #64748b,
    ) !default;
    
    $breakpoints: (
      "sm": 40rem,
      "md": 48rem,
      "lg": 64rem,
    ) !default;
    

    src/scss/abstracts/_index.scss:

    @forward "tokens";
    @forward "mixins";
    

    src/scss/main.scss:

    @use "sass:color";
    @use "abstracts" as a;
    
    // ===== Komponen tombol: varian dihasilkan dari map warna =====
    .tombol {
      display: inline-block;
      padding: a.rem(12px) a.rem(24px);
      font-size: a.rem(16px);
      color: white;
      border: none;
      border-radius: a.rem(8px);
      @include a.cincin-fokus;
    
      @each $nama, $nilai in a.$warna {
        &--#{$nama} {
          background: $nilai;
    
          &:hover {
            background: color.adjust($nilai, $lightness: -8%);
          }
        }
      }
    }
    
    // ===== Utility spacing: .mt-1 ... .mt-4 =====
    @for $i from 1 through 4 {
      .mt-#{$i} { margin-top: $i * 0.25rem; }
    }
    
    // ===== Utility warna teks =====
    @each $nama, $nilai in a.$warna {
      .teks-#{$nama} { color: $nilai; }
    }
    
    // ===== Layout responsif dengan mixin breakpoint =====
    .grid-produk {
      display: grid;
      gap: 1rem;
    
      @include a.naik(sm) { grid-template-columns: repeat(2, 1fr); }
      @include a.naik(lg) { grid-template-columns: repeat(4, 1fr); }
    }
    
    // ===== Placeholder untuk gaya identik =====
    %teks-terpotong {
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
    }
    .kartu__judul { @extend %teks-terpotong; }
    .tabel__sel { @extend %teks-terpotong; }
    

    Potongan hasil compile:

    .tombol {
      display: inline-block;
      padding: 0.75rem 1.5rem;
      font-size: 1rem;
      color: white;
      border: none;
      border-radius: 0.5rem;
    }
    .tombol:focus-visible {
      outline: 3px solid #2563eb;
      outline-offset: 2px;
    }
    .tombol--utama {
      background: #2563eb;
    }
    .tombol--utama:hover {
      background: rgb(7.619047619%, 31.2380952381%, 83.0476190476%);
    }
    /* ... .tombol--sukses, .tombol--bahaya, .tombol--netral dengan pola sama ... */
    
    .mt-1 { margin-top: 0.25rem; }
    .mt-2 { margin-top: 0.5rem; }
    .mt-3 { margin-top: 0.75rem; }
    .mt-4 { margin-top: 1rem; }
    
    .teks-utama { color: #2563eb; }
    /* ... */
    
    .grid-produk {
      display: grid;
      gap: 1rem;
    }
    @media (min-width: 40rem) {
      .grid-produk { grid-template-columns: repeat(2, 1fr); }
    }
    @media (min-width: 64rem) {
      .grid-produk { grid-template-columns: repeat(4, 1fr); }
    }
    
    /* urutan selector hasil @extend bisa tidak sama dengan urutan kamu menulisnya */
    .tabel__sel, .kartu__judul {
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
    }
    /* → Dari sekitar 60 baris SCSS dihasilkan puluhan aturan CSS yang konsisten.
         Tambah satu warna ke map $warna → otomatis lahir .tombol--baru dan .teks-baru. */
    

    Hasil color.adjust pada Dart Sass 1.106.1 berupa rgb() dengan persentase desimal seperti di atas. Itu normal dan tetap valid di browser. Format persisnya bisa berbeda antar versi Sass, jadi jangan heran kalau angkanya tidak sama persis.

    Kesalahan umum

    1. Memakai @extend di dalam @media untuk selector di luar media itu. Sass akan error. Cara benar: pakai mixin.
    2. Membagi dengan /, misalnya $a / 2. Sass menampilkan peringatan deprecation. Cara benar: math.div($a, 2).
    3. Lupa interpolasi saat membuat nama class: .mt-$i tidak akan bekerja. Cara benar: .mt-#{$i}.
    4. Menghasilkan ribuan utility yang tidak dipakai lewat loop besar. File CSS membengkak. Cara benar: buat hanya skala yang dipakai desain (misalnya 4 sampai 8 langkah).
    5. Memakai mixin untuk menghasilkan nilai tunggal. Cara benar: kalau hasilnya satu nilai untuk satu properti, buat @function.

    Latihan

    1. Mudah, Buat mixin flex-tengah yang menghasilkan display: flex; justify-content: center; align-items: center;, lalu pakai di .hero.
    2. Sedang, Buat function spasi($n) yang mengembalikan $n * 0.25rem, lalu loop @for untuk membuat .p-1 sampai .p-6 memakai fungsi itu.
    3. Menantang, Buat mixin turun($nama) (kebalikan naik, memakai max-width dengan nilai breakpoint dikurangi 0.02rem) lengkap dengan @error untuk nama tak dikenal. Pakai untuk menyembunyikan .sidebar di bawah breakpoint md.
    Lihat solusi
    // Soal 1
    @mixin flex-tengah {
      display: flex;
      justify-content: center;
      align-items: center;
    }
    .hero {
      min-height: 60vh;
      @include flex-tengah;
    }
    
    // Soal 2
    @function spasi($n) {
      @return $n * 0.25rem;
    }
    @for $i from 1 through 6 {
      .p-#{$i} { padding: spasi($i); }
    }
    // → .p-1 { padding: 0.25rem; } ... .p-6 { padding: 1.5rem; }
    
    // Soal 3
    @use "sass:map";
    
    $breakpoints: ("sm": 40rem, "md": 48rem, "lg": 64rem);
    
    @mixin turun($nama) {
      @if not map.has-key($breakpoints, $nama) {
        @error "Breakpoint '#{$nama}' tidak dikenal.";
      }
      // Dikurangi sedikit supaya tidak bertabrakan dengan min-width pada nilai yang sama
      @media (max-width: map.get($breakpoints, $nama) - 0.02rem) {
        @content;
      }
    }
    
    .sidebar {
      @include turun(md) { display: none; }
    }
    // → @media (max-width: 47.98rem) { .sidebar { display: none; } }
    

    Ringkasan

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.