Skip to main content

πŸ› οΈ Weekend Project: CSS Preprocessors & Frameworks

Time to put the whole module to work. Over two focused days you'll ship a small but genuinely polished multi-page site that leans on a CSS preprocessor and a framework β€” not a sprawling "do everything" app, but a tight, finished piece you'd be proud to link on your portfolio. This is a guided build with clear milestones, a definition-of-done checklist, and a bar for what "good" looks like.

🎯 Learning Objectives

By the end of this project, you will be able to:

  • Scaffold a real build with a preprocessor (Sass) and a framework (Tailwind or Bootstrap) wired into a dev + production pipeline
  • Customize a framework through its design tokens (variables / config) instead of fighting it with override CSS
  • Build at least three interactive components and one accessible, validated form
  • Ship an optimized, purged, minified production build and deploy it to a live URL
  • Judge your own work against an objective "what good looks like" bar

Estimated Time: One weekend (8–12 focused hours)  β€’  Difficulty: Intermediate

Hands-on: This entire lesson is the exercise β€” you finish with a deployed site and a short write-up.

In This Lesson

The Brief & Scope

Build a 3-to-5 page marketing site for a small fictional business of your choosing β€” a cafΓ©, a photography studio, a climbing gym, a tiny SaaS product. The exact subject does not matter. What matters is that you exercise the module's skills end to end: a preprocessor, a customized framework, real interactivity, a working form, and a shipped build.

⚠️ Scope discipline is the whole point

The single biggest failure mode of weekend projects is scope creep. Three finished pages beat ten half-built ones every time. Pick a small idea, cut features rather than deadlines, and treat "done and deployed" as the target. If you find yourself adding a login system or a database, stop β€” that is a different project.

Minimum requirements (all of these)

  • 3–5 pages that share a header, footer, and navigation (e.g. Home, About, Services/Menu, Contact)
  • A CSS preprocessor β€” Sass/SCSS recommended β€” with at least variables, nesting, and one mixin or partial split
  • A CSS framework β€” Tailwind CSS or Bootstrap β€” customized via its config/variables, not just used stock
  • Three interactive components: e.g. a responsive nav with a mobile menu, an accordion/tabs, and a modal or carousel
  • One accessible contact form with client-side validation and clear error messaging
  • A production build that purges unused CSS and minifies output, deployed to a live URL

Pick two stretch goals (optional but encouraged)

  • Dark/light theme toggle that remembers the user's choice
  • A small tasteful animation on scroll or hover (respecting prefers-reduced-motion)
  • A lightbox image gallery
  • Full keyboard navigation + a Lighthouse Accessibility score of 95+

πŸ“– Why a preprocessor and a framework?

The framework gives you a tested component system and a design token layer for free. The preprocessor gives you the authoring ergonomics β€” variables, partials, mixins β€” to organize your own code around it. Used together they're complementary: the framework is the foundation, the preprocessor is how you keep your customizations tidy and DRY.

The Milestone Map

Rather than a vague "build a website," this project is broken into five milestones. Each one ends with something you can look at and verify. Finish them in order β€” every milestone assumes the previous one works.

flowchart LR M0[M0 Β· Scaffold\nbuild + framework] --> M1[M1 Β· Tokens\n& layout] M1 --> M2[M2 Β· Interactive\ncomponents] M2 --> M3[M3 Β· Form\n+ validation] M3 --> M4[M4 Β· Optimize\n& ship] M4 -->|Definition of Done| DONE([Deployed πŸŽ‰])

πŸ’‘ A suggested rhythm

Saturday: M0 (morning) β†’ M1 (midday) β†’ start M2 (afternoon). Sunday: finish M2 β†’ M3 (midday) β†’ M4 + deploy (afternoon). Leave the last couple of hours for polish and the checklist β€” do not spend them adding new features.

Milestone 0 β€” Scaffold the Build

Goal: a running dev server that compiles your styles, with the framework installed and one page rendering. Do not write real content yet β€” you are proving the pipeline works.

Option A β€” Tailwind CSS + Vite (recommended for utility-first)

# Create the project
npm create vite@latest my-weekend-site -- --template vanilla
cd my-weekend-site
npm install

# Add Tailwind (v4 uses the Vite plugin β€” no config file needed to start)
npm install tailwindcss @tailwindcss/vite

Wire the plugin into Vite and pull Tailwind into your stylesheet:

// vite.config.js
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [tailwindcss()],
});
/* src/style.css */
@import "tailwindcss";

/* Customize with design tokens β€” this is your framework customization */
@theme {
  --color-brand: #5c6ac4;
  --color-brand-dark: #4c58a8;
  --font-display: "Playfair Display", serif;
}

Option B β€” Bootstrap + Sass + Vite (recommended for component-first)

npm create vite@latest my-weekend-site -- --template vanilla
cd my-weekend-site
npm install
npm install bootstrap @popperjs/core sass

Create a Sass entry point that overrides Bootstrap's variables before importing it β€” this is the correct, maintainable way to customize Bootstrap:

// src/scss/main.scss

// 1. Bootstrap functions first, so color helpers are available
@import "bootstrap/scss/functions";

// 2. YOUR overrides (this is the customization layer)
$primary:   #5c6ac4;
$success:   #47d185;
$border-radius: 0.5rem;
$font-family-base: "Montserrat", sans-serif;
$headings-font-family: "Playfair Display", serif;

// 3. The rest of Bootstrap, which now picks up your values
@import "bootstrap/scss/variables";
@import "bootstrap/scss/maps";
@import "bootstrap/scss/mixins";
@import "bootstrap/scss/root";
@import "bootstrap/scss/bootstrap";

// 4. Your own partials, layered on top
@import "components/buttons";
@import "layout/header";
// src/main.js β€” import your styles and Bootstrap's JS bundle
import './scss/main.scss';
import * as bootstrap from 'bootstrap';

βœ… Milestone 0 is done when…

You run npm run dev, open the local URL, and see a page where a heading uses your custom brand font/color. If your token change shows up on screen, the whole pipeline β€” preprocessor/framework β†’ build β†’ browser β€” is proven. Commit now.

⚠️ Common M0 trap: customizing by writing override CSS after the framework (.btn-primary { background: … !important; }). That fights the framework and multiplies your CSS. Always customize through the token layer β€” Bootstrap variables or Tailwind's @theme/config β€” before the framework generates its classes.

Milestone 1 β€” Tokens & Layout

Goal: a consistent visual system and a shared page shell (header + nav + footer) that every page reuses.

Define your design tokens once

Before styling anything specific, decide your palette, type scale, and spacing β€” and encode them as tokens. Everything else references these, so a single change ripples everywhere. That is exactly what preprocessor variables and framework theme config are for.

// src/scss/abstracts/_tokens.scss  (Sass)
$brand:        #5c6ac4;
$brand-ink:    #1f2540;
$surface:      #ffffff;
$radius:       0.5rem;
$space-1:      0.5rem;
$space-2:      1rem;
$space-3:      2rem;

// A reusable mixin keeps focus styles consistent & accessible
@mixin focus-ring {
  outline: 2px solid $brand;
  outline-offset: 2px;
}

Build the shared shell

Every page repeats the same header, nav, and footer. Structure it once as a set of partials/components so the three-to-five pages stay identical where they should be. A quick component map keeps you honest about what you're building:

The shared page shell A page is composed of a shared header with navigation at the top, a page-specific main content area in the middle, and a shared footer at the bottom. Header + Nav shared Β· reused Main content page-specific: hero, cards, sections… this is the part that changes per page Footer shared Β· reused
Figure 1 β€” Only the middle changes from page to page. Build the header, nav, and footer once and reuse them so the site feels like one coherent product.

βœ… Milestone 1 is done when…

All your pages render with an identical header/footer, your nav links between them, and every color, font, and spacing value traces back to a token β€” no magic hex codes scattered through the markup.

Milestone 2 β€” Interactive Components

Goal: three working, accessible interactive components. Build them one at a time, and test each on a phone-sized viewport before moving on.

Component 1 β€” Responsive nav with a mobile menu

A hamburger toggle that reveals the nav on small screens. Keep it accessible: the button needs an aria-expanded state and the toggle must be keyboard-operable.

const toggle = document.querySelector('.nav-toggle');
const menu = document.querySelector('#nav-menu');

toggle.addEventListener('click', () => {
  const isOpen = toggle.getAttribute('aria-expanded') === 'true';
  toggle.setAttribute('aria-expanded', String(!isOpen));
  menu.classList.toggle('is-open');
});

Component 2 β€” Accordion (or tabs)

Great for an FAQ or a menu. The native <details> element gives you an accessible, keyboard-friendly accordion with zero JavaScript β€” reach for it before writing your own:

<details class="faq">
  <summary>Do you take walk-ins?</summary>
  <p>Yes β€” walk-ins are welcome, but reservations are recommended on weekends.</p>
</details>

Component 3 β€” Modal dialog

Use the native <dialog> element β€” it handles focus trapping, Esc to close, and the backdrop for you, which is a lot of accessibility work you'd otherwise hand-roll:

<button id="open-signup">Join the waitlist</button>

<dialog id="signup">
  <form method="dialog">
    <h2>Join the waitlist</h2>
    <p>We'll email you when a table opens up.</p>
    <button value="close">Close</button>
  </form>
</dialog>
const dialog = document.querySelector('#signup');
document.querySelector('#open-signup')
  .addEventListener('click', () => dialog.showModal());

πŸ’‘ Prefer the platform

Notice how much you get for free from <details> and <dialog>. Modern HTML ships accessible, well-tested versions of components that frameworks used to reinvent. Reach for a framework's JS component when you need behavior the platform doesn't offer β€” not by default.

βœ… Milestone 2 is done when…

All three components work with a mouse and a keyboard, look right at 375 px wide, and produce no console errors. Tab through the whole page β€” you should always be able to see where focus is.

Milestone 3 β€” The Form

Goal: one accessible contact form with real validation and clear, specific error messages. Lean on the browser's built-in constraint validation before adding custom JavaScript.

<form id="contact" novalidate>
  <div class="field">
    <label for="name">Full name</label>
    <input id="name" name="name" type="text" required autocomplete="name">
    <p class="error" data-for="name" role="alert"></p>
  </div>

  <div class="field">
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required autocomplete="email">
    <p class="error" data-for="email" role="alert"></p>
  </div>

  <div class="field">
    <label for="message">Message</label>
    <textarea id="message" name="message" rows="5" required></textarea>
    <p class="error" data-for="message" role="alert"></p>
  </div>

  <button type="submit">Send message</button>
</form>

The novalidate attribute turns off the browser's default popups so you can render your own consistent messages, while still using the constraint API to decide validity:

const form = document.querySelector('#contact');

form.addEventListener('submit', (event) => {
  event.preventDefault();
  let firstInvalid = null;

  for (const field of form.querySelectorAll('input, textarea')) {
    const error = form.querySelector(`.error[data-for="${field.name}"]`);
    if (!field.checkValidity()) {
      error.textContent = field.validationMessage;
      field.setAttribute('aria-invalid', 'true');
      firstInvalid ??= field;
    } else {
      error.textContent = '';
      field.removeAttribute('aria-invalid');
    }
  }

  if (firstInvalid) {
    firstInvalid.focus(); // move focus to the first problem
  } else {
    // All good β€” here you'd POST to your backend / form service
    form.reset();
    alert('Thanks! Your message has been sent.');
  }
});

πŸ“– Why role="alert" and moving focus?

A screen reader announces the content of a role="alert" region the moment it changes, so a blind user hears the error. Moving focus to the first invalid field means keyboard users aren't left guessing where the problem is. Accessible validation isn't extra β€” it's the difference between a form that works for everyone and one that works for some.

βœ… Milestone 3 is done when…

Submitting an empty form shows a specific message under each field, focus jumps to the first problem, and a valid submission clears and confirms. No browser default popups, no silent failures.

Milestone 4 β€” Optimize & Ship

Goal: a production build with unused CSS purged and assets minified, deployed to a public URL. A framework's full stylesheet is large; the whole point of a modern build is to ship only what you actually use.

Build for production

# Vite produces an optimized, minified build in dist/
npm run build

# Preview the production build locally before deploying
npm run preview

πŸ’‘ Where the savings come from

Tailwind only generates the utility classes your markup actually references, so a production Tailwind stylesheet is typically a few kilobytes. With Bootstrap-via-Sass, import only the components you use instead of the whole bootstrap bundle. Either way, Vite minifies CSS and JS and fingerprints filenames for caching β€” you get this for free from npm run build.

Deploy

Netlify, Vercel, GitHub Pages, or Cloudflare Pages all host a static dist/ folder for free. The fastest path with Netlify:

# One-time
npm install -g netlify-cli

# From the project root, after npm run build
netlify deploy --prod --dir=dist

Then run Lighthouse (in Chrome DevTools) against the live URL and note your four scores. You'll reference them in your write-up.

βœ… Milestone 4 is done when…

Your site is live at a public URL, the production CSS is a fraction of the framework's full size, and Lighthouse shows no glaring red. Ship it, then do the checklist below before you call it finished.

Definition-of-Done Checklist

"Done" is not a feeling β€” it's a list. Walk through this before you consider the project complete. If every box is checked, you're finished; if not, you know exactly what's left.

βœ… Build & structure

  • A preprocessor is in the pipeline and used for variables + nesting + at least one partial or mixin
  • The framework is customized through its token/variable layer, not override CSS
  • 3–5 pages share one header, nav, and footer

βœ… Interactivity & forms

  • Three interactive components work with mouse and keyboard
  • The contact form validates, shows per-field errors, and moves focus to the first problem
  • Zero errors in the browser console

βœ… Responsive & accessible

  • Everything is usable and readable at 375 px, 768 px, and 1280 px wide
  • Every interactive element has a visible focus style
  • Images have meaningful alt text; color contrast passes AA

βœ… Shipped

  • Production build purges unused CSS and minifies output
  • The site is deployed to a public URL
  • A short README notes the stack, your two stretch goals, and your Lighthouse scores

What Good Looks Like

Meeting the checklist gets you a passing project. This section describes the difference between "it works" and "this is genuinely good" β€” the bar to aim for, and the mistakes that quietly drag a project down.

DimensionJust okayWhat good looks like
Framework use Stock framework look; customization done with !important overrides Customized through tokens so it doesn't look like default Bootstrap/Tailwind, yet still uses the framework's grid and components
Consistency Colors and spacing vary page to page One token set drives everything; the site feels like a single product
Interactivity Works with a mouse only; breaks on keyboard Keyboard-operable, visible focus, announced errors, respects reduced-motion
Responsiveness "Looks fine on my laptop" Deliberately tested at phone, tablet, and desktop widths; no horizontal scroll
Shipping Runs locally; huge unpurged CSS Deployed, purged, minified; strong Lighthouse scores you can point to

⚠️ The three most common ways this project goes wrong

  • Scope creep β€” building ten unfinished pages instead of four polished ones. Cut features, not quality.
  • Fighting the framework β€” piling override CSS on top instead of customizing tokens underneath. If you're writing !important, stop and go to the token layer.
  • Skipping the ship step β€” "it works on my machine" isn't done. The purge, the deploy, and the Lighthouse pass are part of the project, not extras.

πŸ’‘ Reflect before you submit

In two or three sentences each, jot down: Which token change had the biggest visual payoff? Where did the framework save you the most time? What's the one thing you'd redo with another hour? Keeping a short reflection turns a one-off project into a repeatable method.

Summary & Quiz

πŸŽ‰ Key Takeaways

  • Scope small, finish fully. A tight, deployed 3–5 page site beats a sprawling unfinished one.
  • Customize through tokens β€” framework variables and preprocessor variables β€” instead of fighting the framework with override CSS.
  • Prefer the platform for interactivity: <details>, <dialog>, and the constraint validation API give you accessible behavior for free.
  • Shipping is part of the build: purge, minify, deploy, and measure with Lighthouse.
  • "Done" is a checklist, and "good" is a higher bar worth aiming for.

🎯 Quick Quiz

Question 1: What is the recommended way to change a framework's default button color to your brand color?

Question 2: Why favor the native <dialog> element for the project's modal?

Question 3: Which single choice most reliably keeps a weekend project on track?

πŸ“š Further Reading

πŸš€ What's Next?

You've closed out the CSS half of the course with a shipped project. Next, Module 8 pivots to the language that makes pages come alive β€” we start with the history and evolution of JavaScript, so you understand why the language looks the way it does before you dive into its syntax.

πŸŽ‰ Project complete!

You scaffolded, customized, built, optimized, and shipped. That end-to-end loop β€” not any one framework β€” is the real skill you'll reuse on every project from here on.