Developing Full Stack Next.js Web Applications

Chapter 2 — Styling User Interfaces with CSS and Tailwind

Dr. Jose Annunziato

Chapter 1 built Kambaz screens with plain HTML — functional, but visually flat. Browsers apply only minimal default styling to raw tags, which is why every heading, paragraph, and list rendered in the browser's stock black-on-white look. This chapter introduces CSS (Cascading Style Sheets), the language browsers use to control color, spacing, borders, layout, and responsiveness — everything beyond the browser's fixed default look for each tag.

You will practice CSS in layers of increasing convenience. First, plain CSS: the style attribute, external style sheets, and selectors that target specific tags, ids, and classes. Once the fundamentals click, the chapter introduces Tailwind CSS, a utility-class library you compose directly in JSX. Along the way you will also decorate the UI with React Icons, a library of icon components gathered from several icon families.

The chapter closes by returning to Kambaz: styling the components you already extracted in Chapter 1 (CourseCard, Module, Lesson, AssignmentItem) with Tailwind, and replacing table layouts with CSS so Navigation, Dashboard, Modules, Home, and People start to resemble the target product — Dashboard (Figure 2a), Home (Figure 2b), People (Figure 2c), and Account Sign in (Figure 2d). Several screens are left as guided, self-directed exercises — by now you have the tools to style them without a step-by-step walkthrough.

The chapter's objectives are best achieved by building along with the narration — each Lab 2 sample and Kambaz restyle as it appears — rather than reading first and coding later. Glance at the Lab 2 checklist in §2.3.7 and the Kambaz checklist in §2.4.10 so the expected coverage is visible from the start. Those lists are recaps, not a reason to skip ahead: work through each section, then use them to confirm what stuck.

Styled Kambaz Dashboard target
Figure 2a — Dashboard Screen
Styled Kambaz Home target
Figure 2b — Home Screen
Styled Kambaz People target
Figure 2c — People Screen
Styled Kambaz Account Sign in target
Figure 2d — Account Sign in Screen

2.1 Styling React Components with CSS

SlidesVideos

Lab 1 structured the page; the browser still painted it in default black on white. The goal here is to take control of that look — color, spacing, and layout — first on a single tag, then from a CSS file whose selectors can restyle many elements at once.

Keep working in the same webdev-client project from Chapter 1. Under app/labs, create a new directory called lab2 and add page.tsx to hold the exercises, mirroring the structure you already used for app/labs/lab1:

mkdir app/labs/lab2

Start app/labs/lab2/page.tsx the same way Lab 1 started — a single top-level component you will grow one exercise at a time:

Lab2app/labs/lab2/page.tsx
export default function Lab2() {
  return (
    <div id="wd-lab2">
      <h2>Lab 2 - Cascading Style Sheets</h2>
    </div>
  );
}

Add a link to the new lab in both app/labs/page.tsx and app/labs/TOC.tsx, the same two files you updated for Lab 1 in §1.3.10–§1.3.11. Confirm you can reach http://localhost:3000/labs/lab2 from the Labs table of contents before continuing. A coverage checklist for Lab 2 is in §2.3.7 — use it after you have walked through the samples, not instead of building them as you read.

2.1.1 Styling HTML Tags with the Style Attribute

Videos

Every HTML tag accepts a style attribute — the same name/value pattern introduced with id in §1.3.1 — that configures the look and feel of that one tag directly. In JSX the value is not a plain string like in HTML — it is a JavaScript object literal (hence the double curly braces: one pair to enter a JSX expression, one pair for the object itself), and property names are camelCase instead of hyphenated, so background-color becomes backgroundColor.

<p style={{ backgroundColor: "blue", color: "white" }}>
  ...
</p>

Add a styled paragraph to app/labs/lab2/page.tsx warning readers about the very technique it demonstrates — the style attribute is convenient for a quick experiment, but scattering styles across individual tags makes a real project hard to maintain, which is why the rest of this section moves styling into separate CSS files instead:

Lab2app/labs/lab2/page.tsx
export default function Lab2() {
  return (
    <div id="wd-lab2">
      <h2>Lab 2 - Cascading Style Sheets</h2>
      <h3>Styling with the STYLE attribute</h3>
      <p style={{ backgroundColor: "blue", color: "white" }}>
        Style attribute allows configuring look and feel right on the
        element. Although it&apos;s very convenient it is considered bad
        practice and you should avoid using the style attribute
      </p>
    </div>
  );
}

The paragraph's background turns blue and its text turns white — styling applied directly on the element, with no separate CSS file involved:

StyleAttributeapp/labs/lab2/page.tsx

Lab 2 - Cascading Style Sheets

Styling with the STYLE attribute

Style attribute allows configuring look and feel right on the element. Although it's very convenient it is considered bad practice and you should avoid using the style attribute

The book's Lab 2 demos use a sans-serif font so they match the rest of this page. Your own Lab 2 page will most likely render in a serif font such as Times. That's expected: you commented out globals.css in §1.2.5, so Lab 2 gets the browser's default font. Lab 2 never sets a font, and the colors, borders, and layout are what to compare against the demos.

2.1.2 Importing CSS Documents from React

Videos

The recommended alternative to inline styles is a dedicated CSS file: a plain text document you import into a component, the same way you import a React component or a library. Create app/labs/lab2/index.css alongside page.tsx with this content:

Lab2 stylesapp/labs/lab2/index.css
p {
  background-color: green;
  color: white;
}

The code snippet above is referred to as a CSS rule which consists of the following parts:

  • The selector is everything before the opening curly brace — here, the tag name p. The selector is used to refer to the HTML elements that will be styled.
  • The curly braces wrap a declaration block which contains the list of styles to apply to the selected elements.
  • Each line inside the block is a declaration which consists of a property (what to change), a colon, a value (what to change it to), and a semicolon. So background-color is the property and green is the value.

In a CSS file, property names use hyphens — background-color — that is the original CSS syntax. The JSX style object in §2.1.1 is not a CSS document. It is a JavaScript object, so an unquoted key must be a valid identifier. Hyphens are not allowed in identifiers, which is why that object used camelCase: backgroundColor. You can keep the hyphenated CSS name in JavaScript if you quote the key, as in { "background-color": "green" }. Unquoted camelCase is the usual convention in React.

A rule, then, is a selector plus its declaration block. Later sections introduce new kinds of selectors and many more properties; they still assemble into this same shape.

Remove the style attribute from the paragraph and import index.css at the top of page.tsx instead:

Lab2app/labs/lab2/page.tsx
import "./index.css";

export default function Lab2() {
  return (
    <div id="wd-lab2">
      <h2>Lab 2 - Cascading Style Sheets</h2>
      <h3>Styling with the STYLE attribute</h3>
      <p>
        Style attribute allows configuring look and feel right on the
        element. Although it&apos;s very convenient it is considered bad
        practice and you should avoid using the style attribute
      </p>
    </div>
  );
}

Even though the paragraph no longer has a style attribute, it now has a green background and white text — the p selector in index.css reaches every paragraph on the page, current and future, without touching the markup again:

CssImportapp/labs/lab2/index.css

Lab 2 - Cascading Style Sheets

Styling with the STYLE attribute

Style attribute allows configuring look and feel right on the element. Although it's very convenient it is considered bad practice and you should avoid using the style attribute

A tag selector is the broadest option: it matches every instance of that tag in the document, which is powerful but blunt. The next three sections introduce narrower selectors so you can style one element, or a chosen group, without restyling every p on the page.

One catch: Next.js treats every CSS file you import as global. Once Lab 2 has loaded, its rules stay in the browser when you click a next/link link to another page, so a bare h3 rule would also restyle the headings in Lab 1 and Kambaz until you reload. For the practice rules below, write the tag after #wd-lab2, the id of the Lab 2 wrapper div, as in #wd-lab2 h3. That limits the rule to tags inside Lab 2. §2.1.3 explains the # and §2.1.5 explains the space between the two selectors. The sample p rule is only temporary: §2.1.3 comments it out.

2.1.3 Selecting HTML Content with CSS ID Selectors

Videos

Instead of restyling every paragraph on the page, an ID selector targets one specific tag by its id attribute — the same unique identifier introduced for styling and DOM lookups back in §1.3.1. An ID selector is written as the tag name, a #, and the id value. Comment out the blanket p rule in index.css and add two ID-scoped rules instead:

Lab2 stylesapp/labs/lab2/index.css
/* p {
  background-color: green;
  color: white;
} */

p#wd-id-selector-1 {
  background-color: red;
  color: white;
}
p#wd-id-selector-2 {
  background-color: yellow;
  color: black;
}

Add two paragraphs carrying those ids to page.tsx, inside a wrapping div so the exercise stays organized as the file grows:

Lab2app/labs/lab2/page.tsx
<div id="wd-css-id-selectors">
  <h3>ID selectors</h3>
  <p id="wd-id-selector-1">
    Instead of changing the look and feel of all the
    elements of the same name, e.g., P, we can refer to a
    specific element by its ID
  </p>
  <p id="wd-id-selector-2">
    Here&apos;s another paragraph using a different ID and a
    different look and feel
  </p>
</div>

Each paragraph now carries its own color scheme — red-on-white for the first id, yellow-on-black for the second — while any other paragraph on the page is unaffected:

IdSelectorsapp/labs/lab2/page.tsx

Lab 2 - Cascading Style Sheets

ID selectors

Instead of changing the look and feel of all the elements of the same name, e.g., P, we can refer to a specific element by its ID

Here's another paragraph using a different ID and a different look and feel

2.1.4 Selecting HTML Content with CSS Class Selectors

Videos

An id is unique — only one element on the page should carry a given id. When several elements, even of different tag types, should share the same look, use a class selector instead: a name prefixed with a dot (.) in CSS, applied through the className attribute in JSX (React reserves the plain class keyword for JavaScript classes, so JSX renames the attribute). Append a class rule to index.css:

Lab2 stylesapp/labs/lab2/index.css
.wd-class-selector {
  background-color: yellow;
  color: blue;
}

Apply that one class to both a paragraph and a heading in page.tsx:

Lab2app/labs/lab2/page.tsx
<div id="wd-css-class-selectors">
  <h3>Class selectors</h3>
  <p className="wd-class-selector">
    Instead of using IDs to refer to elements, you can use an
    element&apos;s CLASS attribute
  </p>
  <h4 className="wd-class-selector">
    This heading has same style as paragraph above
  </h4>
</div>

Both the paragraph and the heading — two different tag types — pick up the identical yellow background and blue text, because both carry the same class:

ClassSelectorsapp/labs/lab2/page.tsx

Lab 2 - Cascading Style Sheets

Class selectors

Instead of using IDs to refer to elements, you can use an element's CLASS attribute

This heading has same style as paragraph above

2.1.5 Selecting HTML Content Based on the Document Structure

Videos

Selectors can also be combined to target tags by their position in the document tree. Separating two class names with a space, as in .wd-selector-1 .wd-selector-3, matches any element with wd-selector-3 nested anywhere inside an element with wd-selector-1 — a descendant relationship, no matter how deeply nested. Separating them with a >, as in .wd-selector-2 > .wd-selector-3, is stricter: it only matches a direct child. Nest four elements in page.tsx to see both relationships in action:

Lab2app/labs/lab2/page.tsx
<div id="wd-css-document-structure">
  <div className="wd-selector-1">
    <h3>Document structure selectors</h3>
    <div className="wd-selector-2">
      Selectors can be combined to refer elements in particular
      places in the document
      <p className="wd-selector-3">
        This paragraph&apos;s red background is referenced as
        <br />
        .wd-selector-1 .wd-selector-3
        <br />
        meaning the descendant of some ancestor.
        <br />
        <span className="wd-selector-4">
          Whereas this span is a direct child of its parent
        </span>
        <br />
        You can combine these relationships to create specific
        styles depending on the document structure
      </p>
    </div>
  </div>
</div>

Style .wd-selector-3 as a descendant of .wd-selector-1 (any depth), and .wd-selector-4 only when it is a direct child of .wd-selector-3, which in turn must be a direct child of .wd-selector-2:

Lab2 stylesapp/labs/lab2/index.css
.wd-selector-1 .wd-selector-3 {
  background-color: red;
  color: white;
}
.wd-selector-2 > .wd-selector-3 > .wd-selector-4 {
  background-color: yellow;
  color: blue;
}

The paragraph renders with a red background from the first, broader descendant rule, while the span nested directly inside it switches to yellow-on-blue from the second, stricter child rule. The first rule matches the paragraph and the second matches only the span, so the span shows its own colors on top of the red paragraph:

DocumentStructureSelectorsapp/labs/lab2/page.tsx

Lab 2 - Cascading Style Sheets

Document structure selectors

Selectors can be combined to refer elements in particular places in the document

This paragraph's red background is referenced as
.wd-selector-1 .wd-selector-3
meaning the descendant of some ancestor.
Whereas this span is a direct child of its parent
You can combine these relationships to create specific styles depending on the document structure

2.1.6 CSS Selection Rule Mechanism

Videos

By the time a browser paints an element, several rules may compete to set the same property — a browser default, a tag selector, a class, an id. The "Cascading" in Cascading Style Sheets describes exactly how the browser picks a winner:

  1. Specificity: more precise selectors win over more general ones. An id selector beats a class selector, which beats a plain tag selector, which beats the browser's built-in default.
  2. Source order: if two rules have the same specificity, the rule declared later in the CSS wins. Order only breaks ties. The id selectors in §2.1.3 outrank the p tag selector from §2.1.2 no matter where they appear in the file, so they would win even if that tag rule came after them.
  3. Inheritance: some properties, like color and other font properties, pass down from a parent element to its children automatically unless overridden. Layout properties like width and margin do not inherit — each element needs its own rule.

Keep this order in mind whenever a style you expect to see does not appear: check first whether a more specific selector elsewhere is winning, then whether a later rule with equal specificity is overriding it.

2.1.7 Styling an HTML Tag's Foreground Color with CSS

SlidesVideos

The color property sets an element's foreground (text) color. CSS accepts colors in several notations: named colors such as blue or red; hexadecimal triples such as #7070ff (red, green, and blue intensity, two digits each); or the functional rgb(12, 34, 56) form. From this exercise on, move each new demo into its own file under app/labs/lab2/. One exercise per component keeps page.tsx manageable, the same organization used for Lab 1's HTML exercises.

Start with ForegroundColors.tsx:

Lab2 stylesapp/labs/lab2/index.css
.wd-fg-color-black { color: black; }
.wd-fg-color-white { color: white; }
.wd-fg-color-blue { color: #7070ff; }
.wd-fg-color-red { color: #ff7070; }
.wd-fg-color-green { color: green; }
ForegroundColorsapp/labs/lab2/ForegroundColors.tsx
export default function ForegroundColors() {
  return (
    <div id="wd-css-colors">
      <h2>Colors</h2>
      <h3 className="wd-fg-color-blue">Foreground color</h3>
      <p className="wd-fg-color-red">
        The text in this paragraph is red but{" "}
        <span className="wd-fg-color-green">this text is green</span>
      </p>
    </div>
  );
}

The heading renders blue, the paragraph text red, and the nested span green — three foreground colors applied through three separate class selectors on three different elements:

ForegroundColorsapp/labs/lab2/ForegroundColors.tsx

Colors

Foreground color

The text in this paragraph is red but this text is green

Now import each demo into page.tsx. Every later demo component gets the same two lines, an import at the top and a tag inside #wd-lab2. §2.3.7 shows the finished page.tsx with all of them:

Lab2app/labs/lab2/page.tsx
import "./index.css";
import ForegroundColors from "./ForegroundColors";

export default function Lab2() {
  return (
    <div id="wd-lab2">
      <h2>Lab 2 - Cascading Style Sheets</h2>
      {/* ...earlier exercises... */}
      <ForegroundColors />
    </div>
  );
}

2.1.8 Styling an HTML Tag's Background Color with CSS

Videos

The background-color property works the same way, just for the area behind the content instead of the text itself. Add a matching set of background classes and a BackgroundColors.tsx component that stacks background and foreground classes on the same element:

Lab2 stylesapp/labs/lab2/index.css
.wd-bg-color-yellow { background-color: #ffff07; }
.wd-bg-color-blue { background-color: #7070ff; }
.wd-bg-color-red { background-color: #ff7070; }
.wd-bg-color-green { background-color: green; }
.wd-bg-color-gray { background-color: lightgray; }
BackgroundColorsapp/labs/lab2/BackgroundColors.tsx
export default function BackgroundColors() {
  return (
    <div id="wd-css-background-colors">
      <h2 className="wd-bg-color-blue wd-fg-color-white">Background color</h2>
      <p className="wd-bg-color-red wd-fg-color-black">
        This background of this paragraph is red but{" "}
        <span className="wd-bg-color-green wd-fg-color-white">
          the background of this text is green and the foreground white
        </span>
      </p>
    </div>
  );
}

Each element now applies two classes at once — one for background, one for foreground — which is exactly how className composes multiple selectors: list them space-separated and every matching rule's properties apply together:

BackgroundColorsapp/labs/lab2/BackgroundColors.tsx

Background color

This background of this paragraph is red but the background of this text is green and the foreground white

2.1.9 Styling an HTML Tag's Borders with CSS

Videos

Borders are configured with border-width, border-style (solid, dotted, dashed, double, …), and border-color. Declare each aspect of a border as its own reusable class so demos can mix and match a width, a style, and a color independently:

Lab2 stylesapp/labs/lab2/index.css
.wd-border-fat { border-width: 20px 30px 20px 30px; }
.wd-border-thin { border-width: 4px; }
.wd-border-solid { border-style: solid; }
.wd-border-dashed { border-style: dashed; }
.wd-border-yellow { border-color: #ffff07; }
.wd-border-red { border-color: #ff7070; }
.wd-border-blue { border-color: #7070ff; }
Bordersapp/labs/lab2/Borders.tsx
export default function Borders() {
  return (
    <div id="wd-css-borders">
      <h2>Borders</h2>
      <p className="wd-border-fat wd-border-red wd-border-solid">
        Solid fat red border
      </p>
      <p className="wd-border-thin wd-border-blue wd-border-dashed">
        Dashed thin blue border
      </p>
    </div>
  );
}

The first paragraph combines the fat width, red color, and solid style classes into one thick red border; the second combines thin, blue, and dashed into a completely different look — three small classes, many combinations:

Bordersapp/labs/lab2/Borders.tsx

Borders

Solid fat red border

Dashed thin blue border

2.1.10 Styling an HTML Tag's Padding, Margins, and Box Model with CSS

SlidesVideos

Padding is the space between an element's content and its border; margin is the space outside the border, separating the element from its neighbors. Both can be set on all four sides at once, or per side with -top, -right, -bottom, and -left suffixes. Add padding classes and a Padding.tsx that reuses the border and background classes from the previous two exercises so the padded regions are easy to see:

Lab2 stylesapp/labs/lab2/index.css
.wd-padded-top-left {
  padding-top: 50px;
  padding-left: 50px;
}
.wd-padded-bottom-right {
  padding-bottom: 50px;
  padding-right: 50px;
}
.wd-padding-fat {
  padding: 50px;
}
Paddingapp/labs/lab2/Padding.tsx
export default function Padding() {
  return (
    <div id="wd-css-paddings">
      <h2>Padding</h2>
      <div className="wd-padded-top-left wd-border-fat wd-border-red wd-border-solid wd-bg-color-yellow">
        Padded top left
      </div>
      <div className="wd-padded-bottom-right wd-border-fat wd-border-blue wd-border-solid wd-bg-color-yellow">
        Padded bottom right
      </div>
      <div className="wd-padding-fat wd-border-fat wd-border-yellow wd-border-solid wd-bg-color-blue wd-fg-color-white">
        Padded all around
      </div>
    </div>
  );
}

Each box's text sits a fixed distance from its border — only on the top and left for the first box, only bottom and right for the second, and evenly on all sides for the third:

Paddingapp/labs/lab2/Padding.tsx

Padding

Padded top left
Padded bottom right
Padded all around

Margin classes follow the identical pattern, just pushing neighboring content away from the outside of the border instead of the content in from the inside:

Lab2 stylesapp/labs/lab2/index.css
.wd-margin-bottom {
  margin-bottom: 50px;
}
.wd-margin-right-left {
  margin-left: 50px;
  margin-right: 50px;
}
.wd-margin-all-around {
  margin: 30px;
}
Marginsapp/labs/lab2/Margins.tsx
export default function Margins() {
  return (
    <div id="wd-css-margins">
      <h2>Margins</h2>
      <div className="wd-margin-bottom wd-padded-top-left wd-border-fat wd-border-red wd-border-solid wd-bg-color-yellow">
        Margin bottom
      </div>
      <div className="wd-margin-right-left wd-padded-bottom-right wd-border-fat wd-border-blue wd-border-solid wd-bg-color-yellow">
        Margin left right
      </div>
      <div className="wd-margin-all-around wd-padding-fat wd-border-fat wd-border-yellow wd-border-solid wd-bg-color-blue wd-fg-color-white">
        Margin all around
      </div>
    </div>
  );
}

Compare the gaps between these three boxes to the padding demo above — the space now shows up outside each box's border, pushing the boxes apart from one another rather than pushing the text inward:

Marginsapp/labs/lab2/Margins.tsx

Margins

Margin bottom
Margin left right
Margin all around

Padding and margin are two layers of a larger picture: the CSS box model. Every element is a box made of four concentric layers, from the inside out: content (the text or children), padding (space between content and border), border, then margin (space outside the border). Background color fills the content and padding; the border sits on top of that edge; margin is transparent — you see whatever is behind the gap.

The figure is that box the way DevTools draws it in the Computed pane: a tan margin (#F9CC9D), a yellow border (#FDDD9B), a green padding ring (#C3D08B), and a blue content box (#8CB6C0). It illustrates the content-box sample in the exercise below. Study the picture, then copy the Lab 2 files that follow.

Box model — four layers

Those layers also change how width is measured: the box-sizing property chooses the rule. The CSS default is content-box: width: 200px sizes only the content, then padding and border add extra pixels outside it. border-box counts padding and border inside the 200px, so the box you see on screen stays 200px wide. Layout math is much easier with border-box, which is why many style resets (and Tailwind later) set it globally. Add the box-model classes and a BoxModel.tsx that puts margin, border, padding, and content on one box inside a gray parent, then two boxes that share the same width, height, padding, and border but differ only in box-sizing:

Lab2 stylesapp/labs/lab2/index.css
.wd-box-model-parent {
  display: flow-root;
  background-color: lightgray;
  padding-top: 8px;
}
.wd-box-model-margin-label,
.wd-box-model-border-label,
.wd-box-model-padding-label {
  font-size: 12px;
  line-height: 12px;
}
.wd-box-model-box {
  position: relative;
  margin: 20px;
  padding: 20px;
  border: 10px solid #c41e3a;
  background-color: #cfe2ff;
}
.wd-box-model-border-label {
  position: absolute;
  top: -24px;
  left: 0;
  color: #c41e3a;
}
.wd-box-model-margin-label {
  position: absolute;
  bottom: -26px;
  left: -20px;
}
.wd-box-model-padding-label {
  position: absolute;
  top: 2px;
  left: 6px;
}
.wd-box-model-content {
  background-color: #d1e7dd;
  padding: 8px;
}
.wd-box-sizing-demo {
  background-color: lightgray;
  padding: 10px;
}
.wd-box-sizing-content,
.wd-box-sizing-border {
  width: 200px;
  height: 180px;
  padding: 20px;
  border: 10px solid #c41e3a;
  background-color: #ffff07;
  margin-bottom: 10px;
}
.wd-box-sizing-content {
  box-sizing: content-box;
}
.wd-box-sizing-border {
  box-sizing: border-box;
}
BoxModelapp/labs/lab2/BoxModel.tsx
export default function BoxModel() {
  return (
    <div id="wd-css-box-model">
      <h2>Box model</h2>
      <div className="wd-box-model-parent">
        <div>parent background (shows through the margin)</div>
        <div className="wd-box-model-box">
          <span className="wd-box-model-border-label">border (the red ring)</span>
          <span className="wd-box-model-padding-label">padding</span>
          <div className="wd-box-model-content">content</div>
          <span className="wd-box-model-margin-label">
            margin: the 20px gray gap (transparent)
          </span>
        </div>
      </div>
      <h3>box-sizing</h3>
      <div className="wd-box-sizing-demo">
        <div className="wd-box-sizing-content">
          content-box: width 200px plus padding and border
        </div>
        <div className="wd-box-sizing-border">
          border-box: width 200px includes padding and border
        </div>
      </div>
    </div>
  );
}

On the left, right, and bottom, the gray around the blue box is exactly that box's 20px margin. It is transparent, so the parent background shows through. The margin label sits in the bottom gap. The red words sit in the top margin, just outside the red ring, which is the border. The light blue area is the padding, and the green child is the content. Below that, both yellow boxes set margin-bottom: 10px (the other margins stay 0), a 10px border, 20px of padding, width: 200px, and height: 180px. Under content-boxthose lengths are the content box, so that box's content is 200×180 and it measures 260×240 on screen (200 + 40 padding + 20 border wide, and 180 + 40 + 20 tall). The border-box box uses the same width, height, padding, and border, so its content box is 140×120 (200 − 40 − 20 wide, and 180 − 40 − 20 tall) and the painted box stays 200×180. The figure above uses those content-box numbers: margin 0, 0, 10, 0, border 10, padding 20, content 200×180:

BoxModelapp/labs/lab2/BoxModel.tsx

Box model

parent background (shows through the margin)
border (the red ring)padding
content
margin: the 20px gray gap (transparent)

box-sizing

content-box: width 200px plus padding and border
border-box: width 200px includes padding and border

2.1.11 Styling an HTML Tag's Corners with CSS

Videos

The border-radius property rounds an element's corners. Set all four at once, or list four values (top-left, top-right, bottom-right, bottom-left) to round each corner by a different amount, or target one corner directly with properties like border-top-left-radius:

Lab2 stylesapp/labs/lab2/index.css
.wd-rounded-corners-top {
  border-top-left-radius: 40px;
  border-top-right-radius: 40px;
}
.wd-rounded-corners-bottom {
  border-bottom-left-radius: 40px;
  border-bottom-right-radius: 40px;
}
.wd-rounded-corners-all-around {
  border-radius: 50px;
}
.wd-rounded-corners-inline {
  border-radius: 30px 0px 20px 50px;
}
Cornersapp/labs/lab2/Corners.tsx
export default function Corners() {
  return (
    <div id="wd-css-corners">
      <h2>Rounded corners</h2>
      <p className="wd-rounded-corners-top wd-border-thin wd-border-blue wd-border-solid wd-padding-fat">
        Rounded corners on the top
      </p>
      <p className="wd-rounded-corners-bottom wd-border-thin wd-border-blue wd-border-solid wd-padding-fat">
        Rounded corners at the bottom
      </p>
      <p className="wd-rounded-corners-all-around wd-border-thin wd-border-blue wd-border-solid wd-padding-fat">
        Rounded corners all around
      </p>
      <p className="wd-rounded-corners-inline wd-border-thin wd-border-blue wd-border-solid wd-padding-fat">
        Different rounded corners
      </p>
    </div>
  );
}

The four paragraphs each round a different combination of corners — top only, bottom only, all four evenly, and all four by different amounts on the last one:

Cornersapp/labs/lab2/Corners.tsx

Rounded corners

Rounded corners on the top

Rounded corners at the bottom

Rounded corners all around

Different rounded corners

2.1.12 Styling an HTML Tag's Dimensions and Display with CSS

SlidesVideos

By default, block-level elements — div, headings, paragraphs — stretch to fill the full width of their parent container. The width and height properties override that default so an element only occupies the space you specify:

Lab2 stylesapp/labs/lab2/index.css
.wd-dimension-portrait {
  width: 75px;
  height: 100px;
}
.wd-dimension-landscape {
  width: 100px;
  height: 75px;
}
.wd-dimension-square {
  width: 75px;
  height: 75px;
}
Dimensionsapp/labs/lab2/Dimensions.tsx
export default function Dimensions() {
  return (
    <div id="wd-css-dimensions">
      <h2>Dimension</h2>
      <div>
        <div className="wd-dimension-portrait wd-bg-color-yellow">Portrait</div>
        <div className="wd-dimension-landscape wd-bg-color-blue wd-fg-color-white">
          Landscape
        </div>
        <div className="wd-dimension-square wd-bg-color-red">Square</div>
      </div>
    </div>
  );
}

Three small boxes appear where three full-width divs would have stood by default — but block elements still stack vertically even at reduced widths, since narrowing an element does not change whether it starts a new line:

Dimensionsapp/labs/lab2/Dimensions.tsx

Dimension

Portrait
Landscape
Square

width and height apply to block boxes (and to inline-block, below). They do not apply to inline boxes — span, a, and text-level tags from §1.3.1. An inline element sizes to its content and sits in the line; assigning width: 150px is ignored. The CSS display property overrides the tag's default:

  • display: inline — stay in the line; width/height are ignored
  • display: block — start a new line; honor width/height and stretch to the parent by default
  • display: inline-block — sit in the line and honor width/height — the usual choice when you want a box that still flows like a word

Add display classes and a Display.tsx that applies all three to span tags (which default to inline) so the only difference is the display value. Each span also sets width: 150px and height: 50px — watch which ones obey:

Lab2 stylesapp/labs/lab2/index.css
.wd-display-inline {
  display: inline;
  width: 150px;
  height: 50px;
  padding: 5px;
}
.wd-display-inline-block {
  display: inline-block;
  width: 150px;
  height: 50px;
  padding: 5px;
}
.wd-display-block {
  display: block;
  width: 150px;
  height: 50px;
  padding: 5px;
}
Displayapp/labs/lab2/Display.tsx
export default function Display() {
  return (
    <div id="wd-css-display">
      <h2>Display</h2>
      <h3>Inline</h3>
      <div>
        <span className="wd-display-inline wd-bg-color-red">Inline 1</span>
        <span className="wd-display-inline wd-bg-color-yellow">Inline 2</span>
        <span className="wd-display-inline wd-bg-color-blue wd-fg-color-white">
          Inline 3
        </span>
      </div>
      <h3>Inline-block</h3>
      <div>
        <span className="wd-display-inline-block wd-bg-color-red">
          Inline-block 1
        </span>
        <span className="wd-display-inline-block wd-bg-color-yellow">
          Inline-block 2
        </span>
        <span className="wd-display-inline-block wd-bg-color-blue wd-fg-color-white">
          Inline-block 3
        </span>
      </div>
      <h3>Block</h3>
      <div>
        <span className="wd-display-block wd-bg-color-red">Block 1</span>
        <span className="wd-display-block wd-bg-color-yellow">Block 2</span>
        <span className="wd-display-block wd-bg-color-blue wd-fg-color-white">
          Block 3
        </span>
      </div>
    </div>
  );
}

The first row stays a single line of text-sized chips — 150px was ignored. The second row still shares a line, but each chip is a 150×50 box. The third row stacks, one per line:

Displayapp/labs/lab2/Display.tsx

Display

Inline

Inline 1Inline 2Inline 3

Inline-block

Inline-block 1Inline-block 2Inline-block 3

Block

Block 1Block 2Block 3

2.1.13 Styling an HTML Tag's Relative Position with CSS

Videos

The CSS position property overrides where an element would normally sit. Setting it to relative nudges the element away from its default spot — using top, bottom, left, and right to say how far — while leaving a "ghost" of its original space behind, so surrounding elements do not shift to fill the gap:

Lab2 stylesapp/labs/lab2/index.css
.wd-pos-relative-nudge-up-right {
  position: relative;
  bottom: 30px;
  left: 30px;
}
.wd-pos-relative-nudge-down-right {
  position: relative;
  top: 20px;
  left: 20px;
}
.wd-pos-relative {
  position: relative;
}
Positionsapp/labs/lab2/Positions.tsx
export default function Positions() {
  return (
    <div id="wd-css-positions">
      <h2>Positions</h2>
      <div id="wd-css-position-relative">
        <h2>Relative</h2>
        <div className="wd-bg-color-gray">
          <div className="wd-bg-color-yellow wd-dimension-portrait">
            <div className="wd-pos-relative-nudge-down-right">Portrait</div>
          </div>
          <div className="wd-pos-relative-nudge-up-right wd-bg-color-blue wd-fg-color-white wd-dimension-landscape">
            Landscape
          </div>
          <div className="wd-bg-color-red wd-dimension-square">Square</div>
        </div>
      </div>
    </div>
  );
}

The "Portrait" label drifts down and right inside its own yellow box, and the landscape box drifts up and right — both nudged away from where they would otherwise sit, without the gray container or the red square reflowing around them:

Positions (relative)app/labs/lab2/Positions.tsx

Positions

Relative

Portrait
Landscape
Square

2.1.14 Styling a Tag's Absolute Position with CSS

Videos

Setting position to absolute removes the element from the normal flow entirely and positions it relative to its nearest ancestor whose own position is relative, absolute, or fixed — falling back to the page itself if no such ancestor exists. Wrap three absolutely positioned boxes in a relative container so they anchor to it instead of the whole page:

Lab2 stylesapp/labs/lab2/index.css
.wd-pos-absolute-10-10 {
  position: absolute;
  top: 10px;
  left: 10px;
}
.wd-pos-absolute-50-50 {
  position: absolute;
  top: 50px;
  left: 50px;
}
.wd-pos-absolute-120-20 {
  position: absolute;
  top: 20px;
  left: 120px;
}

Paste the TSX below inside #wd-css-positions, after the previous section.

Positionsapp/labs/lab2/Positions.tsx
<div id="wd-css-position-absolute">
  <h2>Absolute position</h2>
  <div className="wd-pos-relative" style={{ height: 150 }}>
    <div className="wd-pos-absolute-10-10 wd-bg-color-yellow wd-dimension-portrait">
      Portrait
    </div>
    <div className="wd-pos-absolute-50-50 wd-bg-color-blue wd-fg-color-white wd-dimension-landscape">
      Landscape
    </div>
    <div className="wd-pos-absolute-120-20 wd-bg-color-red wd-dimension-square">
      Square
    </div>
  </div>
  <br /><br /><br /><br /><br /><br /><br />
</div>

The three boxes stack on top of one another, each offset from the top-left corner of the shared relative container by its own top/left pair — proof that "absolute" means relative to an ancestor, not to some fixed point on the screen. Absolute boxes take up no space, so the tall portrait box hangs below the 150px container; the row of <br /> tags after it leaves room so the next section doesn't slide underneath:

Positions (absolute)app/labs/lab2/Positions.tsx

Absolute position

Portrait
Landscape
Square







2.1.15 Styling an HTML Tag's Fixed Position with CSS

Videos

Setting position to fixed anchors an element to the browser's viewport instead of any ancestor, so it stays put even while the rest of the page scrolls. Add one fixed box pinned to the right edge of the window, halfway down (right: 0, bottom: 50%):

Lab2 stylesapp/labs/lab2/index.css
.wd-pos-fixed {
  position: fixed;
  right: 0px;
  bottom: 50%;
}

Paste the TSX below inside #wd-css-positions, after the previous section.

Positionsapp/labs/lab2/Positions.tsx
<div id="wd-css-position-fixed">
  <h2>Fixed position</h2>
  Checkout the blue square that says &quot;Fixed position&quot; stuck all the way
  on the right and half way down the page. It doesn&apos;t scroll with the
  rest of the page. Its position is &quot;Fixed&quot;.
  <div className="wd-pos-fixed wd-dimension-square wd-bg-color-blue wd-fg-color-white">
    Fixed position
  </div>
</div>

Finished Positions.tsxputs the relative, absolute, and fixed sections in one component. That file is what the figure renders — scroll it and the blue "Fixed position" square stays glued to the right edge instead of scrolling away with the rest of the content:

Positionsapp/labs/lab2/Positions.tsx
export default function Positions() {
  return (
    <div id="wd-css-positions">
      <h2>Positions</h2>
      <div id="wd-css-position-relative">
        <h2>Relative</h2>
        <div className="wd-bg-color-gray">
          <div className="wd-bg-color-yellow wd-dimension-portrait">
            <div className="wd-pos-relative-nudge-down-right">Portrait</div>
          </div>
          <div className="wd-pos-relative-nudge-up-right wd-bg-color-blue wd-fg-color-white wd-dimension-landscape">
            Landscape
          </div>
          <div className="wd-bg-color-red wd-dimension-square">Square</div>
        </div>
      </div>

      <div id="wd-css-position-absolute">
        <h2>Absolute position</h2>
        <div className="wd-pos-relative" style={{ height: 150 }}>
          <div className="wd-pos-absolute-10-10 wd-bg-color-yellow wd-dimension-portrait">
            Portrait
          </div>
          <div className="wd-pos-absolute-50-50 wd-bg-color-blue wd-fg-color-white wd-dimension-landscape">
            Landscape
          </div>
          <div className="wd-pos-absolute-120-20 wd-bg-color-red wd-dimension-square">
            Square
          </div>
        </div>
        <br /><br /><br /><br /><br /><br /><br />
      </div>

      <div id="wd-css-position-fixed">
        <h2>Fixed position</h2>
        Checkout the blue square that says &quot;Fixed position&quot; stuck all the way
        on the right and half way down the page. It doesn&apos;t scroll with the
        rest of the page. Its position is &quot;Fixed&quot;.
        <div className="wd-pos-fixed wd-dimension-square wd-bg-color-blue wd-fg-color-white">
          Fixed position
        </div>
      </div>
    </div>
  );
}
Positionsapp/labs/lab2/Positions.tsx

Positions

Relative

Portrait
Landscape
Square

Absolute position

Portrait
Landscape
Square







Fixed position

Checkout the blue square that says "Fixed position" stuck all the way on the right and half way down the page. It doesn't scroll with the rest of the page. Its position is "Fixed".
Fixed position

This figure box wraps the demo so the fixed square stays inside it for the screenshot — in the real Lab 2 page it anchors to the browser window itself, exactly as position: fixed intends.

2.1.16 Styling an HTML Tag's Z Index with CSS

Videos

Once elements are positioned with relative, absolute, or fixed, they can end up overlapping. By default, later elements in the HTML render on top of earlier ones. The z-index property overrides that default stacking order directly — among positioned siblings like these boxes, which share one container, a higher z-index renders above a lower one regardless of source order. (A positioned parent with its own z-index starts a separate stack, and its children can't rise above elements outside it. That rarely comes up in these labs.)

Lab2 stylesapp/labs/lab2/index.css
.wd-zindex-bring-to-front {
  z-index: 10;
}
Zindexapp/labs/lab2/Zindex.tsx
export default function Zindex() {
  return (
    <div id="wd-z-index">
      <h2>Z index</h2>
      <div className="wd-pos-relative" style={{ height: 150 }}>
        <div className="wd-pos-absolute-10-10 wd-bg-color-yellow wd-dimension-portrait">
          Portrait
        </div>
        <div className="wd-zindex-bring-to-front wd-pos-absolute-50-50 wd-dimension-landscape wd-bg-color-blue wd-fg-color-white">
          Landscape
        </div>
        <div className="wd-pos-absolute-120-20 wd-bg-color-red wd-dimension-square">
          Square
        </div>
      </div>
      <br /><br /><br /><br /><br /><br /><br />
    </div>
  );
}

The blue landscape box is declared before the red square in the markup, yet it renders above it, because wd-zindex-bring-to-front gives it a higher stacking order than the default:

Zindexapp/labs/lab2/Zindex.tsx

Z index

Portrait
Landscape
Square







2.1.17 Floating Images and Content with CSS

SlidesVideos

The float property pulls an element to the left or right edge of its container and lets adjacent inline content wrap around it — the classic technique for flowing paragraphs of text around an image. Because floated elements leave the normal flow, a following element with clear: both is needed to stop the wrapping and resume normal stacking. A length in rem is relative to the root font size, usually 16px, so 2rem = 32px:

Lab2 stylesapp/labs/lab2/index.css
.wd-float-left {
  float: left;
  height: 100px;
}
.wd-float-right {
  float: right;
  height: 100px;
}
img.wd-float-left,
img.wd-float-right {
  width: auto;
  max-width: 35%;
}
img.wd-float-left {
  margin: 0 1rem 0.5rem 0;
}
img.wd-float-right {
  margin: 0 0 0.5rem 1rem;
}
.wd-float-done {
  clear: both;
}
Floatapp/labs/lab2/Float.tsx
const STARSHIP =
  "https://www.staradvertiser.com/wp-content/uploads/2021/08/web1_Starship-gap2.jpg";
const LOREM =
  "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eius hic reprehenderit doloremque adipisci iste deserunt. Inventore, hic. Esse nihil unde aut, dignissimos eos consequatur veniam distinctio?";

export default function Float() {
  return (
    <div id="wd-float-divs">
      <h2>Float</h2>
      <div>
        <img className="wd-float-right" src={STARSHIP} alt="Starship" />
        {LOREM} {LOREM}
        <img className="wd-float-left" src={STARSHIP} alt="Starship" />
        {LOREM} {LOREM}
        <img className="wd-float-right" src={STARSHIP} alt="Starship" />
        {LOREM} {LOREM}
        <img className="wd-float-left" src={STARSHIP} alt="Starship" />
        {LOREM} {LOREM}
        <div className="wd-float-done" />
      </div>
      <div>
        <div className="wd-float-left wd-dimension-portrait wd-bg-color-yellow">
          Yellow
        </div>
        <div className="wd-float-left wd-dimension-portrait wd-bg-color-blue wd-fg-color-white">
          Blue
        </div>
        <div className="wd-float-left wd-dimension-portrait wd-bg-color-red">
          Red
        </div>
        <img className="wd-float-right" src={STARSHIP} alt="Starship" />
        <div className="wd-float-done" />
      </div>
    </div>
  );
}

Swap in some lorem ipsum placeholder text for LOREM and any image URL for STARSHIP. Keep the images small with height: 100px (and a modest max-width) so the paragraph text has room to wrap beside them instead of stacking under a full-width image. The same wd-float-left and wd-float-right classes also arrange plain colored boxes side by side, which is how the next section builds a grid out of nothing but float:

Floatapp/labs/lab2/Float.tsx

Float

StarshipLorem ipsum, dolor sit amet consectetur adipisicing elit. Eius hic reprehenderit doloremque adipisci iste deserunt. Inventore, hic. Esse nihil unde aut, dignissimos eos consequatur veniam distinctio? Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eius hic reprehenderit doloremque adipisci iste deserunt. Inventore, hic. Esse nihil unde aut, dignissimos eos consequatur veniam distinctio?StarshipLorem ipsum, dolor sit amet consectetur adipisicing elit. Eius hic reprehenderit doloremque adipisci iste deserunt. Inventore, hic. Esse nihil unde aut, dignissimos eos consequatur veniam distinctio? Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eius hic reprehenderit doloremque adipisci iste deserunt. Inventore, hic. Esse nihil unde aut, dignissimos eos consequatur veniam distinctio?StarshipLorem ipsum, dolor sit amet consectetur adipisicing elit. Eius hic reprehenderit doloremque adipisci iste deserunt. Inventore, hic. Esse nihil unde aut, dignissimos eos consequatur veniam distinctio? Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eius hic reprehenderit doloremque adipisci iste deserunt. Inventore, hic. Esse nihil unde aut, dignissimos eos consequatur veniam distinctio?StarshipLorem ipsum, dolor sit amet consectetur adipisicing elit. Eius hic reprehenderit doloremque adipisci iste deserunt. Inventore, hic. Esse nihil unde aut, dignissimos eos consequatur veniam distinctio? Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eius hic reprehenderit doloremque adipisci iste deserunt. Inventore, hic. Esse nihil unde aut, dignissimos eos consequatur veniam distinctio?
Yellow
Blue
Red
Starship

2.1.18 Laying Out Content in a Grid Using CSS

Videos

Combine float: left with percentage widths and you can arrange any number of columns side by side — a half-and-half split, a sidebar-and-main-content layout, or anything in between. Each row needs a wrapper with clear: both so the next row starts fresh below the previous one instead of floating up beside it:

Lab2 stylesapp/labs/lab2/index.css
.wd-grid-row {
  clear: both;
}
.wd-grid-col-half-page { width: 50%; float: left; }
.wd-grid-col-third-page { width: 33%; float: left; }
.wd-grid-col-two-thirds-page { width: 67%; float: left; }
.wd-grid-col-left-sidebar { width: 20%; float: left; }
.wd-grid-col-main-content { width: 60%; float: left; }
.wd-grid-col-right-sidebar { width: 20%; float: left; }
GridLayoutapp/labs/lab2/GridLayout.tsx
export default function GridLayout() {
  return (
    <div id="wd-css-grid-layout">
      <h2>Grid layout</h2>
      <div className="wd-grid-row">
        <div className="wd-grid-col-half-page wd-bg-color-yellow">
          <h3>Left half</h3>
        </div>
        <div className="wd-grid-col-half-page wd-bg-color-blue wd-fg-color-white">
          <h3>Right half</h3>
        </div>
      </div>
      <div className="wd-grid-row">
        <div className="wd-grid-col-left-sidebar wd-bg-color-yellow">
          <h3>Side bar</h3>
        </div>
        <div className="wd-grid-col-main-content wd-bg-color-blue wd-fg-color-white">
          <h3>Main content</h3>
        </div>
        <div className="wd-grid-col-right-sidebar wd-bg-color-green wd-fg-color-white">
          <h3>Side bar</h3>
        </div>
      </div>
    </div>
  );
}

The first row splits evenly in two; the second row arranges a narrow sidebar, wide main content, and a second sidebar across the same width — the whole layout built from nothing more than float and percentage widths, no dedicated layout property required yet:

GridLayoutapp/labs/lab2/GridLayout.tsx

Grid layout

Left half

Right half

Side bar

Main content

Side bar

2.1.19 Laying Out Content with CSS Flex

SlidesVideos

Flexbox (display: flex) is a purpose-built alternative to float-based layout. Declaring a container's display as flex and its flex-direction as row immediately lines up its children horizontally — no floats, no clearing, no percentage math:

Lab2 stylesapp/labs/lab2/index.css
.wd-flex-row-container {
  display: flex;
  flex-direction: row;
}
.wd-flex-row-container > div {
  height: 100px;
  padding: 10px;
  white-space: nowrap;
}
Flexapp/labs/lab2/Flex.tsx
export default function Flex() {
  return (
    <div id="wd-css-flex">
      <h2>Flex</h2>
      <div className="wd-flex-row-container">
        <div className="wd-bg-color-yellow">Column 1</div>
        <div className="wd-bg-color-blue wd-fg-color-white">Column 2</div>
        <div className="wd-bg-color-red wd-fg-color-white">Column 3</div>
      </div>
    </div>
  );
}

Three divs that would normally stack vertically instead sit in a single row. A shared rule gives each child a 100px height and a bit of padding; blue and red use wd-fg-color-white so their labels stay readable:

Flex (row)app/labs/lab2/Flex.tsx

Flex

Column 1
Column 2
Column 3

Flex children can also grow to absorb leftover space. Add flex-grow: 1 to the last column so it stretches to fill whatever room the other two do not use:

Lab2 stylesapp/labs/lab2/index.css
.wd-flex-grow-1 {
  flex-grow: 1;
}

Apply wd-flex-grow-1 to Column 3 in Flex.tsx:

Flexapp/labs/lab2/Flex.tsx
export default function Flex() {
  return (
    <div id="wd-css-flex">
      <h2>Flex</h2>
      <div className="wd-flex-row-container">
        <div className="wd-bg-color-yellow">Column 1</div>
        <div className="wd-bg-color-blue wd-fg-color-white">Column 2</div>
        <div className="wd-bg-color-red wd-fg-color-white wd-flex-grow-1">
          Column 3
        </div>
      </div>
    </div>
  );
}

The third column now expands to consume all the remaining width in the row, while the first two stay exactly as wide as their text:

Flex (grow)app/labs/lab2/Flex.tsx

Flex

Column 1
Column 2
Column 3

Finally, pin the first column to a fixed width so it neither shrinks nor grows, letting the third column absorb whatever space is left after that fixed column and the second column's natural width are accounted for:

Lab2 stylesapp/labs/lab2/index.css
.wd-width-110px {
  /* Room for "Column 1" + 10px padding under border-box */
  width: 110px;
  flex-shrink: 0;
}

Apply wd-width-110px to Column 1 and keep wd-flex-grow-1 on Column 3:

Flexapp/labs/lab2/Flex.tsx
export default function Flex() {
  return (
    <div id="wd-css-flex">
      <h2>Flex</h2>
      <div className="wd-flex-row-container">
        <div className="wd-bg-color-yellow wd-width-110px">Column 1</div>
        <div className="wd-bg-color-blue wd-fg-color-white">Column 2</div>
        <div className="wd-bg-color-red wd-fg-color-white wd-flex-grow-1">
          Column 3
        </div>
      </div>
    </div>
  );
}
Flexapp/labs/lab2/Flex.tsx

Flex

Column 1
Column 2
Column 3

2.1.20 Media Queries

SlidesVideos

Media queries apply a block of CSS rules only when the browser matches a condition — most commonly a viewport width range — which is the foundation of responsive design: the same markup rendering differently on a phone, a tablet, and a desktop. Give this demo its own CSS file, since the rules only make sense together as a set:

MediaQueriesDemo stylesapp/labs/lab2/MediaQueriesDemo.css
.wd-media-queries-demo {
  background-color: green;
  color: white;
  padding: 1rem;
}

.wd-media-queries-demo li {
  opacity: 0.55;
  font-weight: normal;
  text-decoration: none;
}

.wd-media-queries-demo li.wd-mq-rule-default {
  opacity: 1;
  font-weight: bold;
  text-decoration: underline;
}

@media (min-width: 750px) and (max-width: 1000px) {
  .wd-media-queries-demo {
    background-color: yellow;
    color: black;
  }
  /* Same specificity as the default highlight — must clear it here */
  .wd-media-queries-demo li.wd-mq-rule-default {
    opacity: 0.55;
    font-weight: normal;
    text-decoration: none;
  }
  .wd-media-queries-demo li.wd-mq-rule-750 {
    opacity: 1;
    font-weight: bold;
    text-decoration: underline;
  }
}

@media (min-width: 1000px) and (max-width: 1250px) {
  .wd-media-queries-demo {
    background-color: blue;
    color: white;
  }
  .wd-media-queries-demo li.wd-mq-rule-default {
    opacity: 0.55;
    font-weight: normal;
    text-decoration: none;
  }
  .wd-media-queries-demo li.wd-mq-rule-1000 {
    opacity: 1;
    font-weight: bold;
    text-decoration: underline;
  }
}

@media (min-width: 1250px) {
  .wd-media-queries-demo {
    background-color: red;
    color: white;
  }
  .wd-media-queries-demo li.wd-mq-rule-default {
    opacity: 0.55;
    font-weight: normal;
    text-decoration: none;
  }
  .wd-media-queries-demo li.wd-mq-rule-1250 {
    opacity: 1;
    font-weight: bold;
    text-decoration: underline;
  }
}
MediaQueriesDemoapp/labs/lab2/MediaQueriesDemo.tsx
import "./MediaQueriesDemo.css";

export default function MediaQueriesDemo() {
  return (
    <div className="wd-media-queries-demo">
      <h2>Media Query Demo</h2>
      <p>
        This demo uses CSS media queries to change colors based on screen width:
      </p>
      <ul>
        <li className="wd-mq-rule-default">
          Default is White text on Green background
        </li>
        <li className="wd-mq-rule-750">
          750px to 1000px: Black text on Yellow background
        </li>
        <li className="wd-mq-rule-1000">
          1000px to 1250px: White text on Blue background
        </li>
        <li className="wd-mq-rule-1250">
          Above 1250px: White text on Red background
        </li>
      </ul>
    </div>
  );
}

Notice that only the last @media block has no max-width, so it matches every width from 1250px upward. The ranges share their edges: at exactly 1000px or 1250px two blocks match, and the later block in the file wins (source order, §2.1.6). A breakpoint is a viewport width at which styles change, for example through a media query. The matching bullet is bold and underlined so you can see which rule is active. The phone frame is 375px, so the default green rule is showing. The desktop frame is 1024px, inside the blue 1000–1250 range, and it is drawn smaller so it fits this column. Each frame is its own page, so the queries follow that frame:

MediaQueriesDemoapp/labs/lab2/MediaQueriesDemo.tsx

Phone, 375px: green

Desktop, 1024px: blue

2.1.21 Check Your Understanding

Before React Icons and Tailwind, pause and test the CSS topics from this section. The practice quiz draws 10 items — concepts (why the style attribute is a bad habit, padding vs margin, the box model, block vs inline), syntax (hyphens vs camelCase, # vs ., box-sizing, display), button types (§1.3.6.7), acronyms, snippets, fill-in-the-blank, and short puzzles. It is a self-check, not part of your course grade. Misses link back to the subsection you should reread; each new attempt draws a different 10.

2.2 Decorating Documents with React Icons

Slides

React Icons bundles thousands of icons from several popular icon families — Font Awesome, Heroicons, and more — and exposes each one as a plain React component. Install it from the project root:

npm install react-icons

Browse react-icons.github.io/react-icons and search by keyword or icon family — each result page shows the import path and component name to copy.

CSS cascade layers are named groups of styles. When two rules conflict, the one in a later layer wins. Writing layer(...) on an import puts that CSS in a named cascade layer, so the order of those styles stays predictable. A design token is a named value a design reuses, such as a color or a spacing step. A CSS variable is a custom property the browser stores, written --name and read with var(--name). Tailwind's theme layer holds those design tokens — colors, spacing, and fonts — as CSS variables. Utilities are the classes you put on an element, such as p-4 and text-lg. Preflightis Tailwind's base reset. It strips the browser's default heading sizes, margins, list bullets, and button styles.

The sampler below uses utilities such as flex and text-3xl. Those classes are not browser defaults. The full library, including Preflight, is scoped to the Tailwind lab in §2.3. For this sampler, add a smaller file that loads only the theme and utilities — no Preflight — and import it from the component:

Tailwind utilitiesapp/labs/lab2/tailwind/utilities.css
/* Theme + utilities without Preflight, for the Labs */
@import "tailwindcss/theme" layer(theme);
@import "tailwindcss/utilities" layer(utilities);

One side effect you may notice in DevTools: once this file loads, the min-h-full flex flex-col classes that create-next-app put on <body> in app/layout.tsx start working, so the Lab 2 body becomes a flex column. It doesn't change how Lab 2 looks, so leave it.

Try a handful of icons from different families in one component:

ReactIconsSamplerapp/labs/lab2/ReactIconsSampler.tsx
import "@/app/labs/lab2/tailwind/utilities.css";
import { FaCalendar, FaEnvelopeOpenText, FaRegClock } from "react-icons/fa";
import { AiOutlineDashboard } from "react-icons/ai";
import { FaBookBible } from "react-icons/fa6";
import { VscAccount } from "react-icons/vsc";

export default function ReactIconsSampler() {
  return (
    <div id="wd-react-icons-sampler" className="mb-4 font-sans">
      <h2 className="text-lg font-semibold">React Icons Sampler</h2>
      <div className="flex gap-3 text-3xl">
        <VscAccount />
        <AiOutlineDashboard />
        <FaBookBible />
        <FaCalendar />
        <FaEnvelopeOpenText />
        <FaRegClock />
      </div>
    </div>
  );
}

Six icons render inline, each imported from a different icon family through a different package path — fa, ai, fa6, and vsc each group icons by their source library. The parent uses text-3xl so each icon scales up. React Icons are sized in em, a length relative to the element's own font size (1em is the current font size, so an icon inherits whatever size its parent sets). That's why a bigger font on the parent gives bigger icons; that utility comes from the file above, and §2.3 covers the rest of the library. Icon components also accept ordinary className, style, and size props like any other element:

ReactIconsSamplerapp/labs/lab2/ReactIconsSampler.tsx

React Icons Sampler

Import ReactIconsSampler into Lab2 to keep it on the growing exercise page, then keep an eye out for icons that fit Kambaz screens later in §2.4 — a dashboard icon for the Dashboard link, a calendar icon for Calendar, and so on.

2.3 Styling Webpages with the Tailwind CSS Library

Slides

Tailwind CSS is a utility-first framework: instead of writing custom CSS rules, you compose a look directly in className out of small, single-purpose utility classes like p-4 (padding) or bg-red-500 (background color). create-next-app already installed and configured Tailwind when you scaffolded webdev-client in Chapter 1 — you simply commented out its import in app/layout.tsx so the HTML exercises could render with plain browser defaults.

Rather than re-enabling Tailwind globally, scope it to a new tailwind sub-lab so only pages that opt in load it. Create a small CSS file that imports the library:

Tailwind entryapp/labs/lab2/tailwind/index.css
@import "tailwindcss";

Then create the page that imports that CSS file — and, because it is its own route under app/labs/lab2/tailwind/, its own separate URL from the rest of Lab 2:

TailwindLabapp/labs/lab2/tailwind/page.tsx
import "./index.css";

export default function TailwindLab() {
  return (
    <div className="p-8">
      <h1 className="text-4xl font-bold mb-8">Tailwind CSS</h1>
    </div>
  );
}

Notice the text-4xl font-bold on the h1. Full Tailwind includes Preflight (§2.2), which resets every heading to the size and weight of body text, so on this page a heading only looks like a heading when you give it classes. That's why every heading in the listings below carries its own size and weight classes, such as text-2xl font-bold. Leave one off and that heading renders as plain 16px text, unlike the figures.

Link to /labs/lab2/tailwind from the main Lab 2 page so both are reachable from the Labs table of contents. Use a plain <a>, not next/link. The Tailwind lab loads full Tailwind with Preflight, and a full page load keeps it from leaking into other pages on Back. Then work through the utility categories below one component at a time.

Lab2 Tailwind linkapp/labs/lab2/page.tsx
<p>
  <a href="/labs/lab2/tailwind">Open Tailwind CSS lab →</a>
</p>

2.3.1 Spacing Slides

Tailwind's spacing utilities follow a compact naming convention: classes starting with m set margin, classes starting with p set padding, and a number suffix (-4, -8, …) sets the amount on a consistent scale. Direction letters narrow which side: nothing for all sides, s/e for the logical start/end (left/right in English), t/b for top/bottom:

TailwindSpacingapp/labs/lab2/tailwind/TailwindSpacing.tsx
export default function TailwindSpacing() {
  return (
    <div>
      <h2 className="text-3xl">Margin</h2>
      <div className="bg-blue-200 mb-4 p-4">
        This div has a bottom margin of 4.
      </div>
      <div className="bg-blue-200 ms-4 me-8 p-4">
        This div has a start margin of 4 and an end margin of 8.
      </div>
      <h2 className="text-3xl mt-8">Padding</h2>
      <div className="bg-green-200 ps-2 pt-4 pb-8 mb-4">
        This div has starting padding of 2, top padding of 4, and bottom padding of 8.
      </div>
      <div className="bg-green-200 p-6">This div has padding all around of 6.</div>
    </div>
  );
}

Each box's spacing changes with nothing but its class list — no separate stylesheet, no selector to name:

TailwindSpacingapp/labs/lab2/tailwind/TailwindSpacing.tsx

Margin

This div has a bottom margin of 4.
This div has a start margin of 4 and an end margin of 8.

Padding

This div has starting padding of 2, top padding of 4, and bottom padding of 8.
This div has padding all around of 6.

Add TailwindSpacing to app/labs/lab2/tailwind/page.tsx. Put this import with the other imports, and render the component under the heading:

TailwindLabapp/labs/lab2/tailwind/page.tsx
import TailwindSpacing from "./TailwindSpacing";

      <h1 className="text-4xl font-bold mb-8">Tailwind CSS</h1>
      <TailwindSpacing />

2.3.2 Typography Slides

Typography utilities cover font size (text-sm through text-3xl) and weight (font-thin through font-black) with the same predictable naming:

TailwindTypographyapp/labs/lab2/tailwind/TailwindTypography.tsx
export default function TailwindTypography() {
  return (
    <div>
      <h2 className="text-3xl">Font Size</h2>
      <p className="text-sm">This is small text.</p>
      <p className="text-base">This is base text.</p>
      <p className="text-lg">This is large text.</p>
      <p className="text-xl">This is extra large text.</p>
      <p className="text-2xl">This is 2x extra large text.</p>
      <p className="text-3xl">This is 3x extra large text.</p>
      <h2 className="text-3xl font-bold mt-4">Font Weight</h2>
      <p className="font-thin">This is thin font weight.</p>
      <p className="font-light">This is light font weight.</p>
      <p className="font-normal">This is normal font weight.</p>
      <p className="font-medium">This is medium font weight.</p>
      <p className="font-semibold">This is semi-bold font weight.</p>
      <p className="font-bold">This is bold font weight.</p>
      <p className="font-extrabold">This is extra-bold font weight.</p>
      <p className="font-black">This is black font weight.</p>
    </div>
  );
}

Font sizes step up visibly from text-sm to text-3xl, and weights step up from a barely-there font-thin to a heavy font-black — build the full component with every step listed in the code above to see the whole scale:

TailwindTypographyapp/labs/lab2/tailwind/TailwindTypography.tsx

Font Size

This is small text.

This is base text.

This is large text.

This is extra large text.

This is 2x extra large text.

This is 3x extra large text.

Font Weight

This is thin font weight.

This is light font weight.

This is normal font weight.

This is medium font weight.

This is semi-bold font weight.

This is bold font weight.

This is extra-bold font weight.

This is black font weight.

Add TailwindTypography to app/labs/lab2/tailwind/page.tsx. Put this import after TailwindSpacing, and render the component after that one:

TailwindLabapp/labs/lab2/tailwind/page.tsx
import TailwindTypography from "./TailwindTypography";

      <TailwindSpacing />
      <hr className="my-8" />
      <TailwindTypography />

2.3.3 Background Colors Slides

Background color utilities follow the pattern bg-{color}-{shade}, where the shade is a number from 50 (lightest) to 950 (darkest) in steps of 100. Pair a background with a contrasting text color so the content stays readable:

TailwindBackgroundColorsapp/labs/lab2/tailwind/TailwindBackgroundColors.tsx
export default function TailwindBackgroundColors() {
  return (
    <div>
      <h2 className="text-3xl font-bold mb-4">Background Colors</h2>
      <div className="bg-red-500 text-white p-4 mb-4">This div has a red background.</div>
      <div className="bg-green-500 text-white p-4 mb-4">This div has a green background.</div>
      <div className="bg-blue-500 text-white p-4 mb-4">This div has a blue background.</div>
      <div className="bg-yellow-500 text-black p-4 mb-4">This div has a yellow background.</div>
    </div>
  );
}

Four bands of color render top to bottom — the same -500 shade across red, green, and blue, but yellow-500 is light enough that it needs black text instead of white to stay legible:

TailwindBackgroundColorsapp/labs/lab2/tailwind/TailwindBackgroundColors.tsx

Background Colors

This div has a red background.
This div has a green background.
This div has a blue background.
This div has a yellow background.

Add TailwindBackgroundColors to app/labs/lab2/tailwind/page.tsx. Put this import after TailwindTypography, and render the component after that one:

TailwindLabapp/labs/lab2/tailwind/page.tsx
import TailwindBackgroundColors from "./TailwindBackgroundColors";

      <TailwindTypography />
      <hr className="my-8" />
      <TailwindBackgroundColors />

2.3.4 Responsive Design Slides

A responsive page changes as the viewport gets wider. A breakpoint (§2.1.20) is a viewport width at which styles change. Tailwind names the common widths and lets you prefix a class with one of them. The five components below each teach one prefix. The card at the end combines them. Each figure is its own page at a fixed width, so a prefix follows that frame, not the width of this column.

2.3.4.1 One breakpoint

Tailwind is mobile-first. An unprefixed class applies at every width. A prefixed class applies at that breakpoint and up. As in §2.1.17, widths are in rem (1rem is usually 16px, so 48rem is 768px). md: starts at 48rem (768px). This box is bg-red-500 at every width, and md:bg-green-500 replaces the red from 768px up:

TailwindResponsiveBreakpointapp/labs/lab2/tailwind/TailwindResponsiveBreakpoint.tsx
export default function TailwindResponsiveBreakpoint() {
  return (
    <div
      id="wd-tailwind-responsive-breakpoint"
      className="bg-red-500 md:bg-green-500 p-4 text-white"
    >
      Red below md, green at md and up.
    </div>
  );
}

The phone frame is 375px, below md, so the box stays red. The desktop frame is 1024px, drawn smaller so it fits this column, and the box is green:

TailwindResponsiveBreakpointapp/labs/lab2/tailwind/TailwindResponsiveBreakpoint.tsx

Phone, 375px: red

Desktop, 1024px: green

2.3.4.2 Show and hide by width

hidden removes an element (display: none) and block puts it back on the page. Prefix either one to tie visibility to a breakpoint. block md:hidden shows the "small screen" line until md, then hides it. hidden md:block keeps the "large screen" line hidden until md, then shows it. Only one line is visible at a time:

TailwindResponsiveShowHideapp/labs/lab2/tailwind/TailwindResponsiveShowHide.tsx
export default function TailwindResponsiveShowHide() {
  return (
    <div id="wd-tailwind-responsive-show-hide">
      <p className="block md:hidden bg-red-200 p-2">small screen</p>
      <p className="hidden md:block bg-green-200 p-2">large screen</p>
    </div>
  );
}

The phone frame shows the red line. The desktop frame shows the green line:

TailwindResponsiveShowHideapp/labs/lab2/tailwind/TailwindResponsiveShowHide.tsx

Phone, 375px: small screen

Desktop, 1024px: large screen

2.3.4.3 Stack, then side by side

CSS flex lines boxes up along one direction. flex-direction: column stacks them, and flex-direction: row places them side by side. Tailwind writes those as flex-col and flex-row. Start with a column, then switch direction at md. flex flex-col stacks the three boxes at every width. md:flex-row lays them side by side from 768px up. gap-4 is the space between the boxes. It is unprefixed, so that space stays in both layouts:

TailwindResponsiveFlexapp/labs/lab2/tailwind/TailwindResponsiveFlex.tsx
export default function TailwindResponsiveFlex() {
  return (
    <div
      id="wd-tailwind-responsive-flex"
      className="flex flex-col md:flex-row gap-4"
    >
      <div className="bg-red-500 p-4 text-white">One</div>
      <div className="bg-green-500 p-4 text-white">Two</div>
      <div className="bg-blue-500 p-4 text-white">Three</div>
    </div>
  );
}

The phone frame stacks One, Two, and Three. The desktop frame puts them in a row:

TailwindResponsiveFlexapp/labs/lab2/tailwind/TailwindResponsiveFlex.tsx

Phone, 375px: stacked

Desktop, 1024px: side by side

2.3.4.4 Grid columns by breakpoint

CSS Grid places children into columns, with a gap between the cells. grid turns the grid on, grid-cols-* sets how many columns, and gap-4 sets that gap. §2.3.6 goes further with grid utilities. Column count uses the same breakpoint prefixes, including ones narrower and wider than md. sm: and lg: are minimum widths: once a prefix applies, it keeps applying at larger widths until a later prefix overrides it.

PrefixMin width
sm:40rem (640px)
md:48rem (768px)
lg:64rem (1024px)
xl:80rem (1280px)
2xl:96rem (1536px)

Eight tiles use grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4. Below 640px there is one column. From 640px up to just under 1024px there are two, because sm: applies and lg: does not yet. From 1024px up there are four:

TailwindResponsiveGridapp/labs/lab2/tailwind/TailwindResponsiveGrid.tsx
export default function TailwindResponsiveGrid() {
  return (
    <div
      id="wd-tailwind-responsive-grid"
      className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"
    >
      <div className="text-center bg-blue-300 p-3">01</div>
      <div className="text-center bg-blue-300 p-3">02</div>
      <div className="text-center bg-blue-300 p-3">03</div>
      <div className="text-center bg-blue-300 p-3">04</div>
      <div className="text-center bg-blue-300 p-3">05</div>
      <div className="text-center bg-blue-300 p-3">06</div>
      <div className="text-center bg-blue-300 p-3">07</div>
      <div className="text-center bg-blue-300 p-3">08</div>
    </div>
  );
}

The phone frame is below sm, so one column. The middle frame is 700px, which is past sm (640px) and short of lg (1024px), so two columns. The desktop frame is 1024px, which is lg, so four columns:

TailwindResponsiveGridapp/labs/lab2/tailwind/TailwindResponsiveGrid.tsx

Phone, 375px: 1 column

700px: 2 columns

Desktop, 1024px: 4 columns

2.3.4.5 Spacing and text size

Spacing and font size take the same prefixes. p-2 and text-base apply at every width. md:p-8 and md:text-2xl increase the padding and the type from 768px up. Both the heading and the paragraph use that pair:

TailwindResponsiveSpacingTextapp/labs/lab2/tailwind/TailwindResponsiveSpacingText.tsx
export default function TailwindResponsiveSpacingText() {
  return (
    <div id="wd-tailwind-responsive-spacing-text">
      <h2 className="bg-yellow-200 p-2 text-base md:p-8 md:text-2xl">
        Spacing and text size
      </h2>
      <p className="bg-yellow-100 p-2 text-base md:p-8 md:text-2xl">
        Padding and the font size grow at md.
      </p>
    </div>
  );
}

These frames stay at their real size, so text-2xl on the desktop frame is actually larger than text-base on the phone. Scroll the desktop frame sideways if it runs past the column:

TailwindResponsiveSpacingTextapp/labs/lab2/tailwind/TailwindResponsiveSpacingText.tsx

Phone, 375px: compact

Desktop, 1024px: larger type

2.3.4.6 A responsive card

Save an image of the React logo to public/images/reactjs.jpg (already available from Chapter 1's Kambaz Dashboard exercise). This card combines the ideas above: an unprefixed layout that changes at md, including a stack that becomes a row. It also uses breakpoint utilities the small demos did not. md:max-w-2xl widens the card. md:flex turns the row on without a flex-col first. On the image, md:w-48 sets a fixed width of 12rem, md:shrink-0 stops flex from shrinking that width, md:h-full fills the card's height, and md:min-h-56 keeps a minimum height of 14rem. object-cover crops the picture so it fills that box without stretching:

TailwindResponsiveDesignapp/labs/lab2/tailwind/TailwindResponsiveDesign.tsx
export default function TailwindResponsiveDesign() {
  return (
    <div className="font-sans">
      <h2 className="text-3xl font-bold mb-4">Responsive Design</h2>
      <div className="mx-auto w-full max-w-md overflow-hidden rounded-xl bg-white shadow-md md:max-w-2xl">
        <div className="md:flex">
          <div className="relative md:w-48 md:shrink-0">
            <img
              className="h-56 w-full object-cover md:h-full md:min-h-56 md:w-48"
              src="/images/reactjs.jpg"
              alt="React JS"
            />
            <div className="absolute inset-0 flex flex-col items-center justify-center text-white">
              <svg
                viewBox="0 0 24 24"
                className="h-24 w-24"
                aria-hidden="true"
              >
                <circle cx="12" cy="12" r="2.05" fill="currentColor" />
                <g fill="none" stroke="currentColor" strokeWidth="1">
                  <ellipse cx="12" cy="12" rx="10" ry="4.2" />
                  <ellipse
                    cx="12"
                    cy="12"
                    rx="10"
                    ry="4.2"
                    transform="rotate(60 12 12)"
                  />
                  <ellipse
                    cx="12"
                    cy="12"
                    rx="10"
                    ry="4.2"
                    transform="rotate(120 12 12)"
                  />
                </g>
              </svg>
              <div className="mt-2 text-2xl font-semibold">React JS</div>
            </div>
          </div>
          <div className="min-w-0 p-8">
            <div className="text-sm font-semibold tracking-wide text-indigo-500 uppercase">
              Professional Courses
            </div>
            <a
              href="#"
              className="mt-1 block text-lg leading-tight font-medium text-black no-underline hover:underline"
            >
              Rocket Propulsion Fundamentals
            </a>
            <p className="mt-2 text-gray-500">
              An in-depth study of the fundamentals of rocket propulsion...
            </p>
          </div>
        </div>
      </div>
    </div>
  );
}

The two frames below are separate pages, so md: follows each frame. The phone frame is 375px and stacks the image on top. The desktop frame is 1024px, drawn smaller so it fits this column, and md:flex puts the image beside the text:

TailwindResponsiveDesignapp/labs/lab2/tailwind/TailwindResponsiveDesign.tsx

Phone, 375px: stacked

Desktop, 1024px: side by side

Import each responsive component into app/labs/lab2/tailwind/page.tsx and render it there. The five small demos come first, then TailwindResponsiveDesign. This is the page at the end of this section: the spacing, typography, and background samples stay above the responsive demos.

TailwindLabapp/labs/lab2/tailwind/page.tsx
import "./index.css";
import TailwindSpacing from "./TailwindSpacing";
import TailwindTypography from "./TailwindTypography";
import TailwindBackgroundColors from "./TailwindBackgroundColors";
import TailwindResponsiveBreakpoint from "./TailwindResponsiveBreakpoint";
import TailwindResponsiveShowHide from "./TailwindResponsiveShowHide";
import TailwindResponsiveFlex from "./TailwindResponsiveFlex";
import TailwindResponsiveGrid from "./TailwindResponsiveGrid";
import TailwindResponsiveSpacingText from "./TailwindResponsiveSpacingText";
import TailwindResponsiveDesign from "./TailwindResponsiveDesign";

export default function TailwindLab() {
  return (
    <div className="p-8">
      <h1 className="text-4xl font-bold mb-8">Tailwind CSS</h1>
      <TailwindSpacing />
      <hr className="my-8" />
      <TailwindTypography />
      <hr className="my-8" />
      <TailwindBackgroundColors />
      <hr className="my-8" />
      <TailwindResponsiveBreakpoint />
      <hr className="my-8" />
      <TailwindResponsiveShowHide />
      <hr className="my-8" />
      <TailwindResponsiveFlex />
      <hr className="my-8" />
      <TailwindResponsiveGrid />
      <hr className="my-8" />
      <TailwindResponsiveSpacingText />
      <hr className="my-8" />
      <TailwindResponsiveDesign />
    </div>
  );
}

2.3.5 Filters

Filter utilities apply visual effects — blur, brightness, contrast, grayscale, and more — straight onto an image or element. Blur a photo at four increasing strengths. Any photo under public/images works; the sample below reuses reactjs.jpg:

TailwindFiltersapp/labs/lab2/tailwind/TailwindFilters.tsx
export default function TailwindFilters() {
  // reactjs.jpg is used here so the lab runs out of the box.
  const src = "/images/reactjs.jpg";
  return (
    <div>
      <h2 className="text-2xl font-bold">Blurs</h2>
      <div className="flex">
        <img className="blur-none w-1/4" src={src} alt="blur none" />
        <img className="blur-sm w-1/4" src={src} alt="blur sm" />
        <img className="blur-lg w-1/4" src={src} alt="blur lg" />
        <img className="blur-2xl w-1/4" src={src} alt="blur 2xl" />
      </div>
    </div>
  );
}

Four copies of the same image sit side by side, the blur growing from imperceptible to nearly unrecognizable — each variation is nothing more than one utility class swapped for another:

TailwindFiltersapp/labs/lab2/tailwind/TailwindFilters.tsx

Blurs

blur noneblur smblur lgblur 2xl

Add TailwindFilters to app/labs/lab2/tailwind/page.tsx. Put this import after TailwindResponsiveDesign, and render the component after that one:

TailwindLabapp/labs/lab2/tailwind/page.tsx
import TailwindFilters from "./TailwindFilters";

      <TailwindResponsiveDesign />
      <hr className="my-8" />
      <TailwindFilters />

2.3.6 CSS Grid Layout Slides

Tailwind also wraps CSS Grid in utility classes: grid grid-cols-4 gap-4 turns a container into a four-column grid with consistent gutters, and children automatically wrap onto new rows once a row fills up. Start TailwindGrids.tsx with the outer wrapper and the h2 Tailwind Grids heading, then the four-column section:

TailwindGridsapp/labs/lab2/tailwind/TailwindGrids.tsx
export default function TailwindGrids() {
  return (
    <div>
      <h2 className="text-2xl font-bold">Tailwind Grids</h2>
      <div>
        <h3 className="mt-6 text-lg font-bold">4 Columns Grid</h3>
        <div className="grid grid-cols-4 gap-4">
          {Array.from({ length: 9 }, (_, i) => (
            <div key={i} className="text-center bg-blue-300 p-3">
              {String(i + 1).padStart(2, "0")}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

Nine numbered cells flow across four columns and wrap onto a third row for the last one. col-span-2 stretches a cell across two tracks of a three-column grid. Paste the TSX below after the 4 Columns Grid, still inside the outer div:

TailwindGridsapp/labs/lab2/tailwind/TailwindGrids.tsx
      <div>
        <h3 className="mt-6 text-lg font-bold">3 Columns Grid</h3>
        <div className="grid grid-cols-3 gap-4">
          <div className="text-center bg-blue-300 p-3">01</div>
          <div className="text-center bg-blue-300 p-3">02</div>
          <div className="text-center bg-blue-300 p-3">03</div>
          <div className="col-span-2 text-center bg-blue-300 p-3">04</div>
          <div className="text-center bg-blue-300 p-3">05</div>
          <div className="text-center bg-blue-300 p-3">06</div>
          <div className="col-span-2 text-center bg-blue-300 p-3">07</div>
        </div>
      </div>

The Grid system mixes an even two-column split with a twelve-column split for a one-third/two-thirds layout and a sidebar/content/sidebar layout — the same page layouts §2.1.18 built with float, this time with Grid. Paste the TSX below after the 3 Columns Grid, still inside the outer div. The heading is an h2, the same level as Tailwind Grids:

TailwindGridsapp/labs/lab2/tailwind/TailwindGrids.tsx
      <div id="wd-tailwind-grid-system" className="mt-6">
        <h2 className="text-2xl font-bold">Grid system</h2>
        <div className="grid grid-cols-2 gap-2">
          <div className="bg-red-500 text-white">
            <h3 className="text-lg font-bold">Left half</h3>
          </div>
          <div className="bg-blue-500 text-white">
            <h3 className="text-lg font-bold">Right half</h3>
          </div>
        </div>
        <div className="grid grid-cols-12 gap-2 mt-2">
          <div className="col-span-4 bg-yellow-500">
            <h3 className="text-lg font-bold">One third</h3>
          </div>
          <div className="col-span-8 bg-green-500 text-white">
            <h3 className="text-lg font-bold">Two thirds</h3>
          </div>
        </div>
        <div className="grid grid-cols-12 gap-2 mt-2">
          <div className="col-span-2 bg-black text-white">
            <h3 className="text-lg font-bold">Sidebar</h3>
          </div>
          <div className="col-span-8 bg-gray-500 text-white">
            <h3 className="text-lg font-bold">Main content</h3>
          </div>
          <div className="col-span-2 bg-blue-400">
            <h3 className="text-lg font-bold">Sidebar</h3>
          </div>
        </div>
      </div>

A twelve-column grid is the sweet spot for page layout because twelve divides evenly by two, three, four, and six — which is why col-span-4 (one third) and col-span-8 (two thirds) add up cleanly to twelve, and why the sidebar/content/sidebar row below it uses 2/8/2. Pasting those three blocks in order is the finished component, which is what the figure renders:

TailwindGridsapp/labs/lab2/tailwind/TailwindGrids.tsx
export default function TailwindGrids() {
  return (
    <div>
      <h2 className="text-2xl font-bold">Tailwind Grids</h2>
      <div>
        <h3 className="mt-6 text-lg font-bold">4 Columns Grid</h3>
        <div className="grid grid-cols-4 gap-4">
          {Array.from({ length: 9 }, (_, i) => (
            <div key={i} className="text-center bg-blue-300 p-3">
              {String(i + 1).padStart(2, "0")}
            </div>
          ))}
        </div>
      </div>
      <div>
        <h3 className="mt-6 text-lg font-bold">3 Columns Grid</h3>
        <div className="grid grid-cols-3 gap-4">
          <div className="text-center bg-blue-300 p-3">01</div>
          <div className="text-center bg-blue-300 p-3">02</div>
          <div className="text-center bg-blue-300 p-3">03</div>
          <div className="col-span-2 text-center bg-blue-300 p-3">04</div>
          <div className="text-center bg-blue-300 p-3">05</div>
          <div className="text-center bg-blue-300 p-3">06</div>
          <div className="col-span-2 text-center bg-blue-300 p-3">07</div>
        </div>
      </div>
      <div id="wd-tailwind-grid-system" className="mt-6">
        <h2 className="text-2xl font-bold">Grid system</h2>
        <div className="grid grid-cols-2 gap-2">
          <div className="bg-red-500 text-white">
            <h3 className="text-lg font-bold">Left half</h3>
          </div>
          <div className="bg-blue-500 text-white">
            <h3 className="text-lg font-bold">Right half</h3>
          </div>
        </div>
        <div className="grid grid-cols-12 gap-2 mt-2">
          <div className="col-span-4 bg-yellow-500">
            <h3 className="text-lg font-bold">One third</h3>
          </div>
          <div className="col-span-8 bg-green-500 text-white">
            <h3 className="text-lg font-bold">Two thirds</h3>
          </div>
        </div>
        <div className="grid grid-cols-12 gap-2 mt-2">
          <div className="col-span-2 bg-black text-white">
            <h3 className="text-lg font-bold">Sidebar</h3>
          </div>
          <div className="col-span-8 bg-gray-500 text-white">
            <h3 className="text-lg font-bold">Main content</h3>
          </div>
          <div className="col-span-2 bg-blue-400">
            <h3 className="text-lg font-bold">Sidebar</h3>
          </div>
        </div>
      </div>
    </div>
  );
}
TailwindGridsapp/labs/lab2/tailwind/TailwindGrids.tsx

Tailwind Grids

4 Columns Grid

01
02
03
04
05
06
07
08
09

3 Columns Grid

01
02
03
04
05
06
07

Grid system

Left half

Right half

One third

Two thirds

Sidebar

Main content

Sidebar

Add TailwindGrids to app/labs/lab2/tailwind/page.tsx. Put this import after TailwindFilters, and render the component after that one:

TailwindLabapp/labs/lab2/tailwind/page.tsx
import TailwindGrids from "./TailwindGrids";

      <TailwindFilters />
      <hr className="my-8" />
      <TailwindGrids />

2.3.7 Exercises

Use this checklist to confirm Lab 2 covers the CSS, icon, and Tailwind topics in §2.1–§2.3. Each item points back to the section where you built the worked example. When you are done, app/labs/lab2/page.tsx should import the CSS samples in order, and the Tailwind samples should live under app/labs/lab2/tailwind/. Each topic is listed once, with Lab, On your own, and With AI nested as a/b/c when that section has those blocks.

Here is the finished app/labs/lab2/page.tsx with every sample from §2.1 and §2.2 imported in order, plus the link to the Tailwind lab from §2.3. Compare it with yours: a missing import means a missing sample on /labs/lab2. Your On your own and With AI additions go on top of this, and the Tailwind samples stay in app/labs/lab2/tailwind/page.tsx:

Lab2 (finished)app/labs/lab2/page.tsx
import "./index.css";
import ForegroundColors from "./ForegroundColors";
import BackgroundColors from "./BackgroundColors";
import Borders from "./Borders";
import Padding from "./Padding";
import Margins from "./Margins";
import BoxModel from "./BoxModel";
import Corners from "./Corners";
import Dimensions from "./Dimensions";
import Display from "./Display";
import Positions from "./Positions";
import Zindex from "./Zindex";
import Float from "./Float";
import GridLayout from "./GridLayout";
import Flex from "./Flex";
import MediaQueriesDemo from "./MediaQueriesDemo";
import ReactIconsSampler from "./ReactIconsSampler";

export default function Lab2() {
  return (
    <div id="wd-lab2">
      <h2>Lab 2 - Cascading Style Sheets</h2>
      <p>
        <a href="/labs/lab2/tailwind">Open Tailwind CSS lab →</a>
      </p>

      <h3>Styling with the STYLE attribute</h3>
      <p>
        Style attribute allows configuring look and feel right on the element.
        Although it&apos;s very convenient it is considered bad practice and you
        should avoid using the style attribute
      </p>

      <div id="wd-css-id-selectors">
        <h3>ID selectors</h3>
        <p id="wd-id-selector-1">
          Instead of changing the look and feel of all the elements of the same
          name, e.g., P, we can refer to a specific element by its ID
        </p>
        <p id="wd-id-selector-2">
          Here&apos;s another paragraph using a different ID and a different look
          and feel
        </p>
      </div>

      <div id="wd-css-class-selectors">
        <h3>Class selectors</h3>
        <p className="wd-class-selector">
          Instead of using IDs to refer to elements, you can use an element&apos;s
          CLASS attribute
        </p>
        <h4 className="wd-class-selector">
          This heading has same style as paragraph above
        </h4>
      </div>

      <div id="wd-css-document-structure">
        <div className="wd-selector-1">
          <h3>Document structure selectors</h3>
          <div className="wd-selector-2">
            Selectors can be combined to refer elements in particular places in
            the document
            <p className="wd-selector-3">
              This paragraph&apos;s red background is referenced as
              <br />
              .wd-selector-1 .wd-selector-3
              <br />
              meaning the descendant of some ancestor.
              <br />
              <span className="wd-selector-4">
                Whereas this span is a direct child of its parent
              </span>
              <br />
              You can combine these relationships to create specific styles
              depending on the document structure
            </p>
          </div>
        </div>
      </div>

      <ForegroundColors />
      <BackgroundColors />
      <Borders />
      <Padding />
      <Margins />
      <BoxModel />
      <Corners />
      <Dimensions />
      <Display />
      <Positions />
      <Zindex />
      <Float />
      <GridLayout />
      <Flex />
      <MediaQueriesDemo />
      <ReactIconsSampler />
    </div>
  );
}
  1. Lab 2 page and CSS file (§2.1)
    1. Lab component — Create app/labs/lab2/page.tsx and index.css, and link Lab 2 from the Labs index and TOC.
  2. Selectors (§2.1.1–§2.1.5)
    1. Lab component — Practice the style attribute, then move rules into the CSS file with id, class, and document-structure selectors.
    2. On your own — Complete each section's On your own in §2.1.1–§2.1.5.
    3. With AI — Complete each section's With AI extra in §2.1.1–§2.1.5.
  3. Color, border, and box model (§2.1.7–§2.1.12)
    1. Lab component — Create the color, border, box-model, corner, dimension, and display samples and import them.
    2. On your own — Complete each section's On your own in §2.1.7–§2.1.12.
    3. With AI — Complete each section's With AI extra in §2.1.7–§2.1.12.
  4. Position, float, flex, and media queries (§2.1.13–§2.1.20)
    1. Lab component — Create the position, z-index, float, grid, flex, and media-query samples and import them.
    2. On your own — Complete each section's On your own in §2.1.13–§2.1.20.
    3. With AI — Complete each section's With AI extra in §2.1.13–§2.1.20.
  5. React Icons (§2.2)
    1. Lab component — Create ReactIconsSampler.tsx and import it.
    2. On your own — In ReactIconsSampler.tsx, import two more icons from families you have not used yet, give them a className for size or color, and keep them on the Lab 2 page.
    3. With AI — Ask the assistant to add two sample icons from other families — leave your personal pair as yours.
  6. Tailwind samples (§2.3)
    1. Lab component — Create the Tailwind samples under app/labs/lab2/tailwind/ — spacing, typography, backgrounds, responsive prefixes, filters, and grids.
    2. On your own — Complete each Tailwind section's On your own (spacing, typography, backgrounds, responsive, filters, grids).
    3. With AI — Complete each Tailwind section's With AI extra.

2.4 Styling Kambaz with CSS and Tailwind

Slides

Chapter 1 prototyped Kambaz with nothing but plain HTML, including table/tr/td elements to force content into side-by-side columns — functional, but exactly the kind of layout-via-table this chapter has spent §2.1.18–§2.1.19 replacing with CSS. Before restyling each screen, we wire Tailwind into the Kambaz shell the right way, then swap those tables for flex layouts. A single coverage checklist is in §2.4.10 — use it after you have walked through the screens, not instead of restyling them as you read.

In §2.3 the Tailwind lab imports the full library (@import "tailwindcss"), which includes Preflight. §2.2 defines that reset. Kambaz screens rely on the browser's default heading sizes, bullets, and margins, and Preflight would strip them, so kambaz.css skips the full import. The file opens with the theme and utilities layers and no Preflight, then a few app-wide rules — a system sans-serif base (without Preflight the browser often falls back to Times), box-sizing, and later the fixed-sidebar offset. Kambaz does not import the Labs file utilities.css. The Kambaz layout imports kambaz.css. (Course Navigation in §2.4.3 imports it too, so its list-group rules come along wherever that component renders. Next.js still bundles the file only once.) Putting font-sans on the root as well means Tailwind's system stack applies even if the CSS rule is incomplete:

Kambaz baseapp/(kambaz)/kambaz.css
@import "tailwindcss/theme" layer(theme);
@import "tailwindcss/utilities" layer(utilities);

/* System sans-serif — Tailwind utilities alone
   do not set the body font */
#wd-kambaz {
  font-family:
    system-ui,
    -apple-system,
    "Segoe UI",
    Roboto,
    "Helvetica Neue",
    "Noto Sans",
    "Liberation Sans",
    Arial,
    sans-serif;
  color: #212529;
  line-height: 1.5;
}
#wd-kambaz,
#wd-kambaz * {
  box-sizing: border-box;
}
KambazLayoutapp/(kambaz)/layout.tsx
import { ReactNode } from "react";
import "./kambaz.css";
import KambazNavigation from "./Navigation";

export default function KambazLayout({
  children,
}: Readonly<{ children: ReactNode }>) {
  return (
    <div id="wd-kambaz" className="font-sans">
      <KambazNavigation />
      <div className="wd-main-content-offset p-3">{children}</div>
    </div>
  );
}

With that shell in place, the remaining table wrappers come out of app/(kambaz)/layout.tsx (if any), app/(kambaz)/courses/[cid]/layout.tsx, app/(kambaz)/courses/[cid]/home/page.tsx, and app/(kambaz)/account/layout.tsx, replaced with flex containers so the Course Navigation sidebar, the Account Navigation sidebar, and the Course Status column sit beside the main content through CSS instead of table cells. Both course files come back into focus below once Navigation and Status are styled — including the responsive hide order (hidden lg:block for Status first, then hidden md:block for both sidebars together).

Each Kambaz screen below follows the same arc: a target screenshot, the plain Chapter 1 prototype already built, the code that closes the styling gap, and a styled result we can compare live.

2.4.1 Styling the Kambaz Navigation Sidebar Slides

The Kambaz Navigation sidebar from §1.4 was a plain vertical list of links. Pin it to the left edge as a fixed black column of icon-and-label tiles so the rest of Kambaz can scroll beside it.

The finished sidebar is expected to look like a narrow black column of centered icon links, with the active route highlighted in white and red (Figure 2.4.1):

Target Kambaz Navigation sidebar with Dashboard
Figure 2.4.1 — Kambaz Navigation

In Chapter 1 it was left looking like this — functional HTML, no real styling:

Chapter 1 — unstyledapp/book/ch1/embeds/PlainKambazNavigation.tsx

If we rebuild the sidebar with React Icons and Tailwind so each link becomes an icon-and-label tile, then pin the whole bar to the window, the gap closes. Keep every link from §1.4.3.1, in the same order, including the Northeastern link (wd-neu-link), which now shows the logo from public/images/NEU.png instead of the word Northeastern. Every link becomes the same black tile with a red icon, except Account, whose icon is white, and Dashboard, which is highlighted:

KambazNavigationapp/(kambaz)/Navigation.tsx
import { AiOutlineDashboard } from "react-icons/ai";
import { IoCalendarOutline } from "react-icons/io5";
import { LiaBookSolid, LiaCogSolid } from "react-icons/lia";
import { FaInbox, FaRegCircleUser } from "react-icons/fa6";
import Link from "next/link";

export default function KambazNavigation() {
  return (
    <nav
      id="wd-kambaz-navigation"
      className="fixed bottom-0 top-0 z-20 hidden w-[120px] bg-black md:block"
    >
      <a
        href="https://www.northeastern.edu/"
        id="wd-neu-link"
        target="_blank"
        rel="noreferrer"
        className="block bg-black py-3 text-center"
      >
        {/* eslint-disable-next-line @next/next/no-img-element */}
        <img
          src="/images/NEU.png"
          width={75}
          height={75}
          alt="Northeastern University"
          className="mx-auto"
        />
      </a>
      <Link
        href="/account"
        id="wd-account-link"
        className="block bg-black py-3 text-center text-sm text-white no-underline"
      >
        <FaRegCircleUser className="inline-block text-3xl text-white" />
        <br />
        Account
      </Link>
      <Link
        href="/dashboard"
        id="wd-dashboard-link"
        className="block bg-white py-3 text-center text-sm text-red-600 no-underline"
      >
        <AiOutlineDashboard className="inline-block text-3xl text-red-600" />
        <br />
        Dashboard
      </Link>
      <Link
        href="/dashboard"
        id="wd-course-link"
        className="block bg-black py-3 text-center text-sm text-white no-underline"
      >
        <LiaBookSolid className="inline-block text-3xl text-red-600" />
        <br />
        Courses
      </Link>
      <Link
        href="/calendar"
        id="wd-calendar-link"
        className="block bg-black py-3 text-center text-sm text-white no-underline"
      >
        <IoCalendarOutline className="inline-block text-3xl text-red-600" />
        <br />
        Calendar
      </Link>
      <Link
        href="/inbox"
        id="wd-inbox-link"
        className="block bg-black py-3 text-center text-sm text-white no-underline"
      >
        <FaInbox className="inline-block text-3xl text-red-600" />
        <br />
        Inbox
      </Link>
      <Link
        href="/labs"
        id="wd-labs-link"
        className="block bg-black py-3 text-center text-sm text-white no-underline"
      >
        <LiaCogSolid className="inline-block text-3xl text-red-600" />
        <br />
        Labs
      </Link>
    </nav>
  );
}

A handful of Tailwind utilities do the positioning work: fixed with top-0 bottom-0 stretches the sidebar the full height of the window and keeps it from scrolling with the page; hidden md:block hides it below the md breakpoint and reveals it again at md and up; and z-20 keeps it above the page content it now overlaps. That overlap is the catch: once the sidebar leaves the normal flow, the content beside it no longer knows to leave 120 pixels of room. If we append this to the kambaz.css base started above, scoped inside a media query, the offset only applies when the sidebar is visible:

Kambaz stylesapp/(kambaz)/kambaz.css
@media (min-width: 768px) {
  .wd-main-content-offset {
    margin-left: 120px;
  }
}

The logo is a plain <img>. If public/images/NEU.png isn't in your project yet, save any Northeastern logo there under that name. The eslint-disable comment only silences ESLint's suggestion to use next/image. With those classes in place, the sidebar looks like this (contained so fixed does not escape this figure):

Styled resultapp/(kambaz)/Navigation.tsx
Dashboard, Courses, and every other Kambaz screen render here, offset by wd-main-content-offset.

The finished sidebar is expected to:

  • be a narrow black column about 110–120 pixels wide
  • use red icons, except the Account icon, which is white
  • highlight one link with a white background and red text (Dashboard, for now); leave the others black with white text
  • center icons and labels in the bar

Which link is highlighted? In this chapter the Dashboard tile is highlighted by hand: its classes say bg-white text-red-600 and every other tile says bg-black text-white. So Dashboard stays highlighted on every Kambaz page, even on Account or Labs. That's expected for A2. Making the highlight follow the URL takes the usePathname() hook, which reads the current path so each link can compare it with its own href. §2.4.3 uses it for Course Navigation, and §3.10.1 applies the same idea to this sidebar.

2.4.2 Styling the Kambaz Dashboard Screen Slides

In §1.4.3 a plain CourseCard was already extracted and several of them rendered inside #wd-dashboard-courses. Those cards still look like unstyled HTML — add borders, shadows, and a responsive grid that wraps as the window narrows.

The finished Dashboard is expected to show a titled page of course cards in a responsive grid — four across at the widest width, fewer columns as the window shrinks — Figure 2.4.2a at a wide width, Figure 2.4.2b as it narrows:

Target Dashboard at wide width
Figure 2.4.2a — Dashboard Screen (wide)
Target Dashboard as the window narrows
Figure 2.4.2b — Dashboard Screen (medium)

In Chapter 1 it was left looking like this — functional HTML, no real styling:

Chapter 1 — unstyledapp/book/ch1/embeds/DashboardDemo.tsx

If we add Tailwind utilities to CourseCard itself, then turn the courses container into a responsive grid with gap-8, the cards close the gap. No compound Card/CardBody kit and no custom Row/Col — the same HTML from Chapter 1 stays in place and gets dressed with utility classes. Starting with CourseCard, the structure from §1.4.3 remains; border, shadow, fixed width, image crop, title truncation, and a primary-looking Go button do the rest:

CourseCardapp/(kambaz)/dashboard/CourseCard.tsx
import Link from "next/link";
import Image from "next/image";

export default function CourseCard({
  id, title, subtitle, image,
}: {
  id: string;
  title: string;
  subtitle: string;
  image: string;
}) {
  return (
    <div className="wd-dashboard-course w-[300px] max-w-full overflow-hidden rounded-lg border border-neutral-200 bg-white shadow-sm">
      <Link
        href={`/courses/${id}/home`}
        className="wd-dashboard-course-link block text-neutral-900 no-underline"
      >
        <Image
          src={image}
          width={300}
          height={160}
          alt={title}
          className="h-40 w-full object-cover"
        />
        <div className="p-4">
          <h5 className="m-0 mb-2 truncate text-lg font-semibold whitespace-nowrap">
            {title}
          </h5>
          <p className="wd-dashboard-course-title m-0 mb-3 h-[100px] overflow-hidden text-sm text-neutral-600">
            {subtitle}
          </p>
          <button
            type="button"
            className="inline-flex items-center justify-center rounded bg-blue-600 px-3 py-1.5 text-sm font-medium text-white"
          >
            Go
          </button>
        </div>
      </Link>
    </div>
  );
}

After that, the Dashboard container becomes one column by default, two from sm, three from xl, four from 2xl, with gap-8 (~32px) between cards. The existing CourseCard calls stay the same:

Dashboardapp/(kambaz)/dashboard/page.tsx
import CourseCard from "./CourseCard";

export default function Dashboard() {
  return (
    <div id="wd-dashboard">
      <h1 id="wd-dashboard-title">Dashboard</h1>
      <hr />
      <h2 id="wd-dashboard-published">Published Courses (3)</h2>
      <hr />
      <div
        id="wd-dashboard-courses"
        className="grid grid-cols-1 gap-8 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"
      >
        <CourseCard
          id="1234"
          title="CS1234 React JS"
          subtitle="Full Stack software developer"
          image="/images/reactjs.jpg"
        />
        <CourseCard
          id="2345"
          title="CS2345 Node JS"
          subtitle="Server side JavaScript"
          image="/images/nodejs.jpg"
        />
        <CourseCard
          id="3456"
          title="CS3456 MongoDB"
          subtitle="NoSQL Databases"
          image="/images/mongodb.jpg"
        />
      </div>
    </div>
  );
}

With those classes in place, the screen looks like this:

Styled resultapp/(kambaz)/dashboard/page.tsx

The finished Dashboard is expected to:

  • show the Dashboard link selected in the sidebar (red text, red icon, white background)
  • start with a Dashboard title and horizontal rule, then a Published Courses subtitle and a second rule
  • render at least 3 courses as cards, each linking to the course Home screen
  • keep cards roughly 300 pixels wide regardless of window width, with at least 32 pixels (the gap-8 gutter) of white space between them; the space grows when a grid column is wider than its card
  • fit at least 4 course cards in a row at the widest window size, wrapping remaining cards as the window narrows

2.4.3 Styling the Course Navigation Sidebar Slides

Clicking a course from the Dashboard opens that course's Home screen with its own Course Navigation sidebar from §1.4.4. Style that sidebar as a narrow list group with red idle links and a black left border on the active route.

The finished Course Navigation is expected to be a compact vertical list — red text for idle links, black text plus a left border for the active route (Figure 2.4.3):

Target Course Navigation sidebar
Figure 2.4.3 — Course Navigation

In Chapter 1 it was left looking like this — functional HTML, no real styling:

Chapter 1 — unstyledapp/book/ch1/embeds/PlainCourseNavigation.tsx

If we keep the sidebar narrow (~140px) — only the label column — and refactor the plain links into a list group, the gap closes. Keep the links in their §1.4.4 order (Home, Modules, Piazza, Zoom, Assignments, Quizzes, Grades, People) so the sidebar matches Figure 2.4.3. Here usePathname highlights the active route: it returns the current URL path, and each link compares that path with its own href. Nested paths count as active too (so /assignments/123 still lights up Assignments) with startsWith.

What's a hook? usePathname is the first hook in this book. A hook is a function whose name starts with use and that you call inside a component to get something React or Next.js provides; here, Next.js provides the current URL path. Call hooks only at the top level of a component, never inside loops or conditions, so they run in the same order every time the component runs. Most hooks need a Client Component, which is why this one comes with the "use client" line explained next. Chapter 4 writes hooks of our own.

The first line, "use client", is new. Next.js renders components on the server by default, and server components can't use hooks such as usePathname, which need the browser. "use client" at the very top of a file marks that component to run in the browser too, so the hook works. Components without hooks, like the Kambaz Navigation sidebar above, don't need it:

CourseNavigationapp/(kambaz)/courses/[cid]/Navigation.tsx
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import "../../kambaz.css";

export default function CourseNavigation({ cid }: { cid: string }) {
  const pathname = usePathname() ?? "";
  const home = `/courses/${cid}/home`;
  const modules = `/courses/${cid}/modules`;
  const assignments = `/courses/${cid}/assignments`;
  return (
    <div id="wd-courses-navigation" className="wd list-group rounded-none text-lg">
      <Link
        href={home}
        id="wd-course-home-link"
        className={
          pathname === home
            ? "list-group-item active border-0"
            : "list-group-item border-0 text-red-600"
        }
      >
        Home
      </Link>
      <Link
        href={modules}
        id="wd-course-modules-link"
        className={
          pathname === modules
            ? "list-group-item active border-0"
            : "list-group-item border-0 text-red-600"
        }
      >
        Modules
      </Link>
      {/* ...Piazza and Zoom, same pattern... */}
      <Link
        href={assignments}
        id="wd-course-assignments-link"
        className={
          pathname === assignments || pathname.startsWith(assignments + "/")
            ? "list-group-item active border-0"
            : "list-group-item border-0 text-red-600"
        }
      >
        Assignments
      </Link>
      {/* ...Quizzes, Grades, People, same pattern... */}
    </div>
  );
}

With list-group rules in kambaz.css, the column stays narrow, idle links are red, and the active link is black with a 3px left border:

Kambaz stylesapp/(kambaz)/kambaz.css
.list-group.wd {
  display: flex;
  flex-direction: column;
  width: 100%;
}

.list-group.wd > .list-group-item {
  display: block;
  padding: 0.4rem 0.75rem;
  text-decoration: none;
  border: 0;
  border-left: 3px solid transparent;
  color: #dc2626;
  background-color: transparent;
  white-space: nowrap;
}

.list-group.wd > .list-group-item.active {
  color: black;
  background-color: white;
  border-left: 3px solid black !important;
  font-weight: 600;
}

With those classes in place, the screen looks like this:

Styled resultapp/(kambaz)/courses/[cid]/Navigation.tsx

2.4.4 Styling the Modules Screen

The Modules list is shared between the Modules screen and the Home screen, so it gets styled once here. In §1.4.5 plain Module and Lesson components were already extracted — this section adds gray module headers, green lesson borders, and checkmark controls.

The finished Modules screen is expected to show a toolbar above a list of modules with gray title bars and green-bordered lessons, each with a checkmark on the right (Figure 2.4.4):

Target styled Modules screen
Figure 2.4.4 — Modules Screen

In Chapter 1 it was left looking like this — functional HTML, no real styling:

Chapter 1 — unstyledapp/book/ch1/embeds/ModulesDemo.tsx
  • Week 1, Lecture 1 - Course Introduction, Syllabus, Agenda
    • LEARNING OBJECTIVES
      • Introduction to the course
      • Learn what is Web Development
    • READING
      • Full Stack Developer - Chapter 1 - Introduction
      • Full Stack Developer - Chapter 2 - Creating User Interfaces
    • SLIDES
      • Introduction to Web Development
      • Creating an HTTP server with Node.js
      • Creating a React Application
  • Week 2
    • LEARNING OBJECTIVES
      • Learn how to create user interfaces with HTML
  • Week 3
    • LEARNING OBJECTIVES
      • CSS Styling

If we keep the HTML from Chapter 1 and add Tailwind classes, the Modules list closes the gap. A green FaCheckCircle icon from react-icons covers the publish indicator every module and lesson needs.

If we drop that icon into Module and add a gray header bar plus spacing:

Moduleapp/(kambaz)/courses/[cid]/modules/Module.tsx
import type { ReactNode } from "react";
import { FaCheckCircle } from "react-icons/fa";

export default function Module({
  title,
  children,
}: {
  title: string;
  children?: ReactNode;
}) {
  return (
    <li className="wd-module mb-5 overflow-hidden border border-neutral-400 p-0 text-xl">
      <div className="wd-title flex items-center justify-between bg-neutral-200 p-3 ps-2">
        <span>{title}</span>
        <FaCheckCircle className="text-green-600" />
      </div>
      <ul className="wd-lessons m-0 list-none p-0">{children}</ul>
    </li>
  );
}

After styling Lesson the same way — checkmark on the right, green left border for the accent (plain CSS in kambaz.css works too if preferred):

Lessonapp/(kambaz)/courses/[cid]/modules/Lesson.tsx
import type { ReactNode } from "react";
import { FaCheckCircle } from "react-icons/fa";

export default function Lesson({
  title,
  children,
}: {
  title: string;
  children?: ReactNode;
}) {
  return (
    <li className="wd-lesson border-l-[3px] border-green-600 p-3 pl-1">
      <div className="flex items-center justify-between">
        <span className="wd-title">{title}</span>
        <FaCheckCircle className="text-green-600" />
      </div>
      <ul className="wd-content mt-2 list-disc pl-6">{children}</ul>
    </li>
  );
}

The Modules page keeps mounting the same Module/Lesson tree from Chapter 1. With the toolbar styled via flex and explicit light borders — bare border often renders near-black — the controls match Canvas with border-neutral-300 on secondary buttons and border-red-600 on the red + Module button:

Modules toolbarapp/(kambaz)/courses/[cid]/modules/page.tsx
<div className="mb-3 flex flex-wrap items-center gap-2">
  <button
    type="button"
    className="rounded border border-neutral-300 bg-white px-3 py-1.5 text-sm"
  >
    Collapse All
  </button>
  <button
    type="button"
    className="rounded border border-neutral-300 bg-white px-3 py-1.5 text-sm"
  >
    View Progress
  </button>
  <select
    defaultValue="publish-all"
    className="rounded border border-neutral-300 bg-white px-3 py-1.5 text-sm"
  >
    <option value="publish-all">Publish All</option>
  </select>
  <button
    type="button"
    className="rounded border border-red-600 bg-red-600 px-3 py-1.5 text-sm font-medium text-white"
  >
    + Module
  </button>
</div>
{/* ...Module / Lesson tree... */}

Put the toolbar above the module tree in modules/page.tsx. The tree is the same Module/Lesson markup from §1.4.5; the only new class on it is m-0 list-none p-0 on <ul id="wd-modules">, which removes the bullets and the browser's left indent so the modules line up with the toolbar. The listing shows one module to save space. Keep every module and lesson you built in Chapter 1 where the comments say so:

Modules pageapp/(kambaz)/courses/[cid]/modules/page.tsx
import Lesson from "./Lesson";
import Module from "./Module";

export default function Modules() {
  return (
    <div>
      <div className="mb-3 flex flex-wrap items-center gap-2">
        <button
          type="button"
          className="rounded border border-neutral-300 bg-white px-3 py-1.5 text-sm"
        >
          Collapse All
        </button>
        <button
          type="button"
          className="rounded border border-neutral-300 bg-white px-3 py-1.5 text-sm"
        >
          View Progress
        </button>
        <select
          defaultValue="publish-all"
          className="rounded border border-neutral-300 bg-white px-3 py-1.5 text-sm"
        >
          <option value="publish-all">Publish All</option>
        </select>
        <button
          type="button"
          className="rounded border border-red-600 bg-red-600 px-3 py-1.5 text-sm font-medium text-white"
        >
          + Module
        </button>
      </div>
      <ul id="wd-modules" className="m-0 list-none p-0">
        <Module title="Week 1, Lecture 1 - Course Introduction, Syllabus, Agenda">
          <Lesson title="LEARNING OBJECTIVES">
            <li className="wd-content-item">Introduction to the course</li>
            <li className="wd-content-item">Learn what is Web Development</li>
          </Lesson>
          <Lesson title="READING">
            <li className="wd-content-item">
              Full Stack Developer - Chapter 1 - Introduction
            </li>
          </Lesson>
          {/* ...keep the rest of your Chapter 1 lessons (SLIDES, ...) */}
        </Module>
        {/* ...keep your other Chapter 1 modules (Week 2, ...) */}
      </ul>
    </div>
  );
}

With those classes in place, the screen looks like this:

Styled resultapp/(kambaz)/courses/[cid]/modules/page.tsx
  • Week 1, Lecture 1 - Course Introduction, Syllabus, Agenda
    • LEARNING OBJECTIVES
      • Introduction to the course
      • Learn what is Web Development
    • READING
      • Full Stack Developer - Chapter 1 - Introduction

The finished Modules screen is expected to:

  • show a row of controls (Collapse All, View Progress, a Publish All dropdown, and an Add Module button) above the module list
  • give each module title a gray header bar, and each lesson a green left border
  • show a green checkmark control on the right of both modules and lessons

2.4.5 Styling the Home Screen

Since the Home screen's main content is the Modules list just styled, only the Course Status column on the right remains — plus swapping the table layout for flex so four columns sit side by side on a wide screen.

The finished Home screen is expected to show Course Navigation, the Modules list, and a Course Status column of styled action buttons on a wide layout (Figure 2.4.5):

Target Home screen with four columns
Figure 2.4.5 — Home Screen

In Chapter 1 it was left looking like this — functional HTML, no real styling:

Chapter 1 — unstyledapp/book/ch1/embeds/HomeDemo.tsx

Courses 1234


  • Week 1, Lecture 1 - Course Introduction, Syllabus, Agenda
    • LEARNING OBJECTIVES
      • Introduction to the course
      • Learn what is Web Development

Course Status








If we restyle the Course Status buttons with Tailwind and React Icons — splitting Unpublish/Publish into a two-column row, and stacking the rest full width — the Status column closes the gap:

CourseStatusapp/(kambaz)/courses/[cid]/home/Status.tsx
import { FaCheckCircle } from "react-icons/fa";
import { MdDoNotDisturbAlt } from "react-icons/md";

export default function CourseStatus() {
  return (
    <div id="wd-course-status">
      <h2 className="mb-3 text-xl font-semibold">Course Status</h2>
      <div className="mb-1 flex gap-1">
        <button
          type="button"
          className="inline-flex min-w-0 flex-1 items-center justify-center rounded border border-neutral-300 bg-white px-1.5 py-1.5 text-xs"
        >
          <MdDoNotDisturbAlt className="me-1 shrink-0 text-base" /> Unpublish
        </button>
        <button
          type="button"
          className="inline-flex min-w-0 flex-1 items-center justify-center rounded bg-green-600 px-1.5 py-1.5 text-xs text-white hover:bg-green-700"
        >
          <FaCheckCircle className="me-1 shrink-0 text-base" /> Publish
        </button>
      </div>
      <button
        type="button"
        className="mb-1 flex w-full items-center rounded border border-neutral-300 bg-white px-3 py-2 text-left text-sm"
      >
        {/* icon */} Import Existing Content
      </button>
      {/* ...repeat full-width bordered buttons for Import from Commons,
             Choose Home Page, and the rest... */}
    </div>
  );
}

Once the pieces are styled, the course layout and Home page can replace remaining tables with flex: app/(kambaz)/courses/[cid]/layout.tsx and home/page.tsx swap their remaining table elements for flex divs. On a wide screen the result is four columns — Kambaz Navigation, Course Navigation, Modules, and Course Status. As the window narrows, columns hide in this order:

  • Course Status first — wrapped in hidden lg:block so it disappears below the lg breakpoint while both sidebars stay visible.
  • Kambaz Navigation and Course Navigation together — both use hidden md:block (Kambaz Navigation already does; Course Navigation gets the same treatment in the course layout) so they leave at the same md width, leaving Modules full width.
CoursesLayoutapp/(kambaz)/courses/[cid]/layout.tsx
<div className="flex gap-4">
  <div className="hidden w-[140px] shrink-0 md:block">
    <CourseNavigation cid={cid} />
  </div>
  <div className="min-w-0 flex-1">{children}</div>
</div>

Home imports the Modules page from §2.4.4:

Homeapp/(kambaz)/courses/[cid]/home/page.tsx
import Modules from "../modules/page";
import CourseStatus from "./Status";

export default function Home() {
  return (
    <div id="wd-home" className="flex gap-4">
      <div className="min-w-0 flex-1">
        <Modules />
      </div>
      <div className="hidden w-[250px] shrink-0 lg:block">
        <CourseStatus />
      </div>
    </div>
  );
}

With those classes in place, the screen looks like this:

Styled resultapp/(kambaz)/courses/[cid]/home/page.tsx
  • Week 1, Lecture 1 - Course Introduction, Syllabus, Agenda
    • LEARNING OBJECTIVES
      • Introduction to the course
      • Learn what is Web Development
    • READING
      • Full Stack Developer - Chapter 1 - Introduction

2.4.6 Implementing the People Screen Slides

The People screen lists the students, teaching assistants, and faculty enrolled in a course as a table. Unlike the screens above, there is no Chapter 1 prototype — this screen is built and styled entirely in this chapter.

The finished People screen is expected to show a clean roster table with a user icon beside each name and alternating row shading (Figure 2.4.6):

Target People table screen
Figure 2.4.6 — People Screen

If we style a plain HTML table with Tailwind — one row per person — the roster takes shape:

PeopleTableapp/(kambaz)/courses/[cid]/people/table/page.tsx
import { FaUserCircle } from "react-icons/fa";

export default function PeopleTable() {
  return (
    <div id="wd-people-table" className="overflow-x-auto">
      <table className="w-full border-collapse text-left text-sm">
        <thead>
          <tr className="border-b border-neutral-300">
            <th className="p-2">Name</th>
            <th className="p-2">Login ID</th>
            <th className="p-2">Section</th>
            <th className="p-2">Role</th>
            <th className="p-2">Last Activity</th>
            <th className="p-2">Total Activity</th>
          </tr>
        </thead>
        <tbody>
          <tr className="odd:bg-neutral-50">
            <td className="p-2 text-nowrap">
              <FaUserCircle className="me-2 inline text-4xl text-neutral-500" />
              Tony Stark
            </td>
            <td className="p-2">001234561S</td>
            <td className="p-2">S101</td>
            <td className="p-2">STUDENT</td>
            <td className="p-2">2020-10-01</td>
            <td className="p-2">10:21:32</td>
          </tr>
          <tr className="odd:bg-neutral-50">
            <td className="p-2 text-nowrap">
              <FaUserCircle className="me-2 inline text-4xl text-neutral-500" />
              Bruce Wayne
            </td>
            <td className="p-2">001234562S</td>
            <td className="p-2">S101</td>
            <td className="p-2">STUDENT</td>
            <td className="p-2">2020-11-02</td>
            <td className="p-2">23:32:23</td>
          </tr>
          <tr className="odd:bg-neutral-50">
            <td className="p-2 text-nowrap">
              <FaUserCircle className="me-2 inline text-4xl text-neutral-500" />
              Steve Rogers
            </td>
            <td className="p-2">001234563S</td>
            <td className="p-2">S101</td>
            <td className="p-2">STUDENT</td>
            <td className="p-2">2020-10-02</td>
            <td className="p-2">13:21:32</td>
          </tr>
          <tr className="odd:bg-neutral-50">
            <td className="p-2 text-nowrap">
              <FaUserCircle className="me-2 inline text-4xl text-neutral-500" />
              Natasha Romanoff
            </td>
            <td className="p-2">001234564S</td>
            <td className="p-2">S101</td>
            <td className="p-2">TA</td>
            <td className="p-2">2020-11-05</td>
            <td className="p-2">11:22:33</td>
          </tr>
        </tbody>
      </table>
    </div>
  );
}

With those classes in place, the screen looks like this. The People link in the Course Navigation sidebar should reach this table:

Styled resultapp/(kambaz)/courses/[cid]/people/table/page.tsx
NameLogin IDSectionRoleLast ActivityTotal Activity
Tony Stark001234561SS101STUDENT2020-10-0110:21:32
Bruce Wayne001234562SS101STUDENT2020-11-0223:32:23
Steve Rogers001234563SS101STUDENT2020-10-0213:21:32
Natasha Romanoff001234564SS101TA2020-11-0511:22:33

2.4.7 Styling the Assignments Screen

Same arc as Dashboard and Modules: the plain AssignmentItem from §1.4.7 stays, and each row plus the search/toolbar layout get Tailwind and React Icons.

The finished Assignments screen is expected to show a search field on the left, action buttons on the right, a gray group header, and green-bordered assignment rows (Figure 2.4.7):

Target Assignments screen
Figure 2.4.7 — Assignments Screen

In Chapter 1 it was left looking like this — functional HTML, no real styling:

Chapter 1 — unstyledapp/book/ch1/embeds/AssignmentsDemo.tsx

Courses 1234


ASSIGNMENTS 40% of Total

  • A1 - ENV + HTML
    Multiple Modules | Not available until May 6 at 12:00am |
    Due May 13 at 11:59pm | 100 pts
  • A2 - CSS + TAILWIND
    Multiple Modules | Not available until May 13 at 12:00am |
    Due May 20 at 11:59pm | 100 pts
  • A3 - JAVASCRIPT + REACT
    Multiple Modules | Not available until May 20 at 12:00am |
    Due May 27 at 11:59pm | 100 pts

If we add Tailwind to AssignmentItem (green left border like Lesson, title weight, muted details), then lay out the search field and toolbar with flex utilities, the gap closes:

AssignmentItemapp/(kambaz)/courses/[cid]/assignments/AssignmentItem.tsx
import Link from "next/link";
import { FaFileAlt } from "react-icons/fa";

export default function AssignmentItem({
  cid, aid, title, details,
}: {
  cid: string;
  aid: string;
  title: string;
  details: string;
}) {
  return (
    <li className="wd-assignment-list-item mb-3 flex gap-3 border border-neutral-300 border-l-[3px] border-l-green-600 bg-white p-3">
      <FaFileAlt className="mt-1 shrink-0 text-xl text-green-700" />
      <div>
        <Link
          href={`/courses/${cid}/assignments/${aid}`}
          className="wd-assignment-link font-semibold text-neutral-900 no-underline"
        >
          {title}
        </Link>
        <div className="mt-1 text-sm text-neutral-600">{details}</div>
      </div>
    </li>
  );
}

On the Assignments page, the search field sits on the left (with a magnifying-glass icon) and the + Group / + Assignment buttons on the right. The group header can reuse the same gray bar treatment as a module title:

Assignmentsapp/(kambaz)/courses/[cid]/assignments/page.tsx
import { FaPlus, FaSearch } from "react-icons/fa";
import AssignmentItem from "./AssignmentItem";

export default async function Assignments({
  params,
}: {
  params: Promise<{ cid: string }>;
}) {
  const { cid } = await params;
  return (
    <div id="wd-assignments">
      <div className="mb-4 flex flex-wrap items-center justify-between gap-2">
        <div className="relative">
          <FaSearch className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-neutral-500" />
          <input
            placeholder="Search for Assignments"
            id="wd-search-assignment"
            className="rounded border py-1.5 pr-3 pl-9 text-sm"
          />
        </div>
        <div className="flex gap-2">
          <button
            id="wd-add-assignment-group"
            type="button"
            className="inline-flex items-center gap-1 rounded border px-3 py-1.5 text-sm"
          >
            <FaPlus /> Group
          </button>
          <button
            id="wd-add-assignment"
            type="button"
            className="inline-flex items-center gap-1 rounded bg-red-600 px-3 py-1.5 text-sm font-medium text-white"
          >
            <FaPlus /> Assignment
          </button>
        </div>
      </div>
      <h3
        id="wd-assignments-title"
        className="mb-3 flex items-center justify-between rounded bg-neutral-200 p-3 text-lg"
      >
        <span>ASSIGNMENTS 40% of Total</span>
        <button type="button" className="rounded border bg-white px-2 py-0.5 text-sm">
          <FaPlus />
        </button>
      </h3>
      <ul id="wd-assignment-list" className="m-0 list-none p-0">
        <AssignmentItem
          cid={cid}
          aid="123"
          title="A1 - ENV + HTML"
          details="Multiple Modules | Not available until May 6 at 12:00am | Due May 13 at 11:59pm | 100 pts"
        />
        {/* ...remaining AssignmentItems... */}
      </ul>
    </div>
  );
}

With those classes in place, the screen looks like this:

Styled resultapp/(kambaz)/courses/[cid]/assignments/page.tsx

ASSIGNMENTS 40% of Total

  • A1 - ENV + HTML
    Multiple Modules | Not available until May 6 at 12:00am | Due May 13 at 11:59pm | 100 pts

The finished Assignments screen is expected to:

  • float the + Group and + Assignment buttons to the right, colored like the buttons in Modules, each with a plus icon
  • render a Search for Assignments field on the left with a placeholder and a magnifying-glass icon
  • use Tailwind margin and padding utilities for white space around and between assignment groups, not manual pixel values
  • give each AssignmentItem a green left border, matching the lesson border style from §2.4.4
  • render each assignment title (A1, A2, …) and its due-date/points subtext as shown in the screenshots — exact dates and times may differ

2.4.8 Styling the Assignment Editor Screen (On Your Own)

Clicking an assignment's title opens the Assignment Editor from §1.4.8 — for now every assignment opens the same editor content; a later chapter wires each assignment to its own data. Tailwind form utilities turn Assignment Name, Description, Points, and Due Date into a clean, labeled form instead of a raw HTML table.

The finished editor is expected to look like a structured form with labeled fields, aligned controls, and Cancel/Save actions at the bottom (Figure 2.4.8):

Target Assignment Editor screen
Figure 2.4.8 — Assignment Editor

In Chapter 1 it was left looking like this — functional HTML, no real styling:

Chapter 1 — unstyledapp/book/ch1/embeds/AssignmentEditorDemo.tsx





Online Entry Options




Assign








Cancel Save

The full form markup already lives in assignments/[aid]/page.tsx. Starting from that structure, Tailwind form utilities — labels above fields, full-width inputs, and spacing instead of table cells — produce the labeled layout:

AssignmentEditorapp/(kambaz)/courses/[cid]/assignments/[aid]/page.tsx
import Link from "next/link";

export default function AssignmentEditor() {
  return (
    <div id="wd-assignments-editor">
      <label htmlFor="wd-name">Assignment Name</label>
      <input id="wd-name" defaultValue="A1 - ENV + HTML" />
      <br />
      <br />
      <textarea id="wd-description">
        The assignment is available online Submit a link to the landing page of
        your Web application running on Vercel.
      </textarea>
      <br />
      <table>
        <tbody>
          <tr>
            <td align="right" valign="top">
              <label htmlFor="wd-points">Points</label>
            </td>
            <td>
              <input id="wd-points" defaultValue={100} />
            </td>
          </tr>
          {/* ...remaining fields from Chapter 1... */}
        </tbody>
      </table>
      <br />
      <Link href="/courses/1234/assignments" id="wd-cancel">Cancel</Link>{" "}
      <Link href="/courses/1234/assignments" id="wd-save">Save</Link>
    </div>
  );
}

The starting markup is still the Chapter 1 form; Tailwind form utilities turn it into the labeled layout in the target. The demo below shows the current starting point — it should still look unstyled until the Tailwind classes are in place:

Your turn to styleapp/(kambaz)/courses/[cid]/assignments/[aid]/page.tsx




Cancel Save

Finished Tailwind editor for lecture — the same ids as §1.4.8, with labels in a right-aligned column and the submission options and Assign fields in bordered boxes:

AssignmentEditor (styled)app/(kambaz)/courses/[cid]/assignments/[aid]/page.tsx
import Link from "next/link";

export default function AssignmentEditor() {
  return (
    <div id="wd-assignments-editor" className="max-w-3xl space-y-4 p-4">
      <div>
        <label htmlFor="wd-name" className="mb-1 block font-medium">
          Assignment Name
        </label>
        <input
          id="wd-name"
          defaultValue="A1 - ENV + HTML"
          className="w-full rounded border px-3 py-2"
        />
      </div>
      <textarea
        id="wd-description"
        rows={6}
        defaultValue="The assignment is available online Submit a link to the landing page of your Web application running on Vercel."
        className="w-full rounded border px-3 py-2"
      />
      <div className="grid grid-cols-[10rem_1fr] items-center gap-4">
        <label htmlFor="wd-points" className="text-right">Points</label>
        <input id="wd-points" defaultValue={100} className="w-full rounded border px-3 py-2" />

        <label htmlFor="wd-group" className="text-right">Assignment Group</label>
        <select id="wd-group" defaultValue="ASSIGNMENTS" className="w-full rounded border px-3 py-2">
          <option value="ASSIGNMENTS">ASSIGNMENTS</option>
          <option value="QUIZZES">QUIZZES</option>
          <option value="EXAMS">EXAMS</option>
          <option value="PROJECT">PROJECT</option>
        </select>

        <label htmlFor="wd-display-grade-as" className="text-right">Display Grade as</label>
        <select id="wd-display-grade-as" defaultValue="PERCENTAGE" className="w-full rounded border px-3 py-2">
          <option value="PERCENTAGE">Percentage</option>
          <option value="POINTS">Points</option>
        </select>

        <label htmlFor="wd-submission-type" className="self-start pt-2 text-right">
          Submission Type
        </label>
        <div className="space-y-3 rounded border p-4">
          <select id="wd-submission-type" defaultValue="ONLINE" className="w-full rounded border px-3 py-2">
            <option value="ONLINE">Online</option>
            <option value="ON_PAPER">On Paper</option>
          </select>
          <div className="font-semibold">Online Entry Options</div>
          <div className="flex items-center gap-2">
            <input type="checkbox" id="wd-text-entry" />
            <label htmlFor="wd-text-entry">Text Entry</label>
          </div>
          <div className="flex items-center gap-2">
            <input type="checkbox" id="wd-website-url" />
            <label htmlFor="wd-website-url">Website URL</label>
          </div>
          <div className="flex items-center gap-2">
            <input type="checkbox" id="wd-media-recordings" />
            <label htmlFor="wd-media-recordings">Media Recordings</label>
          </div>
          <div className="flex items-center gap-2">
            <input type="checkbox" id="wd-student-annotation" />
            <label htmlFor="wd-student-annotation">Student Annotation</label>
          </div>
          <div className="flex items-center gap-2">
            <input type="checkbox" id="wd-file-upload" />
            <label htmlFor="wd-file-upload">File Uploads</label>
          </div>
        </div>

        <div className="self-start pt-2 text-right">Assign</div>
        <div className="space-y-3 rounded border p-4">
          <div>
            <label htmlFor="wd-assign-to" className="mb-1 block font-semibold">Assign to</label>
            <input id="wd-assign-to" defaultValue="Everyone" className="w-full rounded border px-3 py-2" />
          </div>
          <div>
            <label htmlFor="wd-due-date" className="mb-1 block font-semibold">Due</label>
            <input type="date" id="wd-due-date" defaultValue="2024-05-13" className="w-full rounded border px-3 py-2" />
          </div>
          <div className="grid grid-cols-2 gap-4">
            <div>
              <label htmlFor="wd-available-from" className="mb-1 block font-semibold">Available from</label>
              <input type="date" id="wd-available-from" defaultValue="2024-05-06" className="w-full rounded border px-3 py-2" />
            </div>
            <div>
              <label htmlFor="wd-available-until" className="mb-1 block font-semibold">Until</label>
              <input type="date" id="wd-available-until" defaultValue="2024-05-20" className="w-full rounded border px-3 py-2" />
            </div>
          </div>
        </div>
      </div>
      <hr />
      <div className="flex justify-end gap-2">
        <Link href="/courses/1234/assignments" id="wd-cancel" className="rounded bg-neutral-200 px-4 py-2">
          Cancel
        </Link>
        <Link href="/courses/1234/assignments" id="wd-save" className="rounded bg-red-600 px-4 py-2 text-white">
          Save
        </Link>
      </div>
    </div>
  );
}
AssignmentEditor (styled)app/(kambaz)/courses/[cid]/assignments/[aid]/page.tsx
Online Entry Options
Assign

2.4.9 Styling the Account Screens (On Your Own) Slides

The Sign in, Sign up, and Profile screens from §1.4.2, along with the Account Navigation sidebar, get Tailwind form and button classes next. The Course Navigation sidebar from §2.4.3 is a model for the Account Navigation sidebar so the whole account section feels consistent with the rest of Kambaz.

The finished Account screens are expected to show a narrow navigation sidebar beside clean, centered form fields and primary action buttons —Figure 2.4.9a (Sign in) and Figure 2.4.9b (Profile):

Target Account Sign in screen
Figure 2.4.9a — Account Sign in
Target Account Profile screen
Figure 2.4.9b — Account Profile

In Chapter 1 it was left looking like this — functional HTML, no real styling:

Chapter 1 — unstyledapp/book/ch1/embeds/AccountScreensDemo.tsx

Profile








Sign out

If we start with Sign in and apply Tailwind form utilities for full-width inputs, spacing, and a primary Sign in button — then reuse the same patterns on Sign up and Profile — the account forms close the gap. This markup replaces what your Chapter 1 Signin function returns. Keep the import Link from "next/link"; line at the top of the file. Everything else from §1.4.2.1 stays the same: the wd-username/wd-password classes, the defaultValue starter credentials, and the /dashboard target from §1.4.3. Only Tailwind classes are added:

Signinapp/(kambaz)/account/signin/page.tsx
<div id="wd-signin-screen" className="max-w-sm">
  <h1 className="mb-3 text-2xl font-semibold">Sign in</h1>
  <input
    placeholder="username"
    defaultValue="ada"
    className="wd-username mb-2 w-full rounded border border-neutral-300 px-3 py-2"
  />
  <input
    placeholder="password"
    type="password"
    defaultValue="123"
    className="wd-password mb-2 w-full rounded border border-neutral-300 px-3 py-2"
  />
  <Link
    id="wd-signin-btn"
    href="/dashboard"
    className="mb-2 block w-full rounded bg-blue-600 px-3 py-2 text-center text-white no-underline"
  >
    Sign in
  </Link>
  <Link id="wd-signup-link" href="/account/signup">
    Sign up
  </Link>
</div>

With those patterns in place on Sign in, Sign up, Profile, and account/Navigation.tsx, the account section matches the target. The demo below renders the Sign in markup from the listing above:

Your turn to styleapp/(kambaz)/account/signin/page.tsx

Sign up and Profile follow the same way, reusing the classes above as a template, and /account/signin remains the first screen a visitor sees when navigating to Kambaz.

2.4.10 Exercises

Use this checklist to confirm the Kambaz restyle covers every screen in §2.4. Each item points back to the section where you styled the worked example. Restyle the screens in order as you read — this list is for checking coverage, not a substitute for the walkthroughs. Assignment Editor and Account stay On your own: match the figures and interactive demos in those sections. Each screen is listed once, with Lab, On your own, and With AI nested as a/b/c.

  1. Kambaz Navigation (§2.4.1)
    1. Lab component — Style Kambaz Navigation and replace the table layout with flex.
    2. On your own — In Navigation.tsx, finish remaining sidebar links with fitting React Icons, then confirm the active link and wd-main-content-offset still keep content clear of the fixed bar.
    3. With AI — Add a second sample tile (wd-ai-nav-help to /labs) — leave your personal icons as yours.
  2. Dashboard (§2.4.2)
    1. Lab component — Style the Dashboard and CourseCard.
    2. On your own — Personalize a card and confirm the responsive grid.
    3. With AI — Add a fourth sample course card — leave your personal card as yours.
  3. Course Navigation (§2.4.3)
    1. Lab component — Style Course Navigation.
    2. On your own — Finish the list-group links, active border, and ~140px sidebar.
    3. With AI — Add a second sample course link — leave your personal link as yours.
  4. Modules (§2.4.4)
    1. Lab component — Style Modules, Module, and Lesson.
    2. On your own — Add a module or lesson with your title.
    3. With AI — Add a second sample module — leave your personal title as yours.
  5. Home (§2.4.5)
    1. Lab component — Style Home and Course Status.
    2. On your own — Finish the Status buttons and confirm Status stacks below lg and sidebars below md.
    3. With AI — Add a second sample status action — leave your personal button as yours.
  6. People (§2.4.6)
    1. Lab component — Style the People table.
    2. On your own — Show at least three people rows.
    3. With AI — Add three sample roster rows — leave your personal rows as yours.
  7. Assignments (§2.4.7)
    1. Lab component — Style the Assignments screen.
    2. On your own — Add one more assignment row.
    3. With AI — Add a second sample assignment — leave your personal row as yours.
  8. Assignment Editor (§2.4.8)
    1. Lab component — Style the Assignment Editor to match the figures and interactive demo (On your own).
    2. On your own — Apply Tailwind form utilities on the editor; Cancel and Save return to the list.
    3. With AI — Add a second sample field on the editor — leave your personal labels as yours.
  9. Account screens (§2.4.9)
    1. Lab component — Style Sign in, Sign up, Profile, and Account Navigation (On your own).
    2. On your own — Style the account screens and nav so /account/signin is still the first Kambaz screen.
    3. With AI — Add a sample note field on Sign in — do not change Sign up, Profile, or routing.

2.5 Delivery

Submit this chapter's work as a new branch on the same webdev-client repository and deployment from Chapter 1, so graders can compare Chapter 1's HTML-only prototype against this chapter's version styled with CSS and Tailwind, side by side.

  1. Finish every exercise described in this chapter inside the same webdev-client project used in Chapter 1.
  2. Create a branch named a2, then add, commit, and push it to the same GitHub repository from §1.5:
git checkout -b a2
git add .
git commit -am "a2 CSS and Tailwind"
git push -u origin a2
  1. Vercel builds every branch you push as a Preview deployment, separate from your Chapter 1 main (Production) deployment; you don't need to change any setting. Open your project on Vercel, go to Deployments, and click the newest deployment of the a2 branch. Its Domains list shows a branch URL like webdev-client-git-a2-<your-vercel-team>.vercel.app, with -git-a2- in the name. That branch URL is the one to submit for A2. It always shows the latest push to a2. Don't submit your production URL (that is your A1 site from main) or a per-deployment URL with a random code, such as webdev-client-8f3k2j1x9-<your-vercel-team>.vercel.app; neither one contains -git-a2-.
  2. Confirm app/labs/TOC.tsx and app/labs/page.tsx still list every lab and Kambaz, plus a link to your GitHub repository with id wd-github and your full name (first name first, last name second, matching Canvas) on the Labs page — the same requirements from §1.8, now revisited for this chapter.
  3. Push any remaining changes to the a2 branch and confirm the branch deployment on Vercel reflects them.
  4. On kambaz.dev/assignments/a2 (not in Canvas), submit both the GitHub repository URL (pointed at the a2 branch) and the Vercel deployment URL for that branch. Disable Vercel's Deployment Protection on that deployment, as in §1.6, so graders can open it without signing in.

Continue practicing in Labs, browse Lab 2 intermediate steps, or open the live Kambaz prototype to see this chapter's styling applied.

2.6 References

This chapter is about how a page looks: CSS rules, selectors, the box model, layout, React Icons, and Tailwind utility classes. The linked terms open the in-book pages; the list after that names the CSS and layout ideas you practiced that do not have their own term pages.

These ideas also matter in this chapter even though they do not have their own term pages yet:

  • Style attribute versus imported style sheets
  • ID, class, and document-structure selectors
  • Foreground and background color, borders, and corners
  • Padding, margins, and the CSS box model
  • Dimensions, display, relative, absolute, and fixed position
  • Z-index, float, flex, and CSS Grid
  • Media queries and responsive Tailwind breakpoints

2.7 Tools

These are the official references for the styling stack you used in Lab 2 and when restyling Kambaz. Chrome DevTools is the fastest way to see which rule actually won.

  • CSS (MDN) — MDN's CSS reference — properties, selectors, and the box model you used to style Lab 2.
  • Tailwind CSS — The utility-class CSS framework you apply in className strings instead of writing every rule by hand.
  • React Icons — A React wrapper around popular icon sets so you can import icons as components.
  • Chrome DevTools — Chrome's built-in inspector for HTML, CSS, the console, and the Network panel.
  • Next.js — The React framework this course uses for pages, layouts, and later the HTTP server routes.
  • GitHub — The host for your remote Git repository and the place Vercel and Render connect when you deploy.
  • Vercel — The host for the Next.js client; connect the GitHub repo here to put the UI on the public Web.

2.8 AI Tools

Styling is the chapter where design helpers earn their keep. Use them to try color, spacing, and icon choices, then rebuild the look yourself in CSS or Tailwind so you still understand every class.

  • v0 — Generates interface mockups and React starting points from a written prompt.
  • Stitch — Google's design tool for turning a product idea into screen layouts you can iterate on.
  • Rocket.new — Builds a working web app from a short description so you can explore structure before coding by hand.
  • Lucide — A consistent open-source icon set you can search and drop into React screens.
  • Galileo AI — Turns a text description into high-fidelity UI designs for early product exploration.
  • Google Prompt Gallery — A public collection of Gemini prompt examples you can remix for writing, coding, and multimodal tasks.
  • shadcn/ui — Copy-and-own React components styled with Tailwind for composing a polished interface.
  • Figma — Collaborative design software for wireframes, mockups, and handing layouts to developers.