Building a Component Library with SASS
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
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
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.