Bài 19: CSS Preprocessor – SCSS/SASS cơ bản

CSS vanilla (thuần) có một vấn đề: nó lặp lại nhiều. Mỗi khi đổi màu primary, bạn phải tìm và thay đổi ở 20 chỗ. Mỗi lần tạo button, bạn lại viết 10 dòng CSS giống nhau.

SCSS/SASS (CSS Preprocessor) giải quyết vấn đề này bằng cách thêm các tính năng lập trình vào CSS: variables, functions, mixins, nesting… Sau đó, nó tự động compile thành CSS thuần để trình duyệt hiểu được.

Bài này sẽ dạy bạn cách dùng SCSS để viết CSS DRY (Don’t Repeat Yourself), dễ bảo trì, và mạnh mẽ hơn.

SCSS vs CSS vs SASS

Tính năngCSSSASSSCSS
SyntaxCSS tiêu chuẩnKhông cần braces {}Giống CSS
Variables❌ Không✅ Có✅ Có
Nesting❌ Không✅ Có✅ Có
Mixins❌ Không✅ Có✅ Có
Functions❌ Không✅ Có✅ Có
File extension.css.sass.scss
Sử dụng phổ biếnCơ bảnÍt hơn✅ Phổ biến nhất

💡 Khuyến khích: Dùng SCSS vì nó có syntax giống CSS (dễ học) nhưng có tất cả tính năng của SASS.

Setup SCSS – Cài đặt

Cách 1: Online Compiler (Nhanh nhất)

Truy cập https://sassmeister.com hoặc https://jsoncrack.com/scss để test SCSS online mà không cần cài đặt.

Cách 2: Node-sass (Local)

# Cài đặt node-sass
npm install -g node-sass

# Hoặc dùng sass package (mới hơn)
npm install -g sass

# Compile .scss thành .css
scss input.scss output.css

# Watch mode: tự động compile khi file thay đổi
scss --watch . --poll

Cách 3: VS Code Extension

Cài đặt extension “Live Sass Compiler” trong VS Code → click “Watch Sass” → tự động compile.

Variables – Biến trong SCSS

Variables lưu giữ giá trị để tái sử dụng. Khác với CSS custom properties, SCSS variables compile-time (thay thế khi compile).

/* ========== VARIABLES ========== */

// Colors
$color-primary: #3498db;
$color-secondary: #2c3e50;
$color-error: #e74c3c;
$color-success: #27ae60;

// Spacing
$spacing-xs: 8px;
$spacing-sm: 12px;
$spacing-md: 20px;
$spacing-lg: 40px;

// Font sizes
$font-size-sm: 14px;
$font-size-base: 16px;
$font-size-lg: 20px;
$font-size-xl: 28px;

// Border radius
$border-radius: 8px;

// Shadows
$shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
$shadow-md: 0 4px 12px rgba(0, 0, 0, 0.15);
$shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.2);

/* ========== USAGE ========== */

body {
  color: $color-primary;
  font-size: $font-size-base;
}

.button {
  background-color: $color-primary;
  padding: $spacing-sm $spacing-md;
  border-radius: $border-radius;
  box-shadow: $shadow-sm;
}

.button--error {
  background-color: $color-error;
}

.container {
  padding: $spacing-lg;
}

h1 {
  font-size: $font-size-xl;
  margin-bottom: $spacing-md;
}

Lợi ích Variables

  • DRY: Giá trị lưu một nơi, dùng ở nhiều nơi
  • Dễ bảo trì: Đổi 1 variable = đổi tất cả
  • Dễ đọc: $color-primary dễ hiểu hơn #3498db
  • Consistent: Đảm bảo tính nhất quán design

Nesting – Lồng selectors

Nesting cho phép bạn lồng CSS selectors, giảm lặp lại parent selector.

/* ❌ CSS (lặp lại .card nhiều lần) */
.card {
  background: white;
  border-radius: 8px;
}

.card:hover {
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
}

.card__title {
  font-size: 20px;
  margin-bottom: 10px;
}

.card__description {
  color: #666;
}

/* ✅ SCSS (clean, không lặp lại) */
.card {
  background: white;
  border-radius: 8px;

  &:hover {
    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
  }

  &__title {
    font-size: 20px;
    margin-bottom: 10px;
  }

  &__description {
    color: #666;
  }
}

Ampersand (&) – Tham chiếu parent selector

.button {
  padding: 10px 20px;
  background-color: $color-primary;

  /* &:hover = .button:hover */
  &:hover {
    background-color: darken($color-primary, 10%);
  }

  /* &:focus = .button:focus */
  &:focus {
    outline: 2px solid $color-primary;
  }

  /* &--secondary = .button--secondary */
  &--secondary {
    background-color: $color-secondary;
  }

  /* &__text = .button__text */
  &__text {
    font-weight: bold;
  }

  /* .icon & = .icon .button */
  .icon & {
    padding: 8px 16px;
  }
}

Compile result (CSS)

/* Kết quả sau khi compile từ SCSS trên */
.button {
  padding: 10px 20px;
  background-color: #3498db;
}

.button:hover {
  background-color: #2980b9;
}

.button:focus {
  outline: 2px solid #3498db;
}

.button--secondary {
  background-color: #2c3e50;
}

.button__text {
  font-weight: bold;
}

.icon .button {
  padding: 8px 16px;
}

Mixins – Tái sử dụng code

Mixins là CSS blocks mà bạn có thể tái sử dụng. Giống như functions.

/* ========== DEFINE MIXIN ========== */

// Flexbox centering
@mixin flex-center {
  display: flex;
  justify-content: center;
  align-items: center;
}

// Responsive text (clamp)
@mixin responsive-text($min, $max) {
  font-size: clamp($min, 5vw, $max);
}

// Button styles
@mixin button-base {
  padding: 10px 20px;
  border: none;
  border-radius: $border-radius;
  cursor: pointer;
  font-weight: bold;
  transition: all 0.3s;
}

// Button variant
@mixin button-variant($bg-color) {
  background-color: $bg-color;
  color: white;

  &:hover {
    background-color: darken($bg-color, 10%);
  }
}

// Truncate text
@mixin truncate {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

// Shadow
@mixin shadow($level: 'md') {
  @if $level == 'sm' {
    box-shadow: $shadow-sm;
  } @else if $level == 'md' {
    box-shadow: $shadow-md;
  } @else {
    box-shadow: $shadow-lg;
  }
}

/* ========== USE MIXIN ========== */

.flex-container {
  @include flex-center;
  height: 200px;
}

.heading {
  @include responsive-text(24px, 48px);
}

.button {
  @include button-base;
  @include button-variant($color-primary);
}

.button--secondary {
  @include button-base;
  @include button-variant($color-secondary);
}

.button--error {
  @include button-base;
  @include button-variant($color-error);
}

.card-title {
  @include truncate;
}

.card {
  @include shadow('md');
}

Ví dụ hoàn chỉnh – Component Button with SCSS

/* ========== VARIABLES ========== */

$colors: (
  'primary': #3498db,
  'secondary': #2c3e50,
  'error': #e74c3c,
  'success': #27ae60,
);

$sizes: (
  'sm': (padding: 8px 16px, font-size: 14px),
  'md': (padding: 12px 20px, font-size: 16px),
  'lg': (padding: 16px 28px, font-size: 18px),
);

$spacing-base: 20px;
$border-radius: 8px;

/* ========== MIXINS ========== */

@mixin button-base {
  border: none;
  border-radius: $border-radius;
  cursor: pointer;
  font-weight: 600;
  transition: all 0.3s ease;
  
  &:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  }

  &:active {
    transform: translateY(0);
  }

  &:disabled {
    opacity: 0.6;
    cursor: not-allowed;
    transform: none;
  }
}

@mixin button-color($color) {
  background-color: $color;
  color: white;

  &:hover:not(:disabled) {
    background-color: darken($color, 10%);
  }
}

@mixin button-size($size) {
  $padding: map-get($size, 'padding');
  $font-size: map-get($size, 'font-size');
  
  padding: $padding;
  font-size: $font-size;
}

/* ========== COMPONENT ========== */

.button {
  @include button-base;
  @include button-color(map-get($colors, 'primary'));
  @include button-size(map-get($sizes, 'md'));

  // Variants
  &--secondary {
    @include button-color(map-get($colors, 'secondary'));
  }

  &--error {
    @include button-color(map-get($colors, 'error'));
  }

  &--success {
    @include button-color(map-get($colors, 'success'));
  }

  // Sizes
  &--sm {
    @include button-size(map-get($sizes, 'sm'));
  }

  &--lg {
    @include button-size(map-get($sizes, 'lg'));
  }

  // Outline style
  &--outline {
    background-color: transparent;
    border: 2px solid map-get($colors, 'primary');
    color: map-get($colors, 'primary');

    &:hover:not(:disabled) {
      background-color: map-get($colors, 'primary');
      color: white;
    }
  }
}

Functions – Hàm trong SCSS

/* ========== FUNCTIONS ========== */

// Calculate REM from PX
@function rem($pixels) {
  @return $pixels / 16 * 1rem;
}

// Lighten color
@function lighter($color, $amount: 10%) {
  @return lighten($color, $amount);
}

// Get color from map
@function color($name) {
  @return map-get($colors, $name);
}

/* ========== USAGE ========== */

body {
  font-size: rem(16);
  margin: rem(20);
}

.heading {
  color: color('primary');
  font-size: rem(28);
}

.text-light {
  color: lighter(color('secondary'), 20%);
}

Import & Organize – Chia file SCSS

/* ========== main.scss ========== */

// 1. Variables & Functions
@import 'variables';
@import 'functions';

// 2. Mixins
@import 'mixins';

// 3. Base/Reset
@import 'base';
@import 'typography';

// 4. Layout
@import 'layout/header';
@import 'layout/footer';
@import 'layout/container';

// 5. Components
@import 'components/button';
@import 'components/card';
@import 'components/form';

// 6. Utilities
@import 'utilities';

File structure:

scss/
├── main.scss              # Main file (imports all)
├── _variables.scss        # Colors, spacing, fonts
├── _functions.scss        # Reusable functions
├── _mixins.scss          # Reusable mixins
├── _base.scss            # Reset, global styles
├── _typography.scss      # Font, heading styles
├── layout/
│   ├── _header.scss
│   ├── _footer.scss
│   └── _container.scss
├── components/
│   ├── _button.scss
│   ├── _card.scss
│   └── _form.scss
└── _utilities.scss       # Utility classes

💡 Naming convention: Tên file SCSS bắt đầu với underscore (_) khi import (ví dụ: _variables.scss). File chính không có underscore (main.scss).

Advanced Features

1. For Loop

/* Generate margin utilities */
@for $i from 1 through 5 {
  .m-#{$i} {
    margin: $i * 10px;
  }
}

/* Generates: .m-1, .m-2, .m-3, ... */

2. Each Loop (Iterate over map)

$colors: (
  'primary': #3498db,
  'secondary': #2c3e50,
  'error': #e74c3c,
);

@each $name, $color in $colors {
  .bg-#{$name} {
    background-color: $color;
  }
}

/* Generates: .bg-primary, .bg-secondary, .bg-error */

3. If/Else Logic

@mixin responsive-padding($size) {
  @if $size == 'mobile' {
    padding: 10px;
  } @else if $size == 'tablet' {
    padding: 20px;
  } @else {
    padding: 40px;
  }
}

SCSS vs CSS Custom Properties

Tính năngSCSS VariablesCSS Custom Properties
Syntax$color-primaryvar(--color-primary)
Compile timeThay thế khi compileRuntime (trong browser)
ScopeLexical scopeCSS scope (cascade)
DynamicStaticCó thể thay đổi via JS
MathHỗ trợCần calc()
Khi dùngBuild-time configurationDynamic theming
/* Dùng cả hai kết hợp */

// SCSS variables (static)
$spacing-unit: 8px;

:root {
  // CSS variables (dynamic)
  --color-primary: #3498db;
  --color-secondary: #2c3e50;
}

.button {
  // SCSS variable cho spacing
  padding: $spacing-unit * 2;
  
  // CSS variable cho color
  color: var(--color-primary);
}

Tóm tắt

  • SCSS/SASS: CSS Preprocessor với variables, mixins, functions
  • Variables: $color-primary, $spacing-md…
  • Nesting: Lồng selectors, dùng & để tham chiếu parent
  • Mixins: @mixin + @include để tái sử dụng code
  • Functions: Hàm tính toán, lighten, darken…
  • Loops: @for, @each để generate code
  • Organization: Import files theo phần: variables, mixins, layout, components…
  • DRY: Viết ít hơn, maintain dễ hơn
  • Compile: SCSS → CSS (trình duyệt dùng CSS)

🎯 Bài tập thực hành

Bài tập 1: Chuyển đổi CSS vanilla sang SCSS:

  • Lấy file CSS từ dự án cũ
  • Tạo variables cho colors, spacing, font-sizes
  • Chuyển CSS thành SCSS với nesting
  • Compile SCSS → CSS
  • Kiểm tra output CSS đúng không

Bài tập 2: Tạo component library với SCSS:

  • Tạo _variables.scss (colors, spacing, sizes)
  • Tạo _mixins.scss (button-base, button-color…)
  • Tạo components/_button.scss (tất cả button variants)
  • Tạo components/_card.scss (card styles)
  • Tạo main.scss import tất cả
  • Test: compile SCSS, kiểm tra output CSS

Bài tập 3: Tạo responsive utility classes với SCSS loops:

  • Dùng @for loop generate spacing utilities (.m-1, .m-2…)
  • Dùng @each loop generate color utilities (.bg-primary, .bg-secondary…)
  • Dùng @for + media query generate responsive padding
  • Compile → CSS → test trên HTML

Chúc bạn thành công! 🚀

Để lại bình luận

Email của bạn sẽ không được hiển thị.