Back to Frontend
Evergreen··28 min read

CSS from Zero to Design Systems

Everything you need to know about CSS — from selectors to layout to building your own component library. Written so a beginner can follow along and an interviewer will be impressed.

cssdesign-systemslayoutbeginnerinterview
Share

How CSS Actually Works

CSS stands for Cascading Style Sheets. It controls how HTML elements look — colors, sizes, spacing, layout, animation, everything visual.

Here's the mental model: HTML is the structure (walls, rooms, floors). CSS is the interior design (paint, furniture, lighting).

h1 {
  color: navy;
  font-size: 2rem;
}

This says: "Find every <h1> on the page and make its text navy-colored and 2rem tall."

Every CSS rule has two parts:

  1. Selectorwhat to style (h1)
  2. Declaration blockhow to style it ({ color: navy; })

Selectors

Selectors are how you point at elements. Master these and you can target anything.

Basic Selectors

/* Element selector — targets all <p> tags */
p { color: gray; }
 
/* Class selector — targets elements with class="card" */
.card { border: 1px solid #ddd; }
 
/* ID selector — targets the one element with id="header" */
#header { background: black; }
 
/* Universal selector — targets everything */
* { box-sizing: border-box; }

Combinators

/* Descendant — any <p> inside .card, at any depth */
.card p { margin: 0; }
 
/* Child — only direct <p> children of .card */
.card > p { font-weight: bold; }
 
/* Adjacent sibling — the <p> immediately after an <h2> */
h2 + p { margin-top: 0; }
 
/* General sibling — all <p> elements after an <h2> at the same level */
h2 ~ p { color: gray; }

Pseudo-classes

These target elements in a specific state.

a:hover { color: tomato; }          /* mouse is over it */
input:focus { outline: 2px solid blue; } /* it's focused */
li:first-child { font-weight: bold; }    /* first item in list */
li:last-child { border-bottom: none; }   /* last item */
li:nth-child(odd) { background: #f9f9f9; } /* odd rows */
button:disabled { opacity: 0.5; }        /* disabled state */

Pseudo-elements

These style a part of an element.

p::first-line { font-weight: bold; }   /* only the first line */
p::first-letter { font-size: 2em; }    /* drop cap effect */
.tooltip::before { content: "💡 "; }   /* insert content before */
.required::after { content: " *"; color: red; } /* insert after */

Attribute Selectors

input[type="text"] { border: 1px solid #ccc; }
a[href^="https"] { color: green; }   /* starts with https */
a[href$=".pdf"] { color: red; }      /* ends with .pdf */
img[alt] { border: none; }           /* has an alt attribute */

The Cascade, Specificity, and Inheritance

These three concepts explain why one style wins over another. Interviewers love asking about this.

The Cascade

When multiple rules target the same element, the browser follows this order (last wins):

  1. Browser default styles
  2. Your stylesheets (in order they appear)
  3. Rules later in the same file override earlier ones
p { color: blue; }
p { color: red; }  /* red wins — it comes later */

Specificity

When two rules target the same element, the more specific one wins, regardless of order.

Specificity is calculated as a score with three buckets: (ID, Class, Element)

SelectorScoreWhy
p0-0-11 element
.card0-1-01 class
#header1-0-01 ID
.card p0-1-11 class + 1 element
#header .nav a1-1-11 ID + 1 class + 1 element

Higher score wins. IDs beat classes. Classes beat elements.

.card p { color: blue; }   /* specificity: 0-1-1 */
p { color: red; }           /* specificity: 0-0-1 */
/* blue wins because 0-1-1 > 0-0-1 */

!important overrides everything. Avoid it. If you're using !important, you probably have a specificity problem you should fix instead.

Inheritance

Some properties are inherited by child elements automatically. Others are not.

Inherited (flow down to children): color, font-family, font-size, line-height, text-align, visibility, cursor

Not inherited (stay on the element): margin, padding, border, background, width, height, display, position

body {
  font-family: system-ui;  /* every child inherits this */
  color: #333;              /* every child inherits this */
}
 
.card {
  border: 1px solid #ddd;  /* children do NOT inherit this */
}

You can force inheritance with inherit or reset with initial:

.child { border: inherit; }  /* now it inherits parent's border */
.reset { color: initial; }   /* back to browser default */

The Box Model

Every element on a page is a rectangular box. The box model describes what makes up that rectangle.

┌─────────────────────────── margin ──────────────────────────┐
│  ┌──────────────────────── border ──────────────────────┐   │
│  │  ┌───────────────────── padding ─────────────────┐   │   │
│  │  │                                               │   │   │
│  │  │              content area                     │   │   │
│  │  │          (width × height)                     │   │   │
│  │  │                                               │   │   │
│  │  └───────────────────────────────────────────────┘   │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
  • Content — the text, image, or whatever is inside
  • Padding — space between the content and the border
  • Border — a line around the padding
  • Margin — space outside the border, pushing other elements away

box-sizing

By default, width only sets the content width. Padding and border are added on top of it. This makes math annoying.

Fix it with box-sizing: border-box — now width includes padding and border.

/* Always do this at the top of every project */
*, *::before, *::after {
  box-sizing: border-box;
}

This is the single most important CSS reset. Every framework, library, and design system uses it.

Margin Collapse

When two vertical margins touch, they collapse into one (the larger wins). This only happens vertically, never horizontally.

h2 { margin-bottom: 20px; }
p  { margin-top: 30px; }
/* The gap between h2 and p is 30px, not 50px */

This catches everyone off guard. If spacing looks wrong, margin collapse is usually why.


Units

UnitWhat It MeansWhen to Use
pxAbsolute pixelsBorders, shadows, small fixed sizes
remRelative to root font-size (usually 16px)Font sizes, spacing, most things
emRelative to parent's font-sizeComponent-scoped scaling
%Percentage of parentWidths, responsive containers
vw / vh% of viewport width / heightFull-screen sections, hero banners
frFraction of available space (grid only)Grid column/row sizing
chWidth of the "0" characterMax-width for readable text

Best practice: Use rem for most things. It scales with user font preferences and makes your site accessible.

html { font-size: 16px; }       /* 1rem = 16px */
h1   { font-size: 2rem; }       /* 32px */
p    { font-size: 1rem; }       /* 16px */
.container { max-width: 65ch; } /* ~65 characters wide — ideal reading width */

Display and Positioning

Display

The display property controls how an element behaves in the layout.

.block   { display: block; }        /* takes full width, stacks vertically */
.inline  { display: inline; }       /* sits in line with text, ignores width/height */
.ib      { display: inline-block; } /* inline but respects width/height */
.none    { display: none; }         /* completely removed from layout */
.flex    { display: flex; }         /* flexbox container */
.grid    { display: grid; }        /* grid container */

Position

.static   { position: static; }    /* default — normal flow */
.relative { position: relative; }  /* stays in flow, can be nudged with top/left */
.absolute { position: absolute; }  /* removed from flow, positioned relative to nearest positioned ancestor */
.fixed    { position: fixed; }     /* removed from flow, positioned relative to viewport */
.sticky   { position: sticky; top: 0; } /* normal flow until scroll threshold, then sticks */

Key concept: absolute positions relative to the nearest ancestor that has position: relative (or absolute, fixed, sticky). If there's no positioned ancestor, it positions relative to the <body>.

/* Common pattern: position a badge inside a card */
.card {
  position: relative; /* establishes positioning context */
}
 
.badge {
  position: absolute;
  top: 8px;
  right: 8px;
}

Stacking with z-index

z-index controls which element appears on top when they overlap. It only works on positioned elements (not static).

.modal-backdrop { position: fixed; z-index: 100; }
.modal          { position: fixed; z-index: 101; } /* on top of backdrop */
.tooltip        { position: absolute; z-index: 200; } /* on top of modal */

Flexbox

Flexbox is for one-dimensional layout — laying things out in a row or a column.

The Container

.container {
  display: flex;
  flex-direction: row;       /* row (default) | column */
  justify-content: center;   /* alignment along the main axis */
  align-items: center;       /* alignment along the cross axis */
  gap: 16px;                 /* space between items */
  flex-wrap: wrap;           /* allow items to wrap to next line */
}

Main Axis vs Cross Axis

  • flex-direction: row → main axis is horizontal, cross axis is vertical
  • flex-direction: column → main axis is vertical, cross axis is horizontal

justify-content always controls the main axis. align-items always controls the cross axis.

justify-content Values

justify-content: flex-start;     /* pack items to the start */
justify-content: flex-end;       /* pack items to the end */
justify-content: center;         /* center items */
justify-content: space-between;  /* equal space between, none at edges */
justify-content: space-around;   /* equal space around each item */
justify-content: space-evenly;   /* perfectly equal gaps everywhere */

align-items Values

align-items: flex-start;  /* align to the top (or left in column) */
align-items: flex-end;    /* align to the bottom */
align-items: center;      /* vertically center */
align-items: stretch;     /* stretch to fill (default) */
align-items: baseline;    /* align text baselines */

The Children

.item {
  flex-grow: 1;    /* how much this item should grow relative to others */
  flex-shrink: 0;  /* don't shrink smaller than your content */
  flex-basis: 200px; /* ideal starting width before grow/shrink */
}
 
/* Shorthand: flex: grow shrink basis */
.item { flex: 1 0 200px; }
 
/* Common shortcuts */
.item { flex: 1; }       /* grow equally, shrink equally, basis 0 */
.item { flex: none; }    /* don't grow, don't shrink, use content size */

Centering Anything

The most common interview question: "How do you center a div?"

.parent {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh; /* if you want it centered on the full page */
}

That's it. Four lines.


CSS Grid

Grid is for two-dimensional layout — rows AND columns at the same time.

Defining a Grid

.grid {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;  /* 3 equal columns */
  grid-template-rows: auto auto;        /* rows size to content */
  gap: 16px;                            /* space between cells */
}

The fr unit means "fraction of available space." 1fr 2fr 1fr gives you three columns where the middle one is twice as wide.

Handy Functions

/* repeat: avoid writing 1fr twelve times */
grid-template-columns: repeat(12, 1fr);
 
/* minmax: columns are at least 250px, at most 1fr */
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
/* auto-fill creates as many columns as fit — fully responsive, no media queries */

That repeat(auto-fill, minmax(250px, 1fr)) line is one of the most powerful patterns in CSS. It creates a responsive grid that adapts to any screen width without a single media query.

Placing Items

.item {
  grid-column: 1 / 3;  /* span from column line 1 to line 3 (2 columns wide) */
  grid-row: 1 / 2;     /* span row 1 */
}
 
/* Shorthand: span N */
.wide { grid-column: span 2; }  /* span 2 columns from wherever it lands */
.tall { grid-row: span 3; }     /* span 3 rows */

Named Grid Areas

For full page layouts, named areas are incredibly readable.

.layout {
  display: grid;
  grid-template-columns: 240px 1fr;
  grid-template-rows: 60px 1fr 40px;
  grid-template-areas:
    "header  header"
    "sidebar main"
    "footer  footer";
  min-height: 100vh;
}
 
.header  { grid-area: header; }
.sidebar { grid-area: sidebar; }
.main    { grid-area: main; }
.footer  { grid-area: footer; }

Grid vs Flexbox — When to Use Which

Use CaseUse
Navigation barFlexbox
Card gridGrid
Centering one thingFlexbox
Full page layoutGrid
Items in a single row/columnFlexbox
Items in rows AND columnsGrid
Don't know how many itemsFlexbox (with wrap) or Grid (with auto-fill)

Use both together. Grid for the page layout, flexbox for components inside the grid cells.


Responsive Design

Responsive design means your site works on any screen size.

Media Queries

/* Mobile-first: default styles apply to small screens */
.container {
  padding: 16px;
}
 
/* Then enhance for larger screens */
@media (min-width: 768px) {
  .container {
    padding: 32px;
    max-width: 720px;
    margin: 0 auto;
  }
}
 
@media (min-width: 1024px) {
  .container {
    max-width: 960px;
  }
}

Mobile-first means writing your base CSS for small screens and using min-width queries to add complexity for larger ones. This approach leads to simpler, more maintainable code.

Common Breakpoints

/* Small phones */   @media (min-width: 480px) { }
/* Tablets */        @media (min-width: 768px) { }
/* Laptops */        @media (min-width: 1024px) { }
/* Desktops */       @media (min-width: 1280px) { }

Container Queries

Container queries let a component respond to its parent's size instead of the viewport. This is a game-changer for reusable components.

.card-container {
  container-type: inline-size;
}
 
@container (min-width: 400px) {
  .card {
    flex-direction: row; /* horizontal layout when there's room */
  }
}

CSS Custom Properties (Variables)

Variables let you define values once and reuse them everywhere. They're the backbone of design systems.

:root {
  --color-primary: #6366f1;
  --color-primary-light: #818cf8;
  --color-text: #1f2937;
  --color-muted: #6b7280;
  --color-bg: #ffffff;
  --color-surface: #f9fafb;
  --color-border: #e5e7eb;
 
  --font-sans: system-ui, -apple-system, sans-serif;
  --font-mono: 'JetBrains Mono', monospace;
 
  --spacing-xs: 0.25rem;
  --spacing-sm: 0.5rem;
  --spacing-md: 1rem;
  --spacing-lg: 1.5rem;
  --spacing-xl: 2rem;
  --spacing-2xl: 3rem;
 
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 16px;
  --radius-full: 9999px;
 
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
  --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
  --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
}

Use them anywhere:

.button {
  background: var(--color-primary);
  color: white;
  padding: var(--spacing-sm) var(--spacing-md);
  border-radius: var(--radius-md);
  font-family: var(--font-sans);
}

Dark Mode with Variables

Override the variables in a dark context — every component updates automatically.

@media (prefers-color-scheme: dark) {
  :root {
    --color-text: #f9fafb;
    --color-muted: #9ca3af;
    --color-bg: #111827;
    --color-surface: #1f2937;
    --color-border: #374151;
  }
}

Zero component changes needed. Every button, card, and text element switches automatically.


Transitions and Animations

Transitions

Transitions smoothly animate a property change.

.button {
  background: var(--color-primary);
  transition: background 200ms ease, transform 200ms ease;
}
 
.button:hover {
  background: var(--color-primary-light);
  transform: translateY(-1px);
}

Format: transition: property duration timing-function delay

Common timing functions:

  • ease — starts fast, slows down (best default)
  • ease-in-out — slow start, fast middle, slow end
  • linear — constant speed

Keyframe Animations

For more complex, multi-step animations.

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
 
.card {
  animation: fadeIn 300ms ease forwards;
}

prefers-reduced-motion

Some users get motion sickness from animations. Always respect their preference.

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

Typography

Good typography makes or breaks a design.

body {
  font-family: var(--font-sans);
  font-size: 1rem;        /* 16px base */
  line-height: 1.6;       /* comfortable reading */
  color: var(--color-text);
  -webkit-font-smoothing: antialiased;
}
 
h1 { font-size: 2.25rem; line-height: 1.2; font-weight: 700; }
h2 { font-size: 1.75rem; line-height: 1.3; font-weight: 600; }
h3 { font-size: 1.375rem; line-height: 1.4; font-weight: 600; }
 
p + p { margin-top: 1em; }       /* space between paragraphs */
.prose { max-width: 65ch; }       /* ideal reading width */

A Type Scale

A type scale gives your headings consistent, harmonious sizes. Multiply by a ratio (1.25 is a good start).

base:  1rem     (16px)
h4:    1.25rem  (20px)  × 1.25
h3:    1.563rem (25px)  × 1.25
h2:    1.953rem (31px)  × 1.25
h1:    2.441rem (39px)  × 1.25

Building a Component Library

Now let's use everything to build real, reusable components. These are the building blocks of a design system.

The Reset

Every component library starts with a CSS reset to eliminate browser inconsistencies.

*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
 
html {
  font-size: 16px;
  -webkit-text-size-adjust: 100%;
}
 
body {
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.6;
  color: var(--color-text);
  background: var(--color-bg);
  -webkit-font-smoothing: antialiased;
}
 
img, video, svg {
  display: block;
  max-width: 100%;
}
 
button, input, select, textarea {
  font: inherit;
  color: inherit;
}
 
a {
  color: inherit;
  text-decoration: none;
}
 
ul, ol {
  list-style: none;
}

Button Component

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--spacing-sm);
  padding: var(--spacing-sm) var(--spacing-md);
  font-size: 0.875rem;
  font-weight: 500;
  line-height: 1;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background 150ms ease, transform 150ms ease, box-shadow 150ms ease;
  white-space: nowrap;
  user-select: none;
}
 
.btn:active {
  transform: scale(0.98);
}
 
.btn:focus-visible {
  outline: 2px solid var(--color-primary);
  outline-offset: 2px;
}
 
/* Variants */
.btn-primary {
  background: var(--color-primary);
  color: white;
}
.btn-primary:hover { background: var(--color-primary-light); }
 
.btn-secondary {
  background: var(--color-surface);
  color: var(--color-text);
  border-color: var(--color-border);
}
.btn-secondary:hover { background: var(--color-border); }
 
.btn-ghost {
  background: transparent;
  color: var(--color-text);
}
.btn-ghost:hover { background: var(--color-surface); }
 
/* Sizes */
.btn-sm { padding: 0.25rem 0.5rem; font-size: 0.75rem; }
.btn-lg { padding: 0.75rem 1.5rem; font-size: 1rem; }
 
/* Disabled */
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}
<button class="btn btn-primary">Save</button>
<button class="btn btn-secondary">Cancel</button>
<button class="btn btn-ghost btn-sm">Learn more</button>

Card Component

.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  transition: transform 200ms ease, box-shadow 200ms ease;
}
 
.card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-md);
}
 
.card-img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}
 
.card-body {
  padding: var(--spacing-lg);
}
 
.card-title {
  font-size: 1.125rem;
  font-weight: 600;
  margin-bottom: var(--spacing-xs);
}
 
.card-text {
  color: var(--color-muted);
  font-size: 0.875rem;
  line-height: 1.5;
}
 
.card-footer {
  padding: var(--spacing-sm) var(--spacing-lg);
  border-top: 1px solid var(--color-border);
  display: flex;
  align-items: center;
  justify-content: space-between;
}
<div class="card">
  <img class="card-img" src="photo.jpg" alt="Project screenshot" />
  <div class="card-body">
    <h3 class="card-title">Project Alpha</h3>
    <p class="card-text">A brief description of the project and what it does.</p>
  </div>
  <div class="card-footer">
    <span class="text-muted">Mar 2026</span>
    <button class="btn btn-ghost btn-sm">View</button>
  </div>
</div>

Input Component

.input-group {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-xs);
}
 
.label {
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--color-text);
}
 
.input {
  padding: var(--spacing-sm) var(--spacing-md);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-bg);
  font-size: 1rem;
  transition: border-color 150ms ease, box-shadow 150ms ease;
}
 
.input:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
}
 
.input::placeholder {
  color: var(--color-muted);
}
 
.input-error {
  border-color: #ef4444;
}
 
.input-error:focus {
  box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15);
}
 
.helper-text {
  font-size: 0.75rem;
  color: var(--color-muted);
}
 
.error-text {
  font-size: 0.75rem;
  color: #ef4444;
}
<div class="input-group">
  <label class="label" for="email">Email</label>
  <input class="input" type="email" id="email" placeholder="you@example.com" />
  <span class="helper-text">We'll never share your email.</span>
</div>

Badge Component

.badge {
  display: inline-flex;
  align-items: center;
  padding: 0.125rem 0.5rem;
  font-size: 0.75rem;
  font-weight: 500;
  border-radius: var(--radius-full);
  line-height: 1.4;
}
 
.badge-primary { background: rgba(99, 102, 241, 0.1); color: #6366f1; }
.badge-success { background: rgba(34, 197, 94, 0.1); color: #16a34a; }
.badge-warning { background: rgba(234, 179, 8, 0.1); color: #ca8a04; }
.badge-error   { background: rgba(239, 68, 68, 0.1); color: #dc2626; }
<span class="badge badge-success">Active</span>
<span class="badge badge-warning">Pending</span>
<span class="badge badge-error">Failed</span>

Avatar Component

.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-full);
  overflow: hidden;
  background: var(--color-primary);
  color: white;
  font-weight: 600;
  flex-shrink: 0;
}
 
.avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
 
.avatar-sm { width: 32px; height: 32px; font-size: 0.75rem; }
.avatar-md { width: 40px; height: 40px; font-size: 0.875rem; }
.avatar-lg { width: 56px; height: 56px; font-size: 1.125rem; }
 
.avatar-group {
  display: flex;
}
 
.avatar-group .avatar {
  border: 2px solid var(--color-bg);
  margin-left: -8px;
}
 
.avatar-group .avatar:first-child {
  margin-left: 0;
}
<div class="avatar avatar-md">
  <img src="user.jpg" alt="Kevin" />
</div>
 
<div class="avatar avatar-md">K</div>
 
<div class="avatar-group">
  <div class="avatar avatar-sm">A</div>
  <div class="avatar avatar-sm">B</div>
  <div class="avatar avatar-sm">C</div>
</div>
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
  animation: fadeIn 150ms ease;
}
 
.modal {
  background: var(--color-bg);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  width: 90%;
  max-width: 480px;
  max-height: 85vh;
  overflow-y: auto;
  animation: slideUp 200ms ease;
}
 
.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--spacing-lg);
  border-bottom: 1px solid var(--color-border);
}
 
.modal-title {
  font-size: 1.125rem;
  font-weight: 600;
}
 
.modal-close {
  background: none;
  border: none;
  font-size: 1.25rem;
  cursor: pointer;
  color: var(--color-muted);
  padding: var(--spacing-xs);
  border-radius: var(--radius-sm);
}
 
.modal-close:hover { background: var(--color-surface); }
 
.modal-body {
  padding: var(--spacing-lg);
}
 
.modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--spacing-sm);
  padding: var(--spacing-lg);
  border-top: 1px solid var(--color-border);
}
 
@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}
 
@keyframes slideUp {
  from { opacity: 0; transform: translateY(16px) scale(0.97); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}
<div class="modal-backdrop">
  <div class="modal">
    <div class="modal-header">
      <h2 class="modal-title">Confirm Action</h2>
      <button class="modal-close">&times;</button>
    </div>
    <div class="modal-body">
      <p>Are you sure you want to proceed?</p>
    </div>
    <div class="modal-footer">
      <button class="btn btn-secondary">Cancel</button>
      <button class="btn btn-primary">Confirm</button>
    </div>
  </div>
</div>

Design System Principles

A design system is more than a component library. It's a set of rules and constraints that keep your UI consistent.

1. Use a Spacing Scale

Don't use random pixel values. Pick a scale and stick to it.

4px — 8px — 12px — 16px — 24px — 32px — 48px — 64px

That's 0.25rem to 4rem in multiples of 4. Every margin, padding, and gap in your app should use one of these values.

2. Limit Your Colors

A good palette has:

  • 1 primary color (and 1-2 lighter/darker shades)
  • 1 text color + 1 muted text color
  • 1 background + 1 surface (slightly different background for cards)
  • 1 border color
  • 3 semantic colors: success (green), warning (yellow), error (red)

That's it. 10-12 colors total. More than that and your UI starts to look inconsistent.

3. Limit Your Font Sizes

Use 5-7 sizes maximum:

--text-xs:  0.75rem;   /* 12px — captions, labels */
--text-sm:  0.875rem;  /* 14px — secondary text */
--text-base: 1rem;     /* 16px — body text */
--text-lg:  1.125rem;  /* 18px — emphasized text */
--text-xl:  1.25rem;   /* 20px — small headings */
--text-2xl: 1.5rem;    /* 24px — section headings */
--text-3xl: 2rem;      /* 32px — page headings */

4. Consistent Border Radius

Pick 3-4 radius values:

--radius-sm: 4px;      /* small elements like badges */
--radius-md: 8px;      /* buttons, inputs */
--radius-lg: 16px;     /* cards, modals */
--radius-full: 9999px; /* circles, pills */

5. Elevation with Shadows

Shadows create visual hierarchy. Use 3 levels:

--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);    /* cards at rest */
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);     /* cards on hover, dropdowns */
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);    /* modals, popovers */

Common Interview Questions

What is the difference between em and rem?

em is relative to the parent element's font-size. rem is relative to the root (<html>) font-size. rem is more predictable because it doesn't compound — if you nest elements using em, the size multiplies with each level.

What is BEM?

BEM (Block Element Modifier) is a naming convention: .block__element--modifier.

.card { }                  /* Block */
.card__title { }           /* Element (part of card) */
.card__title--large { }    /* Modifier (variation) */

It prevents naming collisions and makes your CSS predictable and easy to read. BEM is widely used in design systems.

What's the difference between display: none and visibility: hidden?

  • display: none — removes the element from the layout entirely. It takes up no space.
  • visibility: hidden — hides the element visually but it still takes up space in the layout.

How does position: sticky work?

The element behaves like relative until the user scrolls past a threshold (set by top, bottom, etc.), then it behaves like fixed within its containing block.

.nav { position: sticky; top: 0; }

What is a stacking context?

A stacking context is a three-dimensional layer for rendering. Elements within a stacking context are ordered by z-index, but they can't escape their parent's context. New stacking contexts are created by:

  • position with a z-index
  • opacity less than 1
  • transform, filter, or will-change
  • isolation: isolate

What is contain and how does it help performance?

contain tells the browser that an element's internals won't affect the rest of the page, enabling rendering optimizations.

.card { contain: layout style paint; }
/* or the shorthand: */
.card { contain: content; }

How do you handle specificity conflicts in a large codebase?

  • Use flat selectors (single class names) to keep specificity low and equal
  • Use a consistent methodology like BEM
  • Layer your CSS: reset → tokens → base → components → utilities
  • Use CSS Layers (@layer) for explicit ordering
@layer reset, tokens, base, components, utilities;
 
@layer components {
  .btn { background: var(--color-primary); }
}
 
@layer utilities {
  .mt-4 { margin-top: 1rem; }  /* utilities always win */
}

What is @supports used for?

Feature detection in CSS. Only apply styles if the browser supports a property.

@supports (display: grid) {
  .layout { display: grid; }
}
 
@supports not (container-type: inline-size) {
  /* fallback for browsers without container queries */
  .card { width: 100%; }
}

Full Design System Demo

Here's a complete, single-file page that uses every concept and component from this guide. Save it as design-system.html and open it in your browser.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Mini Design System</title>
  <style>
    /* ============ TOKENS ============ */
    :root {
      --color-primary: #6366f1;
      --color-primary-light: #818cf8;
      --color-text: #1f2937;
      --color-muted: #6b7280;
      --color-bg: #ffffff;
      --color-surface: #f9fafb;
      --color-border: #e5e7eb;
      --color-success: #16a34a;
      --color-warning: #ca8a04;
      --color-error: #dc2626;
 
      --font-sans: system-ui, -apple-system, sans-serif;
      --spacing-xs: 0.25rem;
      --spacing-sm: 0.5rem;
      --spacing-md: 1rem;
      --spacing-lg: 1.5rem;
      --spacing-xl: 2rem;
      --spacing-2xl: 3rem;
 
      --radius-sm: 4px;
      --radius-md: 8px;
      --radius-lg: 16px;
      --radius-full: 9999px;
 
      --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
      --shadow-md: 0 4px 6px rgba(0,0,0,0.07);
      --shadow-lg: 0 10px 15px rgba(0,0,0,0.1);
    }
 
    @media (prefers-color-scheme: dark) {
      :root {
        --color-text: #f9fafb;
        --color-muted: #9ca3af;
        --color-bg: #111827;
        --color-surface: #1f2937;
        --color-border: #374151;
      }
    }
 
    /* ============ RESET ============ */
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
    html { font-size: 16px; }
    body {
      font-family: var(--font-sans);
      color: var(--color-text);
      background: var(--color-bg);
      line-height: 1.6;
      -webkit-font-smoothing: antialiased;
    }
    img { display: block; max-width: 100%; }
    button, input { font: inherit; color: inherit; }
    ul { list-style: none; }
    a { color: inherit; text-decoration: none; }
 
    /* ============ LAYOUT ============ */
    .page {
      display: grid;
      grid-template-columns: 1fr;
      grid-template-areas:
        "header"
        "main"
        "footer";
      min-height: 100vh;
    }
 
    .page-header {
      grid-area: header;
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: var(--spacing-md) var(--spacing-xl);
      border-bottom: 1px solid var(--color-border);
      position: sticky;
      top: 0;
      background: var(--color-bg);
      z-index: 10;
    }
 
    .logo {
      font-size: 1.25rem;
      font-weight: 700;
      color: var(--color-primary);
    }
 
    .nav-links { display: flex; gap: var(--spacing-lg); }
    .nav-links a {
      font-size: 0.875rem;
      color: var(--color-muted);
      transition: color 150ms;
    }
    .nav-links a:hover { color: var(--color-text); }
 
    .page-main {
      grid-area: main;
      padding: var(--spacing-2xl) var(--spacing-xl);
      max-width: 960px;
      width: 100%;
      margin: 0 auto;
    }
 
    .page-footer {
      grid-area: footer;
      text-align: center;
      padding: var(--spacing-lg);
      font-size: 0.75rem;
      color: var(--color-muted);
      border-top: 1px solid var(--color-border);
    }
 
    /* ============ TYPOGRAPHY ============ */
    .hero-title {
      font-size: 2.25rem;
      font-weight: 700;
      line-height: 1.2;
      margin-bottom: var(--spacing-sm);
    }
 
    .hero-subtitle {
      font-size: 1.125rem;
      color: var(--color-muted);
      max-width: 50ch;
      margin-bottom: var(--spacing-xl);
    }
 
    .section-title {
      font-size: 1.25rem;
      font-weight: 600;
      margin-bottom: var(--spacing-lg);
    }
 
    /* ============ BUTTONS ============ */
    .btn {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: var(--spacing-sm);
      padding: var(--spacing-sm) var(--spacing-md);
      font-size: 0.875rem;
      font-weight: 500;
      border: 1px solid transparent;
      border-radius: var(--radius-md);
      cursor: pointer;
      transition: background 150ms, transform 150ms;
      white-space: nowrap;
    }
    .btn:active { transform: scale(0.98); }
    .btn-primary { background: var(--color-primary); color: white; }
    .btn-primary:hover { background: var(--color-primary-light); }
    .btn-secondary {
      background: var(--color-surface);
      color: var(--color-text);
      border-color: var(--color-border);
    }
    .btn-secondary:hover { background: var(--color-border); }
    .btn-sm { padding: 0.25rem 0.5rem; font-size: 0.75rem; }
 
    /* ============ BADGES ============ */
    .badge {
      display: inline-flex;
      padding: 0.125rem 0.5rem;
      font-size: 0.75rem;
      font-weight: 500;
      border-radius: var(--radius-full);
    }
    .badge-primary { background: rgba(99,102,241,0.1); color: #6366f1; }
    .badge-success { background: rgba(34,197,94,0.1); color: var(--color-success); }
    .badge-warning { background: rgba(234,179,8,0.1); color: var(--color-warning); }
    .badge-error { background: rgba(239,68,68,0.1); color: var(--color-error); }
 
    /* ============ CARDS ============ */
    .card-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
      gap: var(--spacing-lg);
      margin-bottom: var(--spacing-2xl);
    }
 
    .card {
      background: var(--color-surface);
      border: 1px solid var(--color-border);
      border-radius: var(--radius-lg);
      overflow: hidden;
      box-shadow: var(--shadow-sm);
      transition: transform 200ms, box-shadow 200ms;
    }
    .card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
    .card-body { padding: var(--spacing-lg); }
    .card-title { font-size: 1.125rem; font-weight: 600; margin-bottom: var(--spacing-xs); }
    .card-text { color: var(--color-muted); font-size: 0.875rem; line-height: 1.5; }
    .card-footer {
      padding: var(--spacing-sm) var(--spacing-lg);
      border-top: 1px solid var(--color-border);
      display: flex;
      align-items: center;
      justify-content: space-between;
      font-size: 0.75rem;
      color: var(--color-muted);
    }
 
    /* ============ INPUTS ============ */
    .input-group { display: flex; flex-direction: column; gap: var(--spacing-xs); }
    .label { font-size: 0.875rem; font-weight: 500; }
    .input {
      padding: var(--spacing-sm) var(--spacing-md);
      border: 1px solid var(--color-border);
      border-radius: var(--radius-md);
      background: var(--color-bg);
      font-size: 1rem;
      transition: border-color 150ms, box-shadow 150ms;
    }
    .input:focus {
      outline: none;
      border-color: var(--color-primary);
      box-shadow: 0 0 0 3px rgba(99,102,241,0.15);
    }
    .input::placeholder { color: var(--color-muted); }
 
    /* ============ AVATARS ============ */
    .avatar {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 36px; height: 36px;
      border-radius: var(--radius-full);
      background: var(--color-primary);
      color: white;
      font-weight: 600;
      font-size: 0.875rem;
      flex-shrink: 0;
    }
 
    .avatar-group { display: flex; }
    .avatar-group .avatar {
      border: 2px solid var(--color-bg);
      margin-left: -8px;
    }
    .avatar-group .avatar:first-child { margin-left: 0; }
 
    /* ============ DEMO SECTIONS ============ */
    .demo-section {
      margin-bottom: var(--spacing-2xl);
      padding-bottom: var(--spacing-2xl);
      border-bottom: 1px solid var(--color-border);
    }
    .demo-section:last-child { border-bottom: none; }
 
    .demo-row {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: var(--spacing-md);
      margin-top: var(--spacing-md);
    }
 
    .form-grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: var(--spacing-lg);
      max-width: 480px;
      margin-top: var(--spacing-md);
    }
 
    .form-full { grid-column: span 2; }
 
    /* ============ ANIMATIONS ============ */
    @keyframes fadeIn {
      from { opacity: 0; transform: translateY(8px); }
      to { opacity: 1; transform: translateY(0); }
    }
    .fade-in { animation: fadeIn 400ms ease forwards; }
 
    @media (prefers-reduced-motion: reduce) {
      *, *::before, *::after {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
      }
    }
 
    /* ============ RESPONSIVE ============ */
    @media (max-width: 640px) {
      .hero-title { font-size: 1.75rem; }
      .form-grid { grid-template-columns: 1fr; }
      .form-full { grid-column: span 1; }
      .nav-links { display: none; }
    }
  </style>
</head>
<body>
  <div class="page">
    <header class="page-header">
      <span class="logo">DesignKit</span>
      <nav class="nav-links">
        <a href="#">Components</a>
        <a href="#">Tokens</a>
        <a href="#">Docs</a>
      </nav>
    </header>
 
    <main class="page-main fade-in">
      <!-- Hero -->
      <div class="demo-section">
        <h1 class="hero-title">Mini Design System</h1>
        <p class="hero-subtitle">
          A complete design system built with vanilla CSS, custom properties, and zero dependencies.
        </p>
        <div class="demo-row">
          <button class="btn btn-primary">Get Started</button>
          <button class="btn btn-secondary">View on GitHub</button>
        </div>
      </div>
 
      <!-- Buttons -->
      <div class="demo-section">
        <h2 class="section-title">Buttons</h2>
        <div class="demo-row">
          <button class="btn btn-primary">Primary</button>
          <button class="btn btn-secondary">Secondary</button>
          <button class="btn btn-primary btn-sm">Small</button>
          <button class="btn btn-primary" disabled>Disabled</button>
        </div>
      </div>
 
      <!-- Badges -->
      <div class="demo-section">
        <h2 class="section-title">Badges</h2>
        <div class="demo-row">
          <span class="badge badge-primary">New</span>
          <span class="badge badge-success">Active</span>
          <span class="badge badge-warning">Pending</span>
          <span class="badge badge-error">Failed</span>
        </div>
      </div>
 
      <!-- Avatars -->
      <div class="demo-section">
        <h2 class="section-title">Avatars</h2>
        <div class="demo-row">
          <div class="avatar">K</div>
          <div class="avatar" style="background: #ec4899;">A</div>
          <div class="avatar" style="background: #14b8a6;">M</div>
          <div class="avatar-group">
            <div class="avatar">K</div>
            <div class="avatar" style="background: #ec4899;">A</div>
            <div class="avatar" style="background: #14b8a6;">M</div>
            <div class="avatar" style="background: #f59e0b;">+2</div>
          </div>
        </div>
      </div>
 
      <!-- Form -->
      <div class="demo-section">
        <h2 class="section-title">Form Inputs</h2>
        <div class="form-grid">
          <div class="input-group">
            <label class="label" for="first">First name</label>
            <input class="input" type="text" id="first" placeholder="Kevin" />
          </div>
          <div class="input-group">
            <label class="label" for="last">Last name</label>
            <input class="input" type="text" id="last" placeholder="De Asis" />
          </div>
          <div class="input-group form-full">
            <label class="label" for="demo-email">Email</label>
            <input class="input" type="email" id="demo-email" placeholder="kevin@example.com" />
          </div>
          <div class="form-full">
            <button class="btn btn-primary">Submit</button>
          </div>
        </div>
      </div>
 
      <!-- Cards -->
      <div class="demo-section">
        <h2 class="section-title">Cards</h2>
        <div class="card-grid">
          <div class="card">
            <div class="card-body">
              <h3 class="card-title">Authentication</h3>
              <p class="card-text">Secure login flow with OAuth, MFA, and session management baked in.</p>
              <div class="demo-row">
                <span class="badge badge-success">Stable</span>
              </div>
            </div>
            <div class="card-footer">
              <span>v2.1.0</span>
              <button class="btn btn-secondary btn-sm">Docs</button>
            </div>
          </div>
 
          <div class="card">
            <div class="card-body">
              <h3 class="card-title">Data Grid</h3>
              <p class="card-text">Sortable, filterable tables with pagination and virtual scrolling.</p>
              <div class="demo-row">
                <span class="badge badge-warning">Beta</span>
              </div>
            </div>
            <div class="card-footer">
              <span>v0.9.3</span>
              <button class="btn btn-secondary btn-sm">Docs</button>
            </div>
          </div>
 
          <div class="card">
            <div class="card-body">
              <h3 class="card-title">Charts</h3>
              <p class="card-text">Responsive, accessible charts built on SVG with smooth animations.</p>
              <div class="demo-row">
                <span class="badge badge-primary">New</span>
              </div>
            </div>
            <div class="card-footer">
              <span>v1.0.0</span>
              <button class="btn btn-secondary btn-sm">Docs</button>
            </div>
          </div>
        </div>
      </div>
    </main>
 
    <footer class="page-footer">
      Built with vanilla CSS. No frameworks, no build tools, just the platform.
    </footer>
  </div>
</body>
</html>

Try It

Save that file as design-system.html, open it in your browser, and resize the window. The layout adapts, cards reflow, and if your OS is in dark mode, the colors switch automatically. All with zero JavaScript and zero dependencies.


Summary

TopicKey Takeaway
SelectorsquerySelector + class selectors cover 95% of cases
SpecificityKeep it flat. One class per rule. Avoid IDs and !important
Box ModelAlways use box-sizing: border-box
UnitsUse rem for sizing, px for borders/shadows
FlexboxOne-dimensional. justify-content = main axis, align-items = cross axis
GridTwo-dimensional. repeat(auto-fill, minmax(250px, 1fr)) is magic
ResponsiveMobile-first with min-width media queries
VariablesDefine tokens in :root, override for dark mode
ComponentsBuild with flat classes, CSS variables, and BEM naming
Design SystemConstrain spacing, colors, radii, and shadows to fixed scales

CSS is not hard. It's just wide. There are many properties, but the concepts underneath are few: the box model, the cascade, layout modes, and the idea that constraints create consistency. Master these and you can build anything.

You might also like