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); }
- Argumen boleh punya nilai default (
$teks: white). - Argumen bisa dipanggil dengan nama:
@include tombol-varian($teks: black, $latar: yellow);. @contentmembuat mixin bisa menerima blok gaya dari pemanggilnya. Inilah dasar mixin breakpoint.
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:
- Tidak bisa meng-extend selector dari luar
@mediake dalam@media. - Bisa menghasilkan daftar selector panjang dan urutan yang mengejutkan di file besar.
@extendbekerja di seluruh modul yang termuat, sehingga efeknya bisa terasa "dari jauh".
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
@if/@else if/@elseuntuk percabangan.@each $kunci, $nilai in $map { ... }mengulang setiap pasangan di map.@for $i from 1 through 5 { ... }mengulang angka 1 sampai 5 (throughtermasuk angka akhir,totidak).@whileada, tetapi jarang dibutuhkan.
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
- Memakai
@extenddi dalam@mediauntuk selector di luar media itu. Sass akan error. Cara benar: pakai mixin. - Membagi dengan
/, misalnya$a / 2. Sass menampilkan peringatan deprecation. Cara benar:math.div($a, 2). - Lupa interpolasi saat membuat nama class:
.mt-$itidak akan bekerja. Cara benar:.mt-#{$i}. - 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).
- Memakai mixin untuk menghasilkan nilai tunggal. Cara benar: kalau hasilnya satu nilai untuk satu properti, buat
@function.
Latihan
- Mudah, Buat mixin
flex-tengahyang menghasilkandisplay: flex; justify-content: center; align-items: center;, lalu pakai di.hero. - Sedang, Buat function
spasi($n)yang mengembalikan$n * 0.25rem, lalu loop@foruntuk membuat.p-1sampai.p-6memakai fungsi itu. - Menantang, Buat mixin
turun($nama)(kebalikannaik, memakaimax-widthdengan nilai breakpoint dikurangi 0.02rem) lengkap dengan@erroruntuk nama tak dikenal. Pakai untuk menyembunyikan.sidebardi bawah breakpointmd.
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
- Mixin (
@mixin+@include) menyisipkan blok deklarasi; bisa menerima argumen dan@content. - Mixin breakpoint berbasis map dan
@errormembuat media query konsisten dan aman dari salah ketik. - Function (
@function+@return) menghasilkan satu nilai; pakaimath.divuntuk pembagian dansass:coloruntuk variasi warna. @extenddengan placeholder%menggabungkan selector, tetapi punya batasan; utamakan mixin.@eachdan@fordengan interpolasi#{}menghasilkan varian komponen dan utility class dari satu sumber data.