Loading
Create a modular CSS framework with a grid system, utility classes, component styles, dark mode, and responsive design.
Every developer uses CSS frameworks, but few understand how they work under the hood. In this tutorial, you will build a complete CSS framework from scratch using Sass. Your framework will include a 12-column responsive grid, a comprehensive utility class system, pre-built component styles, dark mode support, and a build pipeline that outputs a minified, production-ready CSS file you can publish to npm.
Building your own framework teaches you CSS architecture, Sass metaprogramming, responsive design systems, and how tools like Bootstrap and Tailwind are constructed internally. You will understand not just the what, but the why behind every design decision in modern CSS frameworks.
The framework compiles from Sass to CSS using standard build tools that work identically on macOS, Windows, and Linux.
Update package.json scripts:
Create the main entry point src/nova.scss:
Create src/base/_variables.scss:
Create src/base/_reset.scss:
Create src/base/_typography.scss:
Create src/grid/_grid.scss:
Create src/utilities/_spacing.scss:
Create src/utilities/_display.scss:
Create src/utilities/_colors.scss:
Create src/utilities/_text.scss:
Create src/components/_buttons.scss:
Create src/components/_cards.scss:
Create src/components/_forms.scss:
Create src/components/_navbar.scss:
Add dark mode support by appending to src/nova.scss after all other imports:
Build the framework:
Check the output:
Create an index.html demo page at the project root to test everything:
To prepare for npm publishing, add a main field pointing to dist/nova.min.css in package.json and include only the dist directory in the files array. Your framework is now a single import away for any project that needs a lightweight, well-structured CSS foundation.
mkdir nova-css && cd nova-css
npm init -y
npm install -D sass postcss autoprefixer cssnano postcss-cli clean-css-cli
mkdir -p src/{base,grid,utilities,components} distnpm run buildls -la dist/
# nova.css ~25KB (uncompressed)
# nova.min.css ~18KB (minified){
"scripts": {
"build:sass": "sass src/nova.scss dist/nova.css --no-source-map",
"build:prefix": "postcss dist/nova.css --use autoprefixer -o dist/nova.css --no-map",
"build:min": "cleancss -o dist/nova.min.css dist/nova.css",
"build": "npm run build:sass && npm run build:prefix && npm run build:min",
"watch": "sass src/nova.scss dist/nova.css --watch"
}
}// Nova CSS Framework
@use "base/variables" as *;
@use "base/reset";
@use "base/typography";
@use "grid/grid";
@use "utilities/spacing";
@use "utilities/display";
@use "utilities/colors";
@use "utilities/text";
@use "components/buttons";
@use "components/cards";
@use "components/forms";
@use "components/navbar";// Colors
$colors: (
"primary": #6366f1,
"secondary": #8b5cf6,
"success": #10b981,
"warning": #f59e0b,
"danger": #ef4444,
"info": #06b6d4,
"light": #f1f5f9,
"dark": #0f172a,
"white": #ffffff,
"black": #000000,
);
$grays: (
"50": #f8fafc,
"100": #f1f5f9,
"200": #e2e8f0,
"300": #cbd5e1,
"400": #94a3b8,
"500": #64748b,
"600": #475569,
"700": #334155,
"800": #1e293b,
"900": #0f172a,
"950": #020617,
);
// Spacing scale (rem)
$spacers: (
0: 0,
1: 0.25rem,
2: 0.5rem,
3: 0.75rem,
4: 1rem,
5: 1.25rem,
6: 1.5rem,
8: 2rem,
10: 2.5rem,
12: 3rem,
16: 4rem,
20: 5rem,
);
// Breakpoints
$breakpoints: (
"sm": 640px,
"md": 768px,
"lg": 1024px,
"xl": 1280px,
);
// Typography
$font-family-sans:
"Inter",
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
$font-family-mono: "JetBrains Mono", "Fira Code", "Consolas", monospace;
$font-sizes: (
"xs": 0.75rem,
"sm": 0.875rem,
"base": 1rem,
"lg": 1.125rem,
"xl": 1.25rem,
"2xl": 1.5rem,
"3xl": 1.875rem,
"4xl": 2.25rem,
);
$font-weights: (
"light": 300,
"normal": 400,
"medium": 500,
"semibold": 600,
"bold": 700,
);
// Border radius
$radii: (
"none": 0,
"sm": 0.25rem,
"md": 0.375rem,
"lg": 0.5rem,
"xl": 0.75rem,
"2xl": 1rem,
"full": 9999px,
);
// Shadows
$shadows: (
"sm": 0 1px 2px rgba(0, 0, 0, 0.05),
"md": 0 4px 6px -1px rgba(0, 0, 0, 0.1),
"lg": 0 10px 15px -3px rgba(0, 0, 0, 0.1),
"xl": 0 20px 25px -5px rgba(0, 0, 0, 0.1),
);
$grid-columns: 12;
$grid-gutter: 1.5rem;
$container-max: 1280px;@use "variables" as *;
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
-webkit-text-size-adjust: 100%;
tab-size: 4;
}
body {
font-family: $font-family-sans;
line-height: 1.6;
color: map-get($grays, "900");
background-color: map-get($colors, "white");
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
img,
svg,
video {
display: block;
max-width: 100%;
height: auto;
}
a {
color: inherit;
text-decoration: none;
}
button,
input,
select,
textarea {
font: inherit;
color: inherit;
}
ul,
ol {
list-style: none;
}@use "variables" as *;
@each $name, $size in $font-sizes {
.text-#{$name} {
font-size: $size;
}
}
@each $name, $weight in $font-weights {
.font-#{$name} {
font-weight: $weight;
}
}
h1,
.h1 {
font-size: map-get($font-sizes, "4xl");
font-weight: 700;
line-height: 1.2;
}
h2,
.h2 {
font-size: map-get($font-sizes, "3xl");
font-weight: 700;
line-height: 1.25;
}
h3,
.h3 {
font-size: map-get($font-sizes, "2xl");
font-weight: 600;
line-height: 1.3;
}
h4,
.h4 {
font-size: map-get($font-sizes, "xl");
font-weight: 600;
line-height: 1.35;
}
h5,
.h5 {
font-size: map-get($font-sizes, "lg");
font-weight: 600;
line-height: 1.4;
}
h6,
.h6 {
font-size: map-get($font-sizes, "base");
font-weight: 600;
line-height: 1.5;
}
.font-mono {
font-family: $font-family-mono;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.leading-tight {
line-height: 1.25;
}
.leading-normal {
line-height: 1.5;
}
.leading-relaxed {
line-height: 1.75;
}@use "../base/variables" as *;
.container {
width: 100%;
max-width: $container-max;
margin-left: auto;
margin-right: auto;
padding-left: calc($grid-gutter / 2);
padding-right: calc($grid-gutter / 2);
}
.row {
display: flex;
flex-wrap: wrap;
margin-left: calc($grid-gutter / -2);
margin-right: calc($grid-gutter / -2);
}
// Base column classes
@for $i from 1 through $grid-columns {
.col-#{$i} {
flex: 0 0 auto;
width: calc($i / $grid-columns * 100%);
padding-left: calc($grid-gutter / 2);
padding-right: calc($grid-gutter / 2);
}
}
.col-auto {
flex: 0 0 auto;
width: auto;
padding-left: calc($grid-gutter / 2);
padding-right: calc($grid-gutter / 2);
}
.col {
flex: 1 0 0%;
padding-left: calc($grid-gutter / 2);
padding-right: calc($grid-gutter / 2);
}
// Responsive column classes
@each $bp-name, $bp-value in $breakpoints {
@media (min-width: $bp-value) {
@for $i from 1 through $grid-columns {
.col-#{$bp-name}-#{$i} {
flex: 0 0 auto;
width: calc($i / $grid-columns * 100%);
padding-left: calc($grid-gutter / 2);
padding-right: calc($grid-gutter / 2);
}
}
.col-#{$bp-name}-auto {
flex: 0 0 auto;
width: auto;
}
}
}
// Gap utilities
@each $name, $value in $spacers {
.gap-#{$name} {
gap: $value;
}
.gap-x-#{$name} {
column-gap: $value;
}
.gap-y-#{$name} {
row-gap: $value;
}
}@use "../base/variables" as *;
$directions: (
"": "",
"t": "-top",
"r": "-right",
"b": "-bottom",
"l": "-left",
"x": (
"-left",
"-right",
),
"y": (
"-top",
"-bottom",
),
);
@each $dir-name, $dir-props in $directions {
@each $size-name, $size-value in $spacers {
@if type-of($dir-props) == "list" {
.m#{$dir-name}-#{$size-name} {
@each $prop in $dir-props {
margin#{$prop}: $size-value;
}
}
.p#{$dir-name}-#{$size-name} {
@each $prop in $dir-props {
padding#{$prop}: $size-value;
}
}
} @else {
.m#{$dir-name}-#{$size-name} {
margin#{$dir-props}: $size-value;
}
.p#{$dir-name}-#{$size-name} {
padding#{$dir-props}: $size-value;
}
}
}
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}@use "../base/variables" as *;
$displays: block, inline-block, inline, flex, inline-flex, grid, none;
@each $display in $displays {
.d-#{$display} {
display: $display;
}
}
// Flex utilities
.flex-row {
flex-direction: row;
}
.flex-col {
flex-direction: column;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-nowrap {
flex-wrap: nowrap;
}
.items-start {
align-items: flex-start;
}
.items-center {
align-items: center;
}
.items-end {
align-items: flex-end;
}
.items-stretch {
align-items: stretch;
}
.justify-start {
justify-content: flex-start;
}
.justify-center {
justify-content: center;
}
.justify-end {
justify-content: flex-end;
}
.justify-between {
justify-content: space-between;
}
.justify-around {
justify-content: space-around;
}
.flex-1 {
flex: 1 1 0%;
}
.flex-auto {
flex: 1 1 auto;
}
.flex-none {
flex: none;
}
// Responsive display
@each $bp-name, $bp-value in $breakpoints {
@media (min-width: $bp-value) {
@each $display in $displays {
.d-#{$bp-name}-#{$display} {
display: $display;
}
}
}
}@use "../base/variables" as *;
@each $name, $color in $colors {
.text-#{$name} {
color: $color;
}
.bg-#{$name} {
background-color: $color;
}
.border-#{$name} {
border-color: $color;
}
}
@each $name, $color in $grays {
.text-gray-#{$name} {
color: $color;
}
.bg-gray-#{$name} {
background-color: $color;
}
}.text-left {
text-align: left;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.uppercase {
text-transform: uppercase;
}
.lowercase {
text-transform: lowercase;
}
.capitalize {
text-transform: capitalize;
}
.underline {
text-decoration: underline;
}
.no-underline {
text-decoration: none;
}
.whitespace-nowrap {
white-space: nowrap;
}@use "../base/variables" as *;
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1.25rem;
font-size: map-get($font-sizes, "sm");
font-weight: 500;
line-height: 1.5;
border: 1px solid transparent;
border-radius: map-get($radii, "lg");
cursor: pointer;
transition: all 150ms ease;
text-decoration: none;
white-space: nowrap;
user-select: none;
&:focus-visible {
outline: 2px solid map-get($colors, "primary");
outline-offset: 2px;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
@each $name, $color in $colors {
.btn-#{$name} {
background-color: $color;
color: if(
$name == "light" or $name == "warning" or $name == "white",
map-get($grays, "900"),
white
);
&:hover:not(:disabled) {
filter: brightness(0.9);
}
}
.btn-outline-#{$name} {
background-color: transparent;
color: $color;
border-color: $color;
&:hover:not(:disabled) {
background-color: $color;
color: if(
$name == "light" or $name == "warning" or $name == "white",
map-get($grays, "900"),
white
);
}
}
}
.btn-sm {
padding: 0.375rem 0.75rem;
font-size: map-get($font-sizes, "xs");
}
.btn-lg {
padding: 0.75rem 1.75rem;
font-size: map-get($font-sizes, "lg");
}
.btn-block {
display: flex;
width: 100%;
}@use "../base/variables" as *;
.card {
background-color: white;
border: 1px solid map-get($grays, "200");
border-radius: map-get($radii, "xl");
overflow: hidden;
box-shadow: map-get($shadows, "sm");
transition: box-shadow 200ms ease;
&:hover {
box-shadow: map-get($shadows, "md");
}
}
.card-header {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid map-get($grays, "200");
}
.card-body {
padding: 1.5rem;
}
.card-footer {
padding: 1rem 1.5rem;
border-top: 1px solid map-get($grays, "200");
background-color: map-get($grays, "50");
}
.card-title {
font-size: map-get($font-sizes, "lg");
font-weight: 600;
margin-bottom: 0.5rem;
}
.card-text {
color: map-get($grays, "600");
line-height: 1.6;
}@use "../base/variables" as *;
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
margin-bottom: 0.375rem;
font-size: map-get($font-sizes, "sm");
font-weight: 500;
color: map-get($grays, "700");
}
.form-input,
.form-select,
.form-textarea {
display: block;
width: 100%;
padding: 0.625rem 0.875rem;
font-size: map-get($font-sizes, "base");
line-height: 1.5;
color: map-get($grays, "900");
background-color: white;
border: 1px solid map-get($grays, "300");
border-radius: map-get($radii, "lg");
transition:
border-color 150ms ease,
box-shadow 150ms ease;
appearance: none;
&:focus {
outline: none;
border-color: map-get($colors, "primary");
box-shadow: 0 0 0 3px rgba(map-get($colors, "primary"), 0.15);
}
&::placeholder {
color: map-get($grays, "400");
}
&:disabled {
background-color: map-get($grays, "100");
cursor: not-allowed;
}
}
.form-textarea {
min-height: 5rem;
resize: vertical;
}
.form-help {
margin-top: 0.25rem;
font-size: map-get($font-sizes, "xs");
color: map-get($grays, "500");
}
.form-error {
margin-top: 0.25rem;
font-size: map-get($font-sizes, "xs");
color: map-get($colors, "danger");
}@use "../base/variables" as *;
.navbar {
display: flex;
align-items: center;
padding: 0.75rem 1.5rem;
background-color: white;
border-bottom: 1px solid map-get($grays, "200");
}
.navbar-brand {
font-size: map-get($font-sizes, "xl");
font-weight: 700;
color: map-get($grays, "900");
margin-right: 2rem;
}
.navbar-nav {
display: flex;
align-items: center;
gap: 0.25rem;
flex: 1;
}
.nav-link {
padding: 0.5rem 0.75rem;
font-size: map-get($font-sizes, "sm");
font-weight: 500;
color: map-get($grays, "600");
border-radius: map-get($radii, "md");
transition:
color 150ms ease,
background-color 150ms ease;
&:hover {
color: map-get($grays, "900");
background-color: map-get($grays, "100");
}
&.active {
color: map-get($colors, "primary");
background-color: rgba(map-get($colors, "primary"), 0.08);
}
}
.navbar-end {
margin-left: auto;
display: flex;
align-items: center;
gap: 0.5rem;
}// Dark mode
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
}
body {
color: map-get($grays, "100");
background-color: map-get($grays, "950");
}
.card {
background-color: map-get($grays, "800");
border-color: map-get($grays, "700");
}
.card-footer {
background-color: map-get($grays, "900");
border-color: map-get($grays, "700");
}
.card-header {
border-color: map-get($grays, "700");
}
.form-input,
.form-select,
.form-textarea {
background-color: map-get($grays, "800");
border-color: map-get($grays, "600");
color: map-get($grays, "100");
}
.navbar {
background-color: map-get($grays, "900");
border-color: map-get($grays, "700");
}
.nav-link {
color: map-get($grays, "400");
&:hover {
color: map-get($grays, "100");
background-color: map-get($grays, "800");
}
}
}
// Manual dark mode toggle class
.dark {
body,
& {
color: map-get($grays, "100");
background-color: map-get($grays, "950");
}
}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Nova CSS Demo</title>
<link rel="stylesheet" href="dist/nova.css" />
</head>
<body>
<nav class="navbar">
<a class="navbar-brand" href="#">Nova</a>
<div class="navbar-nav">
<a class="nav-link active" href="#">Home</a>
<a class="nav-link" href="#">Docs</a>
</div>
<div class="navbar-end">
<button class="btn btn-primary btn-sm">Get Started</button>
</div>
</nav>
<div class="container mt-8">
<div class="row gap-6">
<div class="col-md-4 col-12">
<div class="card">
<div class="card-body">
<h3 class="card-title">Grid System</h3>
<p class="card-text">12-column responsive grid with breakpoints.</p>
</div>
</div>
</div>
<div class="col-md-4 col-12">
<div class="card">
<div class="card-body">
<h3 class="card-title">Utilities</h3>
<p class="card-text">Spacing, display, color, and text utilities.</p>
</div>
</div>
</div>
<div class="col-md-4 col-12">
<div class="card">
<div class="card-body">
<h3 class="card-title">Dark Mode</h3>
<p class="card-text">Automatic dark mode via prefers-color-scheme.</p>
</div>
</div>
</div>
</div>
</div>
</body>
</html>