Back to blog
Frontend Engineeringbeginner

CSS Fundamentals: Styling the Web

Master CSS from first principles โ€” selectors, the box model, display, positioning, colors, fonts, and building a polished card component.

Asma HafeezApril 17, 202621 min read
cssweb-fundamentalsstylingbox-modelselectors
Share:๐•

CSS Fundamentals: Styling the Web

HTML gives your page structure. CSS gives it personality. Without CSS, the web would be a sea of black text on white backgrounds โ€” technically functional, completely joyless.

CSS stands for Cascading Style Sheets. The word "cascading" describes something fundamental: styles flow down from parent to child elements, and multiple stylesheets can apply to the same document, with specificity rules determining which styles win. Understanding the cascade is the key to debugging CSS confidently.

This guide covers the core concepts you need to style any element on a page โ€” selectors, the box model, display modes, positioning, colors, and typography โ€” then brings them together in a polished card component.


How CSS Works: The Cascade and Specificity

When multiple rules target the same element, CSS needs to decide which one wins. It uses three factors in order:

  1. Importance โ€” !important declarations beat everything (use sparingly).
  2. Specificity โ€” How specific the selector is.
  3. Source order โ€” When specificity is equal, the last rule wins.

Specificity is calculated as a score with three components:

ID selectors      โ†’ 0-1-0-0  (100 points each)
Class/attribute   โ†’ 0-0-1-0  (10 points each)
Element           โ†’ 0-0-0-1  (1 point each)
CSS
p { color: blue; }                    /* specificity: 0,0,0,1 */
.text { color: green; }               /* specificity: 0,0,1,0 โ€” wins over p */
#intro { color: red; }               /* specificity: 0,1,0,0 โ€” wins over .text */
#intro.text { color: purple; }       /* specificity: 0,1,1,0 โ€” wins over #intro alone */
p#intro.text { color: orange; }      /* specificity: 0,1,1,1 โ€” wins over above */

This is why deeply nested selectors and IDs can cause so much pain โ€” they create high-specificity rules that are hard to override. A good strategy is to use mostly class selectors and keep specificity low and consistent.


Three Ways to Add CSS

Before diving into properties, let's cover where CSS lives:

HTML
<!-- 1. External stylesheet โ€” the right way for anything beyond a quick test -->
<link rel="stylesheet" href="styles.css" />

<!-- 2. Internal style block โ€” OK for single-page demos, bad for multi-page sites -->
<style>
  body {
    font-family: sans-serif;
  }
</style>

<!-- 3. Inline styles โ€” highest specificity, hardest to maintain, avoid unless necessary -->
<p style="color: red; font-size: 16px;">This is red text.</p>

Use external stylesheets in production. They are cached by the browser (so they load faster on subsequent visits), they keep concerns separated, and they apply consistently across all pages.


Selectors

A selector is the pattern CSS uses to identify which elements to style.

Element Selectors

Target all elements of a given type. Good for setting defaults.

CSS
/* All paragraphs */
p {
  line-height: 1.6;
  margin-bottom: 1rem;
}

/* All h2 headings */
h2 {
  font-size: 1.75rem;
  font-weight: 700;
}

/* All links */
a {
  color: #2563eb;
  text-decoration: underline;
}

Class Selectors

Target elements with a specific class. Classes can be reused on multiple elements. This is the workhorse of CSS in real projects.

CSS
/* Elements with class="card" */
.card {
  background: white;
  border-radius: 8px;
  padding: 24px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* Elements with class="btn" */
.btn {
  display: inline-block;
  padding: 10px 20px;
  border-radius: 6px;
  font-weight: 600;
  cursor: pointer;
}

/* Elements with BOTH classes: class="btn btn-primary" */
.btn.btn-primary {
  background-color: #2563eb;
  color: white;
}

ID Selectors

Target a single element with a specific ID. Since IDs must be unique per page, ID selectors are rarely useful for styling. They have high specificity that causes override problems. Use classes instead.

CSS
/* The element with id="main-nav" */
#main-nav {
  background: #1e293b;
}

Attribute Selectors

Target elements based on their attributes.

CSS
/* All links that open in a new tab */
a[target="_blank"] {
  /* Add an icon to indicate external link */
  padding-right: 18px;
  background: url("external-link-icon.svg") no-repeat right center;
  background-size: 14px;
}

/* All text inputs */
input[type="text"],
input[type="email"],
input[type="password"] {
  border: 1px solid #d1d5db;
  border-radius: 4px;
  padding: 8px 12px;
}

/* Elements whose class contains "icon" */
[class*="icon"] {
  display: inline-flex;
  align-items: center;
}

/* Links to PDF files */
a[href$=".pdf"] {
  color: #dc2626;
}

/* Links that start with https */
a[href^="https"] {
  /* Verified secure link */
}

Pseudo-Class Selectors

Target elements based on their state or position.

CSS
/* Hover state */
.btn:hover {
  background-color: #1d4ed8;
  transform: translateY(-1px);
}

/* Focus state โ€” CRITICAL for keyboard navigation */
:focus {
  outline: 2px solid #2563eb;
  outline-offset: 2px;
}

/* Active (being clicked) */
.btn:active {
  transform: translateY(0);
}

/* Visited links */
a:visited {
  color: #7c3aed;
}

/* First child in its parent */
li:first-child {
  border-top: none;
}

/* Last child */
li:last-child {
  border-bottom: none;
}

/* Every other row โ€” great for table striping */
tr:nth-child(even) {
  background-color: #f8fafc;
}

/* First of its type in the parent */
p:first-of-type {
  font-size: 1.125rem;
  font-weight: 500;
}

/* Target :not() to exclude elements */
.nav-item:not(.nav-item--active):hover {
  background-color: #f1f5f9;
}

/* Invalid form inputs */
input:invalid {
  border-color: #dc2626;
}

/* Valid form inputs */
input:valid {
  border-color: #16a34a;
}

Pseudo-Element Selectors

Target parts of an element that do not exist in the HTML.

CSS
/* First line of a paragraph */
p::first-line {
  font-weight: 600;
}

/* First letter โ€” for drop caps */
article::first-letter {
  font-size: 3rem;
  font-weight: 700;
  float: left;
  line-height: 1;
  margin-right: 4px;
}

/* Content before/after an element โ€” inserted via CSS, not HTML */
.required-field::after {
  content: " *";
  color: #dc2626;
}

.external-link::after {
  content: " โ†—";
  font-size: 0.75em;
}

/* Selection highlight color */
::selection {
  background-color: #bfdbfe;
  color: #1e40af;
}

/* Placeholder text styling */
input::placeholder {
  color: #9ca3af;
  font-style: italic;
}

Combinators

CSS
/* Descendant: all p inside .article (any depth) */
.article p {
  color: #374151;
}

/* Child: only direct p children of .article */
.article > p {
  margin-bottom: 1.25rem;
}

/* Adjacent sibling: h2 immediately followed by p */
h2 + p {
  font-size: 1.125rem;
  color: #6b7280;
}

/* General sibling: all p that follow an h2 (same parent) */
h2 ~ p {
  margin-top: 0.5rem;
}

The Box Model

Every element in HTML is a rectangular box. The box model describes the space that box occupies and the layers around its content.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚              MARGIN                      โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚   โ”‚           BORDER                โ”‚   โ”‚
โ”‚   โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚   โ”‚
โ”‚   โ”‚   โ”‚        PADDING          โ”‚   โ”‚   โ”‚
โ”‚   โ”‚   โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚   โ”‚   โ”‚
โ”‚   โ”‚   โ”‚   โ”‚    CONTENT      โ”‚   โ”‚   โ”‚   โ”‚
โ”‚   โ”‚   โ”‚   โ”‚  (width/height) โ”‚   โ”‚   โ”‚   โ”‚
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚   โ”‚   โ”‚
โ”‚   โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚   โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
CSS
.box {
  /* Content area dimensions */
  width: 300px;
  height: 200px;

  /* Space inside the border โ€” between border and content */
  padding: 16px;             /* all sides */
  padding: 16px 24px;        /* top/bottom left/right */
  padding: 8px 12px 16px 20px; /* top right bottom left */

  /* The visible border */
  border: 2px solid #e2e8f0;
  border-radius: 8px;        /* rounded corners */

  /* Space outside the border โ€” between this element and its neighbors */
  margin: 24px;
  margin: 24px auto;         /* center horizontally */
}

box-sizing: border-box

By default, CSS uses content-box sizing, which means width and height only describe the content area. Padding and border are added on top. This is confusing:

CSS
/* Default content-box: total width = 300 + 16 + 16 + 2 + 2 = 336px */
.default-box {
  box-sizing: content-box;  /* default */
  width: 300px;
  padding: 16px;
  border: 2px solid black;
  /* actual total width: 336px */
}

/* border-box: width = 300px total, padding and border included */
.border-box {
  box-sizing: border-box;
  width: 300px;
  padding: 16px;
  border: 2px solid black;
  /* actual total width: 300px */
}

border-box is much more intuitive. When you say width: 300px, you mean it is 300px wide in total โ€” not 300px plus whatever padding and border you add later. This is why virtually every CSS reset includes:

CSS
*,
*::before,
*::after {
  box-sizing: border-box;
}

Apply this at the top of every stylesheet.


Display: Block, Inline, and Inline-Block

The display property controls how an element participates in the document layout.

Block Elements

Block elements:

  • Take up the full width of their parent container (unless you set a specific width)
  • Always start on a new line
  • Respect width, height, margin, and padding in all directions

Default block elements: <div>, <p>, <h1>โ€“<h6>, <ul>, <ol>, <li>, <section>, <article>, <header>, <footer>, <main>, <nav>, <form>, <table>

CSS
.block {
  display: block;
  width: 50%;
  margin: 24px auto;
  background: #f1f5f9;
}

Inline Elements

Inline elements:

  • Sit within the text flow
  • Only take up as much width as their content
  • Do NOT respect width or height
  • Top/bottom margins and padding have limited effect

Default inline elements: <span>, <a>, <strong>, <em>, <img>, <code>, <button>, <input>, <label>

CSS
.highlight {
  display: inline;
  background-color: yellow;
  padding: 2px 4px; /* left/right work, top/bottom are weird */
}

Inline-Block

The best of both worlds: flows inline like text, but respects width, height, and all margins/padding like a block.

CSS
.tag {
  display: inline-block;
  padding: 4px 10px;
  background: #e0f2fe;
  border-radius: 9999px;
  font-size: 0.75rem;
  font-weight: 600;
  color: #0369a1;
  margin-right: 4px;
}

None

display: none removes the element entirely from the layout. The element takes up no space and is invisible to all users (including screen readers). Compare with visibility: hidden, which hides the element visually but preserves its space in the layout.

CSS
/* Completely removed from document flow */
.hidden { display: none; }

/* Visually hidden but space is preserved */
.invisible { visibility: hidden; }

/* Visually hidden but still accessible to screen readers */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

Positioning

The position property controls how elements are placed and whether they participate in normal document flow.

Static (Default)

Every element starts as position: static. It follows the normal document flow. top, right, bottom, left, and z-index have no effect.

Relative

The element stays in normal flow, but you can nudge it with top, right, bottom, left โ€” these offset it from where it would have been.

CSS
.slightly-down {
  position: relative;
  top: 10px;  /* moves DOWN 10px from its normal position */
}

The key use of position: relative is not to move elements โ€” it is to establish a positioning context for absolutely-positioned children.

Absolute

Removes the element from normal flow entirely. Positioned relative to its nearest ancestor with position other than static (usually a position: relative parent). If no such parent exists, it positions relative to the initial containing block (the viewport).

CSS
.parent {
  position: relative;  /* establishes positioning context */
}

.badge {
  position: absolute;
  top: -8px;
  right: -8px;
  background: #dc2626;
  color: white;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  font-size: 11px;
  display: flex;
  align-items: center;
  justify-content: center;
}

Fixed

Removed from normal flow. Positioned relative to the viewport and stays in place even when the page scrolls. Perfect for sticky navigation bars.

CSS
.navbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 64px;
  background: white;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  z-index: 100;
}

/* Compensate for fixed navbar taking up space */
body {
  padding-top: 64px;
}

Sticky

A hybrid between relative and fixed. The element scrolls normally until it reaches a threshold (like top: 0), then it "sticks" in place until its parent container scrolls out of view.

CSS
/* Sticky table header */
thead th {
  position: sticky;
  top: 0;
  background: white;
  z-index: 1;
}

/* Sticky sidebar */
.sidebar {
  position: sticky;
  top: 80px;  /* stick 80px from the top (accounting for navbar) */
  height: calc(100vh - 80px);
  overflow-y: auto;
}

Colors

CSS supports multiple color syntaxes.

CSS
.color-examples {
  /* Named colors โ€” 140+ available */
  color: red;
  color: cornflowerblue;
  color: darkslategray;

  /* Hexadecimal โ€” most common in practice */
  color: #2563eb;          /* full 6-digit hex */
  color: #fff;             /* shorthand for #ffffff */
  color: #2563ebcc;        /* 8-digit hex includes alpha channel */

  /* RGB */
  color: rgb(37, 99, 235);
  color: rgba(37, 99, 235, 0.5);   /* with 50% opacity */

  /* HSL โ€” most intuitive for humans to work with */
  color: hsl(221, 83%, 53%);
  color: hsla(221, 83%, 53%, 0.5); /* with 50% opacity */

  /* Modern CSS color functions (CSS Color Level 4) */
  color: oklch(55% 0.2 260);  /* perceptually uniform color space */
}

HSL is the most intuitive color system for CSS:

  • H = Hue (0โ€“360, the color wheel: 0=red, 120=green, 240=blue)
  • S = Saturation (0%=gray, 100%=full color)
  • L = Lightness (0%=black, 50%=normal, 100%=white)

Want a lighter or darker shade of the same color? Just adjust L. Want a muted version? Lower S. This is hard to do intuitively with hex or rgb.

CSS Custom Properties (Variables)

CSS variables let you define values once and reference them everywhere.

CSS
:root {
  /* Color palette */
  --color-primary: hsl(221, 83%, 53%);
  --color-primary-hover: hsl(221, 83%, 43%);
  --color-secondary: hsl(262, 83%, 58%);
  --color-success: hsl(142, 71%, 45%);
  --color-warning: hsl(38, 92%, 50%);
  --color-danger: hsl(0, 84%, 60%);

  /* Neutral palette */
  --color-gray-50: hsl(210, 40%, 98%);
  --color-gray-100: hsl(210, 40%, 96%);
  --color-gray-200: hsl(214, 32%, 91%);
  --color-gray-700: hsl(215, 25%, 27%);
  --color-gray-900: hsl(222, 47%, 11%);

  /* Typography */
  --font-family-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-family-mono: 'Fira Code', 'JetBrains Mono', 'Courier New', monospace;

  /* Spacing */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;
  --space-12: 48px;
  --space-16: 64px;

  /* Border radius */
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-full: 9999px;

  /* Shadows */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}

.card {
  background: var(--color-gray-50);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  padding: var(--space-6);
}

.btn-primary {
  background: var(--color-primary);
  color: white;
  padding: var(--space-3) var(--space-6);
}

.btn-primary:hover {
  background: var(--color-primary-hover);
}

Typography

CSS
body {
  /* Font stack: try each in order until one is available */
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;

  /* Base font size โ€” rem units scale with the browser's default (usually 16px) */
  font-size: 1rem;      /* 16px */

  /* Line height โ€” unitless value is recommended (multiplied by font size) */
  line-height: 1.6;

  font-weight: 400;     /* normal */
  color: #1e293b;
}

h1, h2, h3, h4, h5, h6 {
  font-family: 'Poppins', var(--font-family-sans);
  font-weight: 700;
  line-height: 1.2;    /* tighter line height for large text */
  letter-spacing: -0.025em;
}

h1 { font-size: 3rem; }     /* 48px */
h2 { font-size: 2.25rem; }  /* 36px */
h3 { font-size: 1.875rem; } /* 30px */
h4 { font-size: 1.5rem; }   /* 24px */
h5 { font-size: 1.25rem; }  /* 20px */
h6 { font-size: 1.125rem; } /* 18px */

/* Loading Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@600;700&display=swap');

/* Text utilities */
.text-sm   { font-size: 0.875rem; }   /* 14px */
.text-base { font-size: 1rem; }       /* 16px */
.text-lg   { font-size: 1.125rem; }   /* 18px */
.text-xl   { font-size: 1.25rem; }    /* 20px */
.text-2xl  { font-size: 1.5rem; }     /* 24px */

.font-normal  { font-weight: 400; }
.font-medium  { font-weight: 500; }
.font-semibold{ font-weight: 600; }
.font-bold    { font-weight: 700; }

.text-muted { color: #6b7280; }
.text-center { text-align: center; }
.uppercase { text-transform: uppercase; letter-spacing: 0.05em; }

Backgrounds

CSS
.hero {
  /* Solid color */
  background-color: #1e293b;

  /* Gradient */
  background: linear-gradient(135deg, #1e293b 0%, #2563eb 100%);
  background: radial-gradient(circle at 50% 50%, #2563eb, #1e293b);

  /* Image */
  background-image: url('hero-bg.jpg');
  background-size: cover;         /* cover entire element */
  background-position: center;   /* center the image */
  background-repeat: no-repeat;  /* do not tile */
  background-attachment: fixed;  /* parallax effect */

  /* Image with overlay using multiple backgrounds */
  background-image: 
    linear-gradient(rgba(0,0,0,0.5), rgba(0,0,0,0.5)),
    url('hero-bg.jpg');
  background-size: cover;
  background-position: center;
}

/* Pattern background */
.pattern-bg {
  background-color: #f8fafc;
  background-image: 
    radial-gradient(circle, #e2e8f0 1px, transparent 1px);
  background-size: 20px 20px;
}

Transitions and Hover Effects

Transitions make state changes smooth instead of jarring.

CSS
/* Apply transitions to the base state, not just :hover */
.btn {
  background-color: #2563eb;
  color: white;
  padding: 10px 24px;
  border-radius: 6px;
  border: none;
  cursor: pointer;
  font-size: 1rem;
  font-weight: 600;

  /* transition: property duration timing-function delay */
  transition: background-color 200ms ease, transform 200ms ease, box-shadow 200ms ease;
}

.btn:hover {
  background-color: #1d4ed8;
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
}

.btn:active {
  transform: translateY(0);
  box-shadow: none;
}

/* Transition shorthand โ€” transition ALL animatable properties */
.card {
  transition: all 300ms ease-in-out;
}

/* Common timing functions */
.ease { transition-timing-function: ease; }           /* default โ€” fast then slow */
.ease-in { transition-timing-function: ease-in; }     /* slow start */
.ease-out { transition-timing-function: ease-out; }   /* slow end */
.ease-in-out { transition-timing-function: ease-in-out; } /* slow start and end */
.linear { transition-timing-function: linear; }       /* constant speed */
.custom { transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); } /* bounce! */

Complete Project: Styled Card Component

Now let's build a complete, production-quality card component.

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Article Card Component</title>
  <link rel="preconnect" href="https://fonts.googleapis.com" />
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" />
  <link rel="stylesheet" href="card.css" />
</head>
<body>
  <main class="demo-container">
    <h1>Article Cards</h1>

    <div class="card-grid">

      <!-- Card 1 -->
      <article class="card">
        <div class="card__image-wrapper">
          <img
            src="https://images.unsplash.com/photo-1587620962725-abab7fe55159?w=600&h=340&fit=crop"
            alt="JavaScript code on a laptop screen"
            class="card__image"
            width="600"
            height="340"
            loading="lazy"
          />
          <span class="card__badge card__badge--beginner">Beginner</span>
        </div>

        <div class="card__body">
          <div class="card__meta">
            <span class="card__category">JavaScript</span>
            <span class="card__separator" aria-hidden="true">ยท</span>
            <span class="card__read-time">8 min read</span>
          </div>

          <h2 class="card__title">
            <a href="/articles/js-variables-functions" class="card__title-link">
              JavaScript Variables and Functions Explained
            </a>
          </h2>

          <p class="card__description">
            Master the building blocks of JavaScript โ€” variables, data types,
            functions, scope, and closures โ€” with clear explanations and real examples.
          </p>

          <div class="card__tags" aria-label="Tags">
            <span class="tag">JavaScript</span>
            <span class="tag">Functions</span>
            <span class="tag">Closures</span>
          </div>
        </div>

        <footer class="card__footer">
          <div class="card__author">
            <img
              src="https://i.pravatar.cc/40?img=47"
              alt="Asma Hafeez"
              class="card__author-avatar"
              width="32"
              height="32"
            />
            <div class="card__author-info">
              <span class="card__author-name">Asma Hafeez</span>
              <time class="card__date" datetime="2026-04-17">Apr 17, 2026</time>
            </div>
          </div>
          <a href="/articles/js-variables-functions" class="card__cta" aria-label="Read JavaScript Variables and Functions Explained">
            Read โ†’
          </a>
        </footer>
      </article>

      <!-- Card 2 โ€” Intermediate -->
      <article class="card">
        <div class="card__image-wrapper">
          <img
            src="https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=600&h=340&fit=crop"
            alt="React component diagram on a whiteboard"
            class="card__image"
            width="600"
            height="340"
            loading="lazy"
          />
          <span class="card__badge card__badge--intermediate">Intermediate</span>
        </div>

        <div class="card__body">
          <div class="card__meta">
            <span class="card__category">React</span>
            <span class="card__separator" aria-hidden="true">ยท</span>
            <span class="card__read-time">12 min read</span>
          </div>

          <h2 class="card__title">
            <a href="/articles/react-hooks" class="card__title-link">
              React Hooks: A Complete Deep Dive
            </a>
          </h2>

          <p class="card__description">
            Understand useState, useEffect, useCallback, useMemo, and useRef
            with practical examples that go beyond the basics.
          </p>

          <div class="card__tags" aria-label="Tags">
            <span class="tag">React</span>
            <span class="tag">Hooks</span>
            <span class="tag">useState</span>
          </div>
        </div>

        <footer class="card__footer">
          <div class="card__author">
            <img
              src="https://i.pravatar.cc/40?img=47"
              alt="Asma Hafeez"
              class="card__author-avatar"
              width="32"
              height="32"
            />
            <div class="card__author-info">
              <span class="card__author-name">Asma Hafeez</span>
              <time class="card__date" datetime="2026-04-10">Apr 10, 2026</time>
            </div>
          </div>
          <a href="/articles/react-hooks" class="card__cta" aria-label="Read React Hooks: A Complete Deep Dive">
            Read โ†’
          </a>
        </footer>
      </article>

    </div>
  </main>
</body>
</html>
CSS
/* card.css */

/* ========================================
   CSS RESET AND BASE
   ======================================== */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

:root {
  --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;

  --color-white: hsl(0, 0%, 100%);
  --color-gray-50: hsl(210, 40%, 98%);
  --color-gray-100: hsl(210, 40%, 96%);
  --color-gray-200: hsl(214, 32%, 91%);
  --color-gray-500: hsl(220, 9%, 46%);
  --color-gray-700: hsl(215, 25%, 27%);
  --color-gray-900: hsl(222, 47%, 11%);

  --color-primary: hsl(221, 83%, 53%);
  --color-primary-light: hsl(221, 100%, 96%);
  --color-success: hsl(142, 71%, 45%);
  --color-warning: hsl(38, 92%, 50%);
  --color-danger: hsl(0, 84%, 60%);

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
  --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.04);
  --shadow-lg: 0 20px 25px -5px rgba(0, 0, 0, 0.08), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
  --shadow-focus: 0 0 0 3px rgba(37, 99, 235, 0.4);

  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-xl: 16px;
}

body {
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.6;
  color: var(--color-gray-700);
  background-color: var(--color-gray-100);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* ========================================
   DEMO PAGE LAYOUT
   ======================================== */
.demo-container {
  max-width: 1100px;
  margin: 0 auto;
  padding: 48px 24px;
}

.demo-container > h1 {
  font-size: 2rem;
  font-weight: 700;
  color: var(--color-gray-900);
  margin-bottom: 32px;
  text-align: center;
}

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 28px;
}

/* ========================================
   CARD COMPONENT
   ======================================== */
.card {
  background: var(--color-white);
  border-radius: var(--radius-xl);
  overflow: hidden;
  box-shadow: var(--shadow-md);
  display: flex;
  flex-direction: column;

  /* Smooth transition for hover effect */
  transition: transform 250ms ease, box-shadow 250ms ease;
}

.card:hover {
  transform: translateY(-4px);
  box-shadow: var(--shadow-lg);
}

/* ---- Image Section ---- */
.card__image-wrapper {
  position: relative;
  overflow: hidden;
  aspect-ratio: 16 / 9;
}

.card__image {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform 400ms ease;
}

.card:hover .card__image {
  transform: scale(1.04);
}

/* Badge: difficulty level */
.card__badge {
  position: absolute;
  top: 12px;
  right: 12px;
  padding: 4px 10px;
  border-radius: var(--radius-sm);
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--color-white);
}

.card__badge--beginner {
  background-color: var(--color-success);
}

.card__badge--intermediate {
  background-color: var(--color-warning);
  color: var(--color-gray-900);
}

.card__badge--advanced {
  background-color: var(--color-danger);
}

/* ---- Body Section ---- */
.card__body {
  padding: 24px 24px 16px;
  flex: 1;  /* takes up remaining space, pushes footer down */
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.card__meta {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.8125rem;
  color: var(--color-gray-500);
}

.card__category {
  font-weight: 600;
  color: var(--color-primary);
}

.card__separator {
  color: var(--color-gray-200);
}

.card__title {
  font-size: 1.1875rem;
  font-weight: 700;
  line-height: 1.35;
  color: var(--color-gray-900);
}

.card__title-link {
  color: inherit;
  text-decoration: none;
  transition: color 200ms ease;
}

.card__title-link:hover {
  color: var(--color-primary);
}

.card__title-link:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
  border-radius: 2px;
}

.card__description {
  font-size: 0.9375rem;
  color: var(--color-gray-500);
  line-height: 1.65;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Tags */
.card__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: auto;  /* push to bottom of flex container */
}

.tag {
  display: inline-block;
  padding: 3px 10px;
  background: var(--color-primary-light);
  color: var(--color-primary);
  border-radius: var(--radius-sm);
  font-size: 0.75rem;
  font-weight: 600;
}

/* ---- Footer Section ---- */
.card__footer {
  padding: 16px 24px;
  border-top: 1px solid var(--color-gray-100);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.card__author {
  display: flex;
  align-items: center;
  gap: 10px;
}

.card__author-avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid var(--color-gray-200);
}

.card__author-info {
  display: flex;
  flex-direction: column;
}

.card__author-name {
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--color-gray-700);
  line-height: 1.2;
}

.card__date {
  font-size: 0.75rem;
  color: var(--color-gray-500);
}

.card__cta {
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--color-primary);
  text-decoration: none;
  padding: 6px 12px;
  border-radius: var(--radius-sm);
  transition: background-color 200ms ease, color 200ms ease;
}

.card__cta:hover {
  background-color: var(--color-primary-light);
}

.card__cta:focus-visible {
  outline: none;
  box-shadow: var(--shadow-focus);
}

Common Mistakes to Avoid

Not applying box-sizing: border-box. The default content-box model means padding and border add to declared widths. This causes elements to overflow their containers in unexpected ways. Reset it globally.

Using px everywhere for font sizes. Use rem for font sizes so they scale with the user's browser preferences. A user who has set their browser to large text (an accessibility feature) cannot override px font sizes.

Relying on !important to fix specificity problems. Once you start using !important, you end up in an arms race. Fix the underlying specificity issue instead.

Animating layout properties. Animating width, height, margin, padding, or top/left/right/bottom triggers browser layout recalculations and can cause janky animation. Animate transform and opacity instead โ€” they run on the compositor thread.

Missing focus styles. Many developers remove the default outline for aesthetics without providing an alternative. This makes the page unusable for keyboard users. Always provide a visible :focus-visible style.

Using IDs for styling. IDs have very high specificity, causing override problems. Use classes for styling; reserve IDs for JavaScript hooks and anchor links.


Key Takeaways

  • The cascade applies styles in order of importance, specificity, and source order. Understanding this eliminates most CSS confusion.
  • box-sizing: border-box should be applied globally on every project โ€” it makes sizing predictable.
  • Use semantic HTML elements first; CSS selectors describe what to style, not what the element means.
  • position: relative establishes a positioning context for position: absolute children.
  • CSS custom properties (variables) allow you to define a design system once and reference it everywhere โ€” essential for maintainable, themeable code.
  • Animate transform and opacity for smooth, performant transitions.
  • Never remove focus styles without replacing them โ€” keyboard accessibility depends on them.

Enjoyed this article?

Explore the Frontend Engineering learning path for more.

Found this helpful?

Share:๐•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.