SASS/SCSS

Building a Component Library with SASS

25 min Lesson 22 of 30

Building a Component Library with SASS

A component library is a collection of reusable UI elements with consistent styling, behavior, and documentation. Building your own component library with SASS allows you to create a unified design system that scales across your entire application. In this comprehensive lesson, we'll explore how to architect, design, and implement a professional component library using SASS's advanced features.

Component-Based Architecture

Component-based architecture is a design approach where the UI is broken down into independent, reusable pieces. Each component encapsulates its own styling, structure, and sometimes behavior.

Benefits of Component-Based Design

  • Reusability: Write once, use everywhere across your application
  • Consistency: Ensures visual and functional consistency across pages
  • Maintainability: Changes to a component automatically apply everywhere it's used
  • Scalability: Easy to extend with new variants and states
  • Collaboration: Teams can work on different components independently
  • Testing: Isolated components are easier to test
  • Documentation: Components serve as living documentation
Note: A well-designed component library is the foundation of any design system. It bridges the gap between design and development, ensuring that what designers envision is exactly what developers build.

Component Library Structure

Let's establish a clear file structure for our component library:

Component Library File Structure

scss/
├── abstracts/
│   ├── _variables.scss        // Design tokens
│   ├── _mixins.scss           // Component mixins
│   └── _functions.scss        // Helper functions
├── base/
│   ├── _reset.scss            // CSS reset
│   └── _typography.scss       // Base typography
├── components/
│   ├── _buttons.scss          // Button components
│   ├── _cards.scss            // Card components
│   ├── _forms.scss            // Form components
│   ├── _alerts.scss           // Alert components
│   ├── _modals.scss           // Modal components
│   ├── _navs.scss             // Navigation components
│   └── _badges.scss           // Badge components
└── main.scss                  // Import manifest

Design Tokens (Variables)

Design tokens are the foundation of your component library. They define colors, spacing, typography, and other design decisions:

Design Token Configuration

// abstracts/_variables.scss

// Color System
$primary: #007bff;
$secondary: #6c757d;
$success: #28a745;
$danger: #dc3545;
$warning: #ffc107;
$info: #17a2b8;
$light: #f8f9fa;
$dark: #343a40;

// Color variants (lighter/darker shades)
$primary-light: lighten($primary, 10%);
$primary-dark: darken($primary, 10%);
$primary-lightest: lighten($primary, 30%);

// Spacing scale
$spacer: 1rem;
$spacers: (
    0: 0,
    1: $spacer * 0.25,   // 4px
    2: $spacer * 0.5,    // 8px
    3: $spacer,          // 16px
    4: $spacer * 1.5,    // 24px
    5: $spacer * 3,      // 48px
);

// Typography scale
$font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
$font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, monospace;

$font-size-base: 1rem;
$font-sizes: (
    xs: $font-size-base * 0.75,   // 12px
    sm: $font-size-base * 0.875,  // 14px
    md: $font-size-base,          // 16px
    lg: $font-size-base * 1.25,   // 20px
    xl: $font-size-base * 1.5,    // 24px
);

// Border radius
$border-radius: 0.25rem;
$border-radius-sm: 0.2rem;
$border-radius-lg: 0.3rem;
$border-radius-pill: 50rem;

// Shadows
$shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
$shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
$shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);

// Transitions
$transition-base: all 0.2s ease-in-out;
$transition-fast: all 0.1s ease-in-out;
$transition-slow: all 0.3s ease-in-out;

Building Button Components

Buttons are one of the most fundamental components. Let's build a comprehensive button system:

Button Base Styles

// components/_buttons.scss

// Base button styles
.btn {
    display: inline-block;
    font-family: $font-family-base;
    font-weight: 400;
    text-align: center;
    white-space: nowrap;
    vertical-align: middle;
    user-select: none;
    border: 1px solid transparent;
    padding: 0.375rem 0.75rem;
    font-size: $font-size-base;
    line-height: 1.5;
    border-radius: $border-radius;
    transition: $transition-base;
    cursor: pointer;
    text-decoration: none;

    &:hover {
        text-decoration: none;
    }

    &:focus {
        outline: 0;
        box-shadow: 0 0 0 0.2rem rgba($primary, 0.25);
    }

    &:disabled,
    &.disabled {
        opacity: 0.65;
        cursor: not-allowed;
        pointer-events: none;
    }
}

Button Variants Using Maps

Instead of writing each button variant manually, we use SASS maps to generate them automatically:

Button Variant Generator

// Button color map
$button-colors: (
    "primary": $primary,
    "secondary": $secondary,
    "success": $success,
    "danger": $danger,
    "warning": $warning,
    "info": $info,
    "light": $light,
    "dark": $dark
);

// Mixin to generate button variants
@mixin button-variant($background, $border: $background) {
    background-color: $background;
    border-color: $border;
    color: color-contrast($background);

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

    &:focus,
    &.focus {
        box-shadow: 0 0 0 0.2rem rgba($background, 0.5);
    }

    &:active,
    &.active {
        background-color: darken($background, 10%);
        border-color: darken($border, 12.5%);
    }
}

// Generate button variants
@each $name, $color in $button-colors {
    .btn-#{$name} {
        @include button-variant($color);
    }
}

// This generates:
// .btn-primary, .btn-secondary, .btn-success, etc.

Button Sizes

Button Size Variants

// Button sizes using map
$button-sizes: (
    "sm": (
        padding: 0.25rem 0.5rem,
        font-size: map-get($font-sizes, sm),
        border-radius: $border-radius-sm
    ),
    "md": (
        padding: 0.375rem 0.75rem,
        font-size: $font-size-base,
        border-radius: $border-radius
    ),
    "lg": (
        padding: 0.5rem 1rem,
        font-size: map-get($font-sizes, lg),
        border-radius: $border-radius-lg
    )
);

// Generate size classes
@each $size, $properties in $button-sizes {
    .btn-#{$size} {
        padding: map-get($properties, padding);
        font-size: map-get($properties, font-size);
        border-radius: map-get($properties, border-radius);
    }
}

Outline and Ghost Button Variants

Outline Button Styles

// Outline button mixin
@mixin button-outline-variant($color) {
    color: $color;
    background-color: transparent;
    border-color: $color;

    &:hover {
        color: color-contrast($color);
        background-color: $color;
        border-color: $color;
    }

    &:focus,
    &.focus {
        box-shadow: 0 0 0 0.2rem rgba($color, 0.5);
    }
}

// Generate outline variants
@each $name, $color in $button-colors {
    .btn-outline-#{$name} {
        @include button-outline-variant($color);
    }
}

// Block button (full width)
.btn-block {
    display: block;
    width: 100%;

    + .btn-block {
        margin-top: 0.5rem;
    }
}

Building Card Components

Cards are versatile content containers. Let's build a flexible card component system:

Card Component Structure

// components/_cards.scss

.card {
    position: relative;
    display: flex;
    flex-direction: column;
    min-width: 0;
    word-wrap: break-word;
    background-color: #fff;
    background-clip: border-box;
    border: 1px solid rgba(0, 0, 0, 0.125);
    border-radius: $border-radius;
    box-shadow: $shadow-sm;

    // Card header
    &__header {
        padding: 0.75rem 1.25rem;
        margin-bottom: 0;
        background-color: rgba(0, 0, 0, 0.03);
        border-bottom: 1px solid rgba(0, 0, 0, 0.125);
        border-radius: $border-radius $border-radius 0 0;

        &:first-child {
            border-radius: $border-radius $border-radius 0 0;
        }
    }

    // Card body
    &__body {
        flex: 1 1 auto;
        padding: 1.25rem;
    }

    // Card title
    &__title {
        margin-bottom: 0.75rem;
        font-size: 1.25rem;
        font-weight: 500;
    }

    // Card subtitle
    &__subtitle {
        margin-top: -0.375rem;
        margin-bottom: 0;
        color: #6c757d;
    }

    // Card text
    &__text {
        &:last-child {
            margin-bottom: 0;
        }
    }

    // Card footer
    &__footer {
        padding: 0.75rem 1.25rem;
        background-color: rgba(0, 0, 0, 0.03);
        border-top: 1px solid rgba(0, 0, 0, 0.125);

        &:last-child {
            border-radius: 0 0 $border-radius $border-radius;
        }
    }

    // Card image
    &__img {
        width: 100%;
        border-radius: $border-radius $border-radius 0 0;

        &--bottom {
            border-radius: 0 0 $border-radius $border-radius;
        }
    }

    // Card image overlay
    &__img-overlay {
        position: absolute;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        padding: 1.25rem;
        border-radius: $border-radius;
    }
}

Card Variants and Modifiers

Card Color Variants

// Card color variants
@each $name, $color in $button-colors {
    .card-#{$name} {
        background-color: $color;
        border-color: $color;
        color: color-contrast($color);

        .card__header,
        .card__footer {
            background-color: darken($color, 5%);
            border-color: darken($color, 10%);
        }
    }
}

// Horizontal card layout
.card-horizontal {
    flex-direction: row;

    .card__img {
        border-radius: $border-radius 0 0 $border-radius;
        max-width: 40%;
        object-fit: cover;
    }
}

// Card groups
.card-group {
    display: flex;
    flex-flow: row wrap;

    .card {
        flex: 1 0 0%;

        + .card {
            margin-left: 0;
            border-left: 0;
        }

        &:first-child {
            border-top-right-radius: 0;
            border-bottom-right-radius: 0;
        }

        &:last-child {
            border-top-left-radius: 0;
            border-bottom-left-radius: 0;
        }
    }
}

Building Form Components

Forms are critical for user interaction. Let's create a comprehensive form component system:

Form Input Base Styles

// components/_forms.scss

// Form group container
.form-group {
    margin-bottom: 1rem;
}

// Labels
.form-label {
    display: inline-block;
    margin-bottom: 0.5rem;
    font-weight: 500;
}

// Base input styles
.form-control {
    display: block;
    width: 100%;
    padding: 0.375rem 0.75rem;
    font-size: $font-size-base;
    font-weight: 400;
    line-height: 1.5;
    color: #495057;
    background-color: #fff;
    background-clip: padding-box;
    border: 1px solid #ced4da;
    border-radius: $border-radius;
    transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;

    &:focus {
        color: #495057;
        background-color: #fff;
        border-color: lighten($primary, 25%);
        outline: 0;
        box-shadow: 0 0 0 0.2rem rgba($primary, 0.25);
    }

    &::placeholder {
        color: #6c757d;
        opacity: 1;
    }

    &:disabled,
    &[readonly] {
        background-color: #e9ecef;
        opacity: 1;
    }
}

// Textarea
textarea.form-control {
    height: auto;
    resize: vertical;
}

Form Sizes and States

Input Sizes and Validation States

// Input sizes
.form-control-sm {
    padding: 0.25rem 0.5rem;
    font-size: map-get($font-sizes, sm);
    border-radius: $border-radius-sm;
}

.form-control-lg {
    padding: 0.5rem 1rem;
    font-size: map-get($font-sizes, lg);
    border-radius: $border-radius-lg;
}

// Validation states
$form-feedback-colors: (
    "valid": $success,
    "invalid": $danger
);

@each $state, $color in $form-feedback-colors {
    .form-control.is-#{$state} {
        border-color: $color;

        &:focus {
            border-color: $color;
            box-shadow: 0 0 0 0.2rem rgba($color, 0.25);
        }
    }

    .#{$state}-feedback {
        display: none;
        width: 100%;
        margin-top: 0.25rem;
        font-size: 0.875rem;
        color: $color;
    }

    .form-control.is-#{$state} ~ .#{$state}-feedback {
        display: block;
    }
}

Custom Checkbox and Radio Buttons

Custom Form Controls

// Custom checkbox
.custom-checkbox {
    position: relative;
    display: block;
    padding-left: 1.5rem;

    input[type="checkbox"] {
        position: absolute;
        left: 0;
        opacity: 0;

        &:checked ~ .custom-checkbox__label::before {
            background-color: $primary;
            border-color: $primary;
        }

        &:checked ~ .custom-checkbox__label::after {
            opacity: 1;
        }

        &:focus ~ .custom-checkbox__label::before {
            box-shadow: 0 0 0 0.2rem rgba($primary, 0.25);
        }

        &:disabled ~ .custom-checkbox__label {
            color: #6c757d;
            cursor: not-allowed;

            &::before {
                background-color: #e9ecef;
            }
        }
    }

    &__label {
        position: relative;
        margin-bottom: 0;
        cursor: pointer;

        &::before {
            content: "";
            position: absolute;
            left: -1.5rem;
            display: block;
            width: 1rem;
            height: 1rem;
            pointer-events: none;
            background-color: #fff;
            border: 1px solid #adb5bd;
            border-radius: $border-radius-sm;
            transition: $transition-base;
        }

        &::after {
            content: "";
            position: absolute;
            left: -1.5rem;
            top: 0.25rem;
            display: block;
            width: 1rem;
            height: 1rem;
            background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3e%3c/svg%3e");
            background-repeat: no-repeat;
            background-position: center;
            background-size: 50% 50%;
            opacity: 0;
            transition: opacity 0.1s ease-in-out;
        }
    }
}

// Custom radio
.custom-radio {
    @extend .custom-checkbox;

    .custom-radio__label {
        &::before {
            border-radius: 50%;
        }

        &::after {
            background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
        }
    }
}

Building Alert Components

Alert Component System

// components/_alerts.scss

.alert {
    position: relative;
    padding: 0.75rem 1.25rem;
    margin-bottom: 1rem;
    border: 1px solid transparent;
    border-radius: $border-radius;

    // Alert heading
    &__heading {
        color: inherit;
        margin-bottom: 0.5rem;
        font-weight: 500;
    }

    // Alert link
    &__link {
        font-weight: 700;
    }

    // Dismissible alert
    &--dismissible {
        padding-right: 4rem;

        .alert__close {
            position: absolute;
            top: 0;
            right: 0;
            padding: 0.75rem 1.25rem;
            color: inherit;
            background-color: transparent;
            border: 0;
            cursor: pointer;
            opacity: 0.5;

            &:hover {
                opacity: 0.75;
            }
        }
    }
}

// Alert variants
@mixin alert-variant($background, $border, $color) {
    color: $color;
    background-color: $background;
    border-color: $border;

    .alert__link {
        color: darken($color, 10%);
    }
}

$alert-colors: (
    "primary": ($primary-lightest, $primary-light, $primary-dark),
    "success": (lighten($success, 40%), lighten($success, 30%), darken($success, 10%)),
    "danger": (lighten($danger, 40%), lighten($danger, 30%), darken($danger, 10%)),
    "warning": (lighten($warning, 35%), lighten($warning, 25%), darken($warning, 10%)),
    "info": (lighten($info, 40%), lighten($info, 30%), darken($info, 10%))
);

@each $name, $colors in $alert-colors {
    .alert-#{$name} {
        @include alert-variant(nth($colors, 1), nth($colors, 2), nth($colors, 3));
    }
}

Consistent Spacing and Sizing

To maintain consistency across all components, define spacing and sizing utilities:

Spacing Utility Generator

// Generate spacing utilities (margin and padding)
$properties: (
    "m": "margin",
    "p": "padding"
);

$directions: (
    "t": "top",
    "r": "right",
    "b": "bottom",
    "l": "left"
);

@each $prop-abbr, $prop in $properties {
    @each $size-name, $size-value in $spacers {
        // All sides: .m-1, .p-2, etc.
        .#{$prop-abbr}-#{$size-name} {
            #{$prop}: $size-value !important;
        }

        // Individual sides: .mt-1, .pr-2, etc.
        @each $dir-abbr, $dir in $directions {
            .#{$prop-abbr}#{$dir-abbr}-#{$size-name} {
                #{$prop}-#{$dir}: $size-value !important;
            }
        }

        // Horizontal: .mx-1, .px-2, etc.
        .#{$prop-abbr}x-#{$size-name} {
            #{$prop}-left: $size-value !important;
            #{$prop}-right: $size-value !important;
        }

        // Vertical: .my-1, .py-2, etc.
        .#{$prop-abbr}y-#{$size-name} {
            #{$prop}-top: $size-value !important;
            #{$prop}-bottom: $size-value !important;
        }
    }
}

Component File Organization

Tip: Keep each component in its own file and use a clear naming convention. This makes it easier to find, maintain, and test individual components.

Main SCSS Import File

// main.scss - Component Library Entry Point

// 1. Abstracts (no CSS output)
@use "abstracts/variables" as *;
@use "abstracts/functions" as *;
@use "abstracts/mixins" as *;

// 2. Base styles
@use "base/reset";
@use "base/typography";

// 3. Components (alphabetically ordered)
@use "components/alerts";
@use "components/badges";
@use "components/buttons";
@use "components/cards";
@use "components/forms";
@use "components/modals";
@use "components/navs";

// 4. Utilities
@use "utilities/spacing";
@use "utilities/display";
@use "utilities/flexbox";

Exercise 1: Build a Complete Button System

Create a comprehensive button component with the following requirements:

  • Base button class with proper typography and padding
  • 5 color variants: primary, secondary, success, danger, info
  • 3 size variants: small, medium, large
  • Outline button variants for each color
  • Disabled state for all variants
  • Block button (full-width) modifier
  • Test all combinations in HTML

Exercise 2: Create a Card Component System

Build a flexible card component with:

  • Card container with shadow and border
  • Card header, body, and footer sections
  • Support for card images (top and bottom)
  • Card color variants (primary, success, danger)
  • Horizontal card layout modifier
  • Create 3 different card examples showcasing various features

Exercise 3: Build Custom Form Controls

Create styled form components including:

  • Text input with 3 sizes (small, medium, large)
  • Textarea with resize control
  • Validation states (valid and invalid) with colored borders and feedback messages
  • Custom checkbox with animated check mark
  • Custom radio button with circular selection
  • Disabled states for all form controls
  • Build a complete registration form using these components

Summary

In this lesson, you've learned how to build a professional component library with SASS. You now understand:

  • The principles of component-based architecture
  • How to structure a component library with proper file organization
  • Building button components with variants, sizes, and states using maps
  • Creating flexible card components with multiple sections
  • Developing comprehensive form components including custom controls
  • Implementing alert components with color variants
  • Using design tokens for consistent spacing and sizing
  • Organizing component files for scalability and maintainability

A well-designed component library accelerates development, ensures consistency, and makes your codebase more maintainable. With SASS, you can create powerful, flexible components that adapt to your project's evolving needs.