Table of Contents
- Module 1: Course Overview
- Module 2: The Responsive Layout Process
- Module 3: Designers and Front-End Developers
- Module 4: Layout Principles and Patterns
- Module 5: Clean Code and Accessibility
- Module 6: Flexbox and Grid Revisited
- Flexbox and Grid: Shared Concepts
- Key Differences: One-Dimensional vs Two-Dimensional
- Understanding Align and Justify Properties
- Building the Holy Grail Layout with Flexbox
- Building the Holy Grail Layout with Grid
- Defining Grids in Depth
- Positioning Items on the Grid in Depth
- Hands-On Challenge: Pie Card Layout
- When to Use Flexbox vs Grid
- Module 7: Building a Responsive Layout
- Layout Strategy Overview
- Designing a Universal Grid System
- Base Markup and Section Structure
- Working with Media Queries
- Implementing the About Page Layout
- Implementing the Global Header and Footer Layout
- Implementing the Visit Us Page Layout
- Implementing the Home Page Layout
- Implementing the Pies Page Layout
- Module Summary: Responsive Layouts
- Module 8: Building Responsive Components
- Four Categories of Responsive Components
- Techniques for Responsive Images
- Container Queries
- Implementing the About Page Components
- Implementing the Global Header and Footer Components
- Implementing the Visit Us Page Components
- Implementing the Home Page Hero Section
- Implementing the Home Page Pies and Visit Us Sections
- Implementing the Pies Page Card Layout
- Implementing the Pies Page Styling Details
- Module Summary: Responsive Components
- Module 9: Summary and Next Steps
- Summary
Creating a layout is the foundational activity of building a website. A well-made layout displays properly across a wide variety of devices and forms the structure of the entire web development project — a project can succeed or fail based on how the layout is implemented. This course presents a framework for approaching layout: starting with the firmest possible foundation and ending with a complete, responsive website. Major topics include planning a web layout before writing any code, implementing that layout in a modular and responsive way, and implementing each component so that it responds gracefully to a wide variety of shapes and sizes.
Throughout the course, a running case study is used: Bethany’s Pie Shop, a small business website with four pages (Home, About, Pies, Visit Us) that is built from nothing to a complete, responsive site.
Module 1: Course Overview
Course Objectives
By the end of this material, you should have a dependable, repeatable framework for approaching any web layout project, along with the technical know-how to implement it. The major goals are to:
- Make a plan for a strong web layout before writing any code.
- Implement a web layout in a modular, responsive way.
- Implement each component on a page so that it responds correctly to a wide variety of viewport shapes and sizes.
Prerequisites
Familiarity with basic HTML and CSS is assumed. The course does not define fundamental terms for Flexbox or Grid — instead, it deepens and reinforces existing knowledge of them.
Module 2: The Responsive Layout Process
The Three-Step Process
When starting a new web page, a developer typically has a list of competing requirements to satisfy at once:
- The site must fulfill specific business or personal requirements (portfolio, client business, etc.).
- It needs to be responsive — usable from the smallest phone to the widest monitor.
- It needs to be accessible to everybody, including people who increase font size or use a screen reader.
- It needs to be maintainable, since changes are inevitable.
- It needs to look good and feel professional.
Layout is the key that makes this process approachable. Layout gives structure not only to the web page but to the developer’s thinking while building it. The recommended workflow breaks the process into three distinct, sequential steps:
flowchart LR
A[1. Make a Plan] --> B[2. Implement a Responsive Layout]
B --> C[3. Implement Responsive Components]
Treating these as separate steps avoids getting overwhelmed by a blank document — at every point there is a clear answer to “what do I need to do next?” As confidence grows, a developer can blur the lines between the steps, but working through them in isolation is a powerful learning and working strategy.
Step 1: Make a Plan
A common bad habit is trying to design in code: sitting down at a text editor with only a fuzzy idea of the end result. Uncertainty in the designer’s mind gets captured directly into fuzzy code — elements defined vaguely, pieces fit together randomly, with a lot of code being erased and rewritten.
Design is inherently an iterative process: create something, criticize it, restructure it, and rearrange it. The difference between designing with sketches/wireframes and designing directly in code is the cost of changing things. Discarding a rough collection of rectangles on paper is cheap. Discarding markup, layout styles, and appearance styles that have already been written is expensive.
Making a plan means having a validated idea of what to build before building it. This lets a developer focus purely on the “how” — which element to use, which styles achieve a given effect, how to generalize the CSS rules. Those questions are far easier to answer once what needs to be achieved is already clear.
A plan is also a way to validate ideas early and often. If a design is developed for weeks before showing a client, and the client dislikes it, it may be cheaper to start over than to accommodate every change request. Sharing a design early, gathering feedback, and iterating avoids this costly rewrite cycle.
Step 2: Implement Responsive Layouts
“Implementing a responsive layout” is distinct from implementing the final components that make up a page. Comparing a page at a wide viewport and a narrow viewport typically shows that most components look the same at both sizes — what changes dramatically is the layout: sections that were side-by-side stack vertically, sidebars move below main content, elements shrink from full width to a fraction of the width, and so on.
The recommended approach is to implement the entire site as a set of boxes that reflow as the window changes size, and only once the layout is fully solved to move on to the details of each component. This keeps attention focused on the CSS that’s actually responsible for layout, without being distracted by typography, colors, or other component-level details.
Step 3: Implement Responsive Components
Implementing responsive components means going component-by-component, making each one fit correctly within the layout already created. Several different strategies apply depending on the component:
- Some components look and behave identically from desktop to mobile (e.g., simple headers).
- Some need to wrap gracefully as space shrinks (e.g., paragraphs, tag lists) — often solved with Flexbox’s wrapping behavior.
- Some subtly shift their internal arrangement when there’s enough room (e.g., switching a one-column layout to two columns).
- Some need to be recreated almost entirely between breakpoints (e.g., an accordion on mobile vs. a full list of links on desktop).
Modern CSS also allows a component to query its own size (not just the viewport size) and make layout decisions accordingly. This technique — container queries — lets components switch designs once they cross a certain width, independent of the overall viewport width.
Case Study and Course Roadmap
The remainder of the course applies this three-step process to a single, complete project: a responsive website for Bethany’s Pie Shop, spanning four pages (Home, About, Pies, Visit Us). The roadmap is:
| Focus | What Happens |
|---|---|
| Planning in an enterprise context | How layouts move from idea to implementation inside a product team |
| Planning as a solo developer | The full design process: sketches → wireframes → hi-fi mockups |
| Clean code and accessibility | Naming conventions, semantic HTML, and accessibility fundamentals that inform all later decisions |
| Flexbox and Grid, revisited | A deep, fluent understanding of both layout systems |
| Building a responsive layout | Implementing the layout skeleton of all four pages, across every breakpoint |
| Building responsive components | Filling in every component so the site matches the approved mockups |
Module 3: Designers and Front-End Developers
Two Development Contexts
Web development looks very different depending on the organization. To ground the material in reality, two contrasting contexts are used throughout the course, both centered on the same fictional business, Bethany’s Pie Shop:
- An independent small business. Bethany owns and operates her own pie shop. She hires a freelance developer who is responsible for design, front end, back end, and hosting.
- A subsidiary of a large corporation. Bethany’s Pie Shop is one brand belonging to a larger company with dedicated product teams for UX research, UI design, front-end development, back-end development, and DevOps.
In both contexts, the same person writes the CSS and HTML, but expectations differ enormously: is the developer expected to both design and develop, or only to implement designs handed down with high fidelity? Who is responsible for gathering requirements and getting stakeholder approval? These organizational differences shape the entire workflow.
Working Inside a Product Team
In a large organization, many stakeholders are involved in approving how a page should look and behave: product managers, UX researchers, UI designers, art directors, executives, clients, and salespeople. Scaled agile product management structures this: a product manager owns overall outcomes for a product, portfolio managers oversee multiple products in a domain, and a product team (UX researcher, UI designer, front-end developer, and possibly back-end/DevOps roles) does the work.
By the time approved mockups reach a front-end developer, they already represent the consensus of every stakeholder. This is:
- Liberating — there’s little risk of major rejection or many change requests.
- Restrictive — deviating from the approved design either gets rejected outright or requires re-running the whole approval process.
The developer’s role in this context is to interpret the mockups and implement them using HTML, CSS, and JavaScript as faithfully as possible.
Clarifying Requirements Before Coding
Before writing code against a set of mockups, two truths must be kept in mind: mockups represent the result of significant stakeholder discussion and buy-in, and they may not have been created with a developer’s input in mind. Requirements should be clarified against three criteria:
| Criterion | Key Questions |
|---|---|
| Technical feasibility | Can this be implemented as designed? Does it require new data-fetching capability or a third-party integration not yet in place? |
| Responsiveness | How should each element behave between the provided breakpoints (e.g., a menu that looks fine at one width — should items shrink, wrap to two per row, or reflow differently)? |
| Corner cases | What happens with a long pie name that wraps to two lines? What is the null state when a filtered list has no matching items? |
Raising these questions as early as possible — ideally as soon as the mockups are seen — prevents costly rework and removes the need for the developer to guess at a designer’s intent mid-implementation.
Dissecting a Mockup into Semantic Elements
All web pages can be deconstructed into rectangles — this is why the box model is central to CSS: pages are just boxes arranged on a canvas. Dissecting a mockup means identifying the major boxes and choosing the correct semantic element for each, which matters both for code organization and for accessibility. It helps to look at mobile and desktop mockups side by side, since an element’s position or visibility can differ drastically between breakpoints.
A recommended process:
- Identify the site-wide header (present on every page) and footer (also site-wide).
- Identify the main section — the unique content that is the reason a visitor comes to this particular page. On more complex pages this may sit alongside
asideelements, additional navigation, or ads. - Identify nav elements — sets of links used to navigate the site or the current page. A footer that merely repeats the primary nav’s links does not itself need to be a
nav, since assistive technology can already reach the main nav quickly. - Identify heading levels. Think of headings as an outline: one
h1per page,h2for direct subsections,h3only for a section nested within anh2section. If the visual “title” of the page is actually an image (e.g., a logo), add a visually hiddenh1for accessibility. - Assemble the final document outline.
For Bethany’s Pie Shop, this process produces:
flowchart TD
Body --> Header[header]
Body --> Main[main]
Body --> Footer[footer]
Header --> Nav1["nav (primary links + logo)"]
Header --> H1["h1 (visually hidden): Bethany's Pie Shop"]
Main --> S1["section: About Bethany's Pie Shop (h2)"]
Main --> S2["section: Pies (h2)"]
Main --> S3["section: Visit Us (h2)"]
S3 --> H3a["h3: Address"]
S3 --> H3b["h3: Hours"]
Doing this analysis before writing markup means the hard structural questions are answered up front, so implementation can proceed without later restructuring.
Module 4: Layout Principles and Patterns
Designing as a Solo Developer
When working as an independent freelancer rather than inside a large product team, there is no one handing over pre-approved mockups — the developer must come up with the design as well. A client won’t pay for a site that doesn’t meet their needs, so time shouldn’t be spent implementing an idea that hasn’t been validated. Recommended practices:
- Validate ideas as early as possible. Don’t invest heavily in one direction until the client has responded to it.
- Target the largest points of uncertainty first, and resolve them early.
- Keep the client in the loop as designs get more detailed.
- Remember that criticism comes more naturally than creation — clients often struggle to describe what they want but are quite good at reacting to something concrete. Share work frequently.
- Accept that “good” means “pleasing the client.” The developer is the expert in web development; the client is the expert in their own business. Advocate for accessibility and performance, but let the client have final say on look and feel, and resist the urge to argue for a personally preferred approach.
Why You Should Avoid Designing in Code
Complete creative freedom comes with a downside: both the what (structure) and the how (implementation) must be figured out, instead of only the how. Designing directly in code — building up a page, evaluating it, then changing it — is discouraged for several reasons:
- It’s slower. A rectangle can be drawn on paper faster than it can be implemented on a web page. Low-fidelity tools (whiteboard, paper) are best for quickly generating and evaluating ideas.
- It excludes the client from the loop early, when their reaction to a rough idea is most valuable and least costly to act on.
- It biases design decisions toward ease of implementation rather than what’s actually best for the user — the “easy to build” option tends to win even when it’s the weaker design.
Even a developer wearing every hat (designer, front end, back end) benefits from proceeding through the roles as discrete steps: fully finish “UI designer mode” — with the what validated and approved — before switching into “front-end developer mode” to figure out the how.
Principles of Good Layout Design
A few durable principles guide good layout design, independent of trends:
- Have a solid hierarchy. Users scan pages rather than read them linearly. Define the major sections of a page clearly, and use a large headline for the page title with smaller headlines for subsections, so the structure is perceivable instantly.
- Use a grid. A grid — in the layout-design sense — usually means a set of equal-width columns (a 12-column grid is common because 12 divides evenly by 2, 3, 4, and 6). Columns have consistent gaps between them, and page sections span some number of columns, aligning to the grid lines. Aligning elements to a consistent grid makes a layout feel more professional even when the viewer can’t consciously identify why.
- Use white space. White space (which doesn’t have to be literally white) is the negative space left unfilled by content. It draws the eye through a layout and makes the page comfortable to read; without it, a layout feels cramped.
These qualities — hierarchy, grid, white space — are the foundation for the more subjective feelings clients often ask for (“make it pop,” “make it modern,” “make it friendly”). Until a strong intuition for design is developed, a proven, common layout pattern is a safe and effective starting point.
Common Layout Patterns
Good UI design deliberately avoids surprising the user — a familiar layout pattern feels comfortable immediately. A few widely used patterns:
| Pattern | Description | Best Suited For |
|---|---|---|
| Single-column layout | Content stacked in a single, linear flow, read top to bottom. | Mobile phones and any page where scan order should be unambiguous; often the correct choice even with room for more columns. |
| Fixed sidebar layout | A second column, usually for navigation (or supplemental “aside” content on the opposite side from the main reading flow). | Sites with many pages, where top navigation would get unwieldy. |
| Holy Grail layout | Header across the top, footer across the bottom, three columns in the middle (side nav, main content, sidebar). | Large content platforms and news sites needing both large nav and side content while preserving a large main content area. |
Two important nuances:
- “Columns” in this design sense are different from grid columns. A single-column layout (in terms of visual blocks) can still be built on top of a 12-column grid system used for alignment.
- More visual columns mean more competition for the user’s attention. Every additional column introduces uncertainty about where the user’s eye will land first — a risk that must be weighed against the benefits of showing more content simultaneously.
Case Study: From Sketch to Approved Mockup
A complete, real-world design process for Bethany’s Pie Shop:
- Requirement gathering. Conversation with the client narrowed the site down to four pages: Home, About the Shop, Pies (currently available), and Access/Visit Us.
- Design input. An existing logo (with a distinctive green color) was reused, and the green became a recurring brand color. The client wanted the site to feel “friendly and traditional” but had no strong opinion on fonts.
- Whiteboard sketch. Rather than debating more details up front, a whiteboard sketch was produced immediately, favoring “criticism over creation.” Starting with the mobile layout first made the process easier, since it’s simpler to scale a mobile layout up to desktop than to compress a desktop layout down to mobile. Each subpage got a small preview section on the home page, plus a button linking to the full page, along with a traditional navigation section and a footer listing all pages again.
- Low-fidelity wireframe. The whiteboard sketch was converted into a wireframe in a UI design tool — deliberately keeping it free of colors, fonts, and real images, so feedback focuses purely on layout. A 12-column grid was applied to the wireframe for consistent alignment.
- First round of feedback. The client requested more/larger pie pictures, a larger About-section image (even on mobile), removal of the Visit Us image on mobile, and reframing the About section to be about the shop rather than about the owner personally.
- Hi-fi mockup. Real images, fonts, and copy were added; the green from the logo was used throughout.
- Second round of feedback. The client felt there was “too much green.” Colors were adjusted to mostly white and light gray, letting the green pop only in buttons.
- Approval. The revised mockups were approved, clearing the way to begin implementation with confidence that large change requests were unlikely.
Module 5: Clean Code and Accessibility
Why Clean Code Matters
Any code will “work” as long as the resulting page meets the requirements — but code is also a communication medium. Anyone working on a large codebase, including your future self, must be able to read and understand code before safely changing it. Writing code is also building a system: individual style questions (“how do I center this?”) are easy to look up, but bigger questions — how to organize styles for easy editing, or how to name classes — only become intuitive after working through a number of messy, disorganized projects. The test to apply is always: does this make the code easier or harder to read and change?
Writing Clean HTML
HTML and CSS blur together in situations where either could solve the same problem. For example, a group of images could be nested in HTML to mirror their visual arrangement (one group containing a group, containing another group, and so on), letting simple CSS position each level — or the images could be laid out flat in HTML, with CSS doing all the positioning work.
Guideline: avoid overcomplicating markup just to make CSS easier to write. If styles will never change, a structure-heavy approach might be acceptable, but styles usually do change — and if the same elements must look different at different breakpoints (as in a responsive site), markup needs to serve every layout, which pushes toward CSS doing the heavy lifting rather than deeply nested markup.
Recommended workflow: write markup as simply as possible, try to achieve the desired style purely with CSS, and only add structure to the markup if a style genuinely isn’t achievable otherwise. This naturally encourages exploring more of what modern CSS can do and keeps markup lightweight.
The second major goal of clean HTML is embedding meaning (semantics) wherever possible:
- Use the right element. Prefer purpose-built semantic elements (
main,address,nav, etc.) over genericdiv/sectionwherever one exists for the concept being represented. - Name classes and IDs to convey meaning, not style.
menu-item,featured-post, andvegandescribe what something is;red-background,bold, andcolumn-2describe only how something looks and provide no real information when scanning a stylesheet.
Web Standards and Accessibility
Using the correct HTML elements has a major side benefit: built-in accessibility. Consider an assistive technology creator building a device usable without hands (for example, a sip-and-puff device, controlled by sipping and puffing into a straw-like input). It would be impractical to ask every website in the world to add custom support for one specific device. Instead, assistive technology is built against existing Web Standards (such as those published by the W3C) — well-known guidelines that all web developers already follow.
If a site is built according to these standards, any assistive technology — screen readers, sip-and-puff devices, tools for users with cognitive or other physical disabilities — can navigate it without any site-specific work. Fulfilling this contract as a developer comes down, again, to embedding meaning in markup: wrapping an article’s main content in a meaningless div conveys nothing to assistive technology; wrapping it in a section conveys that it’s a major section; adding a heading gives it a title; using main explicitly identifies it as the primary content of the page, unlocking behaviors across many different kinds of assistive technology at once.
Semantic HTML and ARIA
Semantic elements such as main behave visually identically to a div, so what actually confers the accessibility benefit? The answer is ARIA (Accessible Rich Internet Applications) — a set of roles and attributes that supplement HTML to convey meaning to assistive technology. A screen reader looking for the main content of a page doesn’t search for the literal main tag; it searches for the element carrying the ARIA role of main.
<!-- These two elements are functionally equivalent to assistive technology -->
<main>...</main>
<div role="main">...</div>
Roles can be applied explicitly to otherwise meaningless elements (div, span) via the role attribute — list, listitem, link, table, and many others exist — but semantic HTML elements already carry an implicit role, so this is rarely necessary:
| Semantic Element | Implicit ARIA Role |
|---|---|
main | main |
aside | complementary |
a (with href) | link |
table | table |
Guideline: prefer the semantic element with an implicit role over manually assigning an ARIA role to a meaningless element. ARIA roles remain useful for concepts that have no dedicated semantic element (menu, tablist, status, etc.), but this is largely outside the scope of layout work.
Writing Clean CSS
Clean CSS is primarily about readability and maintainability for the developer, since (unlike HTML) it carries no direct accessibility semantics of its own. Concrete techniques:
- Use sensible grouping. Grouping barely matters for a 20-line stylesheet, but matters enormously once a file reaches hundreds or thousands of lines. Start a file with major section headers already in place — a common starting set is
GLOBAL,LAYOUT,TYPOGRAPHY, andCOMPONENTS— and add more categories as needed. - Make modular classes. Two similar rulesets (e.g.,
button-activeandbutton-destructive) often do two jobs at once: define shared button styles, and define the specific variant colors. Splitting this into one generalbuttonclass plus one modifier class per variant avoids duplicating the shared rules, and both classes are applied together in the markup. - Use class names to inject meaning. A ruleset that targets
main > articleis technically valid but conveys little when skimming a stylesheet. Adding purposeful classes (pie-menu,menu-item) to both the HTML and the CSS selector makes intent obvious at a glance. - Use white space. Blank lines of varying size communicate separation between sections; space around a ruleset helps convey that it’s one cohesive group; consistent alignment keeps the whole file scannable. Since the stylesheet is an environment the developer works in for a long time, keeping it visually pleasant is worthwhile in its own right.
CSS Naming Conventions: BEM
Even a solo developer benefits from an internally consistent naming system; teams benefit even more from a well-known, battle-tested one. The most widely used is BEM — Block, Element, Modifier.
/* Block: a standalone entity referred to by one name */
.menu-item { }
/* Element: something that belongs within a block,
named with the parent block plus a double underscore */
.menu-item__name { }
.menu-item__image { }
/* Modifier: a variation of a block or element,
named with a double dash */
.menu-item--vegan { }
/* Block + Element + Modifier combined in a single class */
.menu-item__price--discounted { }
| BEM Part | Symbol | Meaning |
|---|---|---|
| Block | (plain class name) | A standalone, reusable entity, even if it contains multiple sub-elements. |
| Element | __ (double underscore) | A piece that belongs to, and is named after, its parent block. |
| Modifier | -- (double dash) | A variation of the normal appearance of a block or element. |
There’s no requirement to adopt BEM specifically, but using some consistent, well-documented convention is what allows a team (or a future version of yourself) to build confidently on top of an existing CSS system.
Module 6: Flexbox and Grid Revisited
Flexbox and Grid: Shared Concepts
Flexbox and Grid are built to behave more similarly than they might first appear:
- Both work with the concept of a container and one or more items inside it.
- Both are activated with the
displayproperty:display: flexordisplay: grid(orinline-flex/inline-gridfor an inline-level container). This automatically turns direct children into flex items or grid items. - Adjustments are made primarily on the container, affecting all items at once.
- Both support a
gapproperty controlling the minimum space between items (distinct from space surrounding the items). - Both default to stretching items along the cross axis (the axis perpendicular to the direction content flows) — overridable via
align-items. - Both share the same align/justify property system (covered in detail below).
.flex-container {
display: flex;
gap: 1em;
}
.grid-container {
display: grid;
gap: 1em;
}
Key Differences: One-Dimensional vs Two-Dimensional
The core distinction: Flexbox is a one-dimensional layout system; Grid is a two-dimensional layout system.
A default (un-configured) Grid container still looks fairly one-dimensional, because Grid ships with defaults that make it behave one-dimensionally until explicitly configured:
.grid-container {
display: grid;
/* grid-auto-flow defaults to "row": one column, as many rows as needed */
}
grid-auto-flow controls the direction new implicit tracks are created in once existing ones are full:
.grid-container {
display: grid;
grid-auto-flow: column; /* new columns are created instead of new rows */
}
Flexbox achieves a superficially similar wrapping effect with flex-wrap, but the underlying behavior is different: Flexbox lays out a single dimension and wraps overflow onto new lines, with each line’s item widths determined by content, not by a shared column grid. Grid, once explicitly configured, can enforce identical gridlines throughout the entire container — even across the two axes at once:
.grid-container {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
}
.flex-container {
display: flex;
flex-wrap: wrap;
}
This is the “killer feature” of Grid: gridlines can be respected consistently across an entire two-dimensional structure. Neither system is inherently “better” — they suit different mental models:
- Grid is Layout-In: define the structure first, and content is placed to fit it.
- Flexbox is Content-Out: the content itself dictates the layout that emerges.
Understanding Align and Justify Properties
The align/justify properties are some of the most confusing in CSS, but they follow one consistent, simple rule once understood. There are six related properties, four applied to the container and two applied to individual items (as an override/exception to the container-level rule):
| Property | Applies To | Axis | Controls |
|---|---|---|---|
justify-content | Container | Main axis | Spacing of the entire clump of items |
justify-items | Container (Grid only) | Main axis | Space allocated to each individual item |
align-content | Container | Cross axis | Spacing of the entire clump of items |
align-items | Container | Cross axis | Space allocated to each individual item |
justify-self | Item | Main axis | Override for one item |
align-self | Item | Cross axis | Override for one item |
The main axis is whatever direction content flows (left-to-right for Grid, or whatever flex-direction specifies for Flexbox). The cross axis is the axis perpendicular to it. A simple mnemonic used throughout the course: “MJ CA” — Main Justify, Cross Align.
.container {
justify-content: center; /* space-between | space-evenly | space-around | end */
align-items: center; /* stretch (default) | center | start | end */
}
.item {
align-self: stretch; /* overrides align-items for this one item */
justify-self: stretch; /* Grid only — overrides justify-items */
}
justify-content accepts several distribution keywords:
| Value | Effect |
|---|---|
start / end | Aligns the whole clump to one edge of the main axis |
center | Equal space on both sides |
space-between | Space divided evenly between items only |
space-evenly | Equal space between items and at the edges |
space-around | Half as much space at the edges as between items |
justify-items has no effect on Flexbox, because Flexbox items are never allocated extra space along the main axis — they are always sized to their content along that axis.
flowchart TD
Container["Container (Flexbox or Grid)"] --> Main["Main Axis → justify-content / justify-items"]
Container --> Cross["Cross Axis → align-content / align-items"]
Main --> ItemOverrideJ["justify-self overrides justify-items on one item"]
Cross --> ItemOverrideA["align-self overrides align-items on one item"]
Building the Holy Grail Layout with Flexbox
The classic “Holy Grail” layout — a header across the top, a footer across the bottom, and three columns in the middle (side nav, main content, sidebar) — is a good exercise for comparing the two systems.
The markup:
<div class="layout">
<header>Header</header>
<div class="horizontal-layout">
<nav>Side nav</nav>
<main>Main content</main>
<aside>Sidebar</aside>
</div>
<footer>Footer</footer>
</div>
Because Flexbox is one-dimensional, achieving this layout requires two nested flex containers: an outer vertical container (header, middle row, footer) and an inner horizontal container (nav, main, aside):
.layout {
display: flex;
flex-direction: column;
gap: 0.5em;
}
.horizontal-layout {
display: flex;
gap: 0.5em;
}
To make the three inner columns fill the full width of the container, the flex shorthand property is needed on the items themselves — it is a shorthand for flex-grow, flex-shrink, and flex-basis (default: 0 1 auto):
| Sub-property | Behavior |
|---|---|
flex-grow | Whether an item grows to absorb extra space along the main axis (0 = off, 1 = on; higher numbers absorb proportionally more space). |
flex-shrink | Whether an item shrinks below its flex-basis when the container is too small (1 = on by default, 0 = never shrink). |
flex-basis | The item’s starting/intrinsic size before growing or shrinking is applied (auto uses the content or explicit size). |
.horizontal-layout > * {
flex: 1 1 33%; /* grow: 1, shrink: 1, basis: 33% each => three even columns */
}
Setting flex-grow: 2 on one item gives it twice as much of the extra space as items with flex-grow: 1 (not necessarily twice the total size — existing content size is unaffected by the ratio).
Building the Holy Grail Layout with Grid
The same layout in Grid needs no nested containers and no markup changes at all — a single grid definition handles both the header/footer spanning and the three-column middle row:
.layout {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 0.5em;
}
.layout > header,
.layout > footer {
grid-column: 1 / 4; /* span from the first line to the last */
}
Because grid-template-rows isn’t specified, Grid automatically creates as many rows as needed, each sized to fit its content. This example demonstrates why Grid is so convenient for layout work: very little CSS is required, no markup restructuring is needed, and the result is exactly as flexible as the two-container Flexbox version — which becomes especially valuable once responsive breakpoints are introduced.
Defining Grids in Depth
Working with Grid happens in two distinct stages: first, define the grid; second, position items onto it.
flowchart TD
A["1. Define the Grid"] --> A1["grid-template-columns"]
A --> A2["grid-template-rows"]
A --> A3["grid-template-areas"]
B["2. Position Items onto the Grid"] --> B1["grid-column / grid-row"]
B --> B2["grid-area"]
B --> B3["Named lines & named areas"]
A --> B
Track sizes — the values plugged into grid-template-columns / grid-template-rows:
| Track Size | Behavior |
|---|---|
A static value (e.g., 100px) | Fixed size regardless of content. |
| A percentage | Relative to the grid container’s size. |
1fr (fractional unit) | A share of the remaining space after fixed tracks are subtracted. |
min-content | The smallest possible size the content can occupy (e.g., the widest unbreakable word). |
max-content | The size needed to display the content without any wrapping at all. |
auto (default) | Behaves like a mixture of max-content and 1fr — takes extra space if no other fr tracks compete for it, otherwise behaves like max-content. |
minmax(min, max) | Clamps a track between two values, e.g. minmax(max-content, 1fr). |
.grid {
display: grid;
grid-template-columns: min-content 1fr max-content;
}
.grid-flexible {
display: grid;
grid-template-columns: minmax(max-content, 1fr) minmax(max-content, 1fr) minmax(max-content, 1fr);
}
Writing an explicit 12-column grid one track at a time is tedious, so the repeat() function is used instead — and its repeat count can itself be auto-fill or auto-fit to create as many tracks as will fit in the available space:
.grid-12col {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 10px;
}
.responsive-cards {
display: grid;
grid-template-columns: repeat(auto-fill, 100px); /* fills as many 100px columns as fit */
}
The grid shorthand can combine grid-template-rows and grid-template-columns (and an auto-flow keyword) in one declaration, though many developers find it less readable than writing the rules separately:
.grid {
grid: 200px 200px / auto-flow 100px;
/* two 200px rows; new columns auto-created at 100px each */
}
Finally, a grid has both an explicit portion (the rows/columns actually declared) and an implicit portion (extra rows/columns Grid must create automatically if an item is placed outside the explicit bounds, or if there isn’t room for it). The size of implicitly created tracks is controlled with grid-auto-rows / grid-auto-columns:
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
grid-auto-rows: 100px; /* any extra, implicitly created rows will be 100px tall */
}
Positioning Items on the Grid in Depth
By default, Grid places each direct child, in source order, into the next available grid area. To position items manually, target the item, not the container, using grid line numbers:
.item {
grid-column-start: 1;
grid-column-end: 4;
grid-row-start: 1;
grid-row-end: 2;
}
/* Shorthand for start/end pairs */
.item {
grid-column: 1 / 4;
grid-row: 1 / 2;
}
/* Shorthand for all four lines at once: row-start / column-start / row-end / column-end */
.item {
grid-area: 1 / 1 / 2 / 4;
}
Negative line numbers count from the far (right/bottom) edge — useful when the number of columns isn’t fixed or predictable (e.g., with auto-fill):
.item {
grid-column: 1 / -1; /* span the full width of the grid, however many columns it has */
}
The span keyword defines an item’s size rather than requiring both a start and end line:
.header {
grid-column-end: span 2; /* spans two columns from wherever it starts */
}
Grid lines can also be named, and entire grid areas can be named and referenced directly — often the most readable approach for complex, responsive layouts:
/* Named lines */
.grid {
display: grid;
grid-template-columns: [left-edge] 1fr 1fr 1fr [right-edge];
}
.item {
grid-column: left-edge / right-edge;
}
/* Named areas */
.grid {
display: grid;
grid-template-areas:
"header header header"
"nav main aside"
"footer footer footer";
}
.header { grid-area: header; }
.nav { grid-area: nav; }
.main { grid-area: main; }
.aside { grid-area: aside; }
.footer { grid-area: footer; }
Repeating the same name across multiple cells in grid-template-areas merges them into one larger area (as with header and footer above, each spanning all three columns). Although naming areas requires more typing up front than positioning by line number, it pays off enormously once a layout must be redefined per breakpoint, since each breakpoint can simply redeclare grid-template-areas with a different arrangement of the same names.
Hands-On Challenge: Pie Card Layout
A practical exercise combining both systems: laying out a set of pie cards, each containing a name, image, description, and two prices, arranged in a responsive grid of cards.
Markup:
<main class="pies">
<div class="pie">
<h3 class="pie__name">Apple Pie</h3>
<img class="pie__image" src="img/apple-pie.png" alt="Apple pie" />
<p class="pie__description">A classic, flaky apple pie.</p>
<div class="pie__prices">
<div class="pie-price">
<span>$18</span>
<span>Whole</span>
</div>
<div class="pie-price">
<span>$4</span>
<span>Slice</span>
</div>
</div>
</div>
<!-- additional .pie elements repeat this structure -->
</main>
Solution, worked through step by step:
.pies {
display: grid;
/* auto-fit (not auto-fill) avoids leaving blank trailing columns */
grid-template-columns: repeat(auto-fit, 10em);
justify-content: space-between; /* main axis: spread the clump of cards */
gap: 1em;
}
.pie {
display: flex;
flex-direction: column;
gap: 0.25em;
}
.pie__description {
flex-grow: 1; /* pushes the prices down to the bottom of every card */
}
.pie__prices {
display: flex;
justify-content: space-between; /* the two price boxes are pushed to the edges */
}
.pie-price {
display: flex;
flex-direction: column;
align-items: center; /* cross axis: centers price + label horizontally */
}
Key decisions in this solution: repeat(auto-fit, 10em) on the Grid container both makes the number of columns responsive to viewport width and avoids empty trailing columns (the difference between auto-fill, which reserves space for potential empty tracks, and auto-fit, which collapses them); flex-grow: 1 on the description is what pins the price row to the bottom of cards with varying amounts of description text.
When to Use Flexbox vs Grid
flowchart TD
Start["Need to arrange elements?"] --> Q1{"Do you need to control<br/>BOTH rows and columns<br/>as a coordinated structure?"}
Q1 -->|Yes| Grid["Use CSS Grid<br/>(Layout-In system)"]
Q1 -->|No, one direction is enough| Q2{"Should the content itself<br/>determine how big the<br/>layout ends up being?"}
Q2 -->|Yes| Flexbox["Use Flexbox<br/>(Content-Out system)"]
Q2 -->|No, exact track sizes matter| Grid
| Aspect | Flexbox | Grid |
|---|---|---|
| Dimensionality | One-dimensional | Two-dimensional |
| Mental model | Content-out (content determines layout) | Layout-in (layout is defined, content fills it) |
| Nesting | Can nest flex containers inside flex containers for complex 1D layouts | Can nest a grid inside another grid’s item |
| Best for | Whole-page layout, controlling the overall responsive grid across breakpoints | Component-level arrangements, wrapping tag/button lists, centering content |
| Typical usage in a full site | Fewer, larger containers | More, smaller containers, used throughout |
Grid, as the “layout-in” tool, is recommended for laying out an entire page, especially because of how much easier it becomes to reflow the layout at different breakpoints. Flexbox containers, as the “content-out” tool, tend to be used far more often in aggregate across a site, since so many smaller layout problems are naturally one-dimensional. Neither is objectively superior — many effects are achievable with either system, and comfort with both is what allows the correct one to be chosen for a given problem instead of forcing a technique that doesn’t fit.
Module 7: Building a Responsive Layout
Layout Strategy Overview
This stage takes the approved mockups for Bethany’s Pie Shop and begins implementing them as real web pages — but only the layout step of the three-step process; component-level details (typography, colors, images) are deferred to the next stage. The goal here is to define every major section of every page and make sure it flows correctly at the narrowest screens, the widest screens, and everything in between.
Working this way (layout in isolation, then components in isolation) is a vertical slice through the world of CSS — every property involved in building a real page is exercised — deliberately split into two horizontal layers so that at any given moment only one concern needs attention. It also makes it easier to notice when two visually unrelated sections (e.g., a hero section and a page header) actually share the same underlying layout rules — a discovery that’s easy to miss when building each section fully, one at a time.
Designing a Universal Grid System
The ideal outcome is a single grid system reused across every page and every breakpoint, with page-specific and breakpoint-specific work limited to repositioning items on that shared grid.
Inspecting the mockups reveals two consistent patterns:
- Every section’s content aligns to the same left/right content boundary, with a margin outside it that grows as the viewport gets wider (so content never becomes absurdly large on an ultra-wide monitor).
- Each section’s background color/image needs to span the full viewport width, even though its content is constrained to that centered, capped-width column.
This leads to a three-layer structure repeated for every section on every page:
flowchart TD
Section["section.pies (full-width background)"] --> Container["div.max-width-container<br/>(flex, justify-content: center)"]
Container --> MaxWidth["div.max-width<br/>(max-width: 1024px)"]
MaxWidth --> Grid["div.section-grid<br/>(display: grid, 12 columns)"]
Grid --> Item1["Grid item"]
Grid --> Item2["Grid item"]
Analysis of the mockups (both mobile and desktop) confirms a conventional 12-column grid: individual mockup elements consistently measure as clean fractions of 12 (e.g., a large pie photo spanning one-third of the width lines up with exactly 4 of 12 columns). The same 12-column grid works for mobile too — elements simply span far more of the available columns.
Base Markup and Section Structure
The shared section structure is implemented once and reused for every section on every page:
/* GLOBAL */
* {
box-sizing: border-box;
}
/* LAYOUT */
.max-width {
max-width: 1024px;
}
.max-width-container {
display: flex;
justify-content: center;
}
.max-width-container .max-width {
width: 100%;
}
.section-grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 10px;
padding: 1em;
}
/* Sensible default: every grid item spans the full grid unless overridden */
.section-grid > * {
grid-column: 1 / -1;
}
<section class="pies">
<div class="max-width-container">
<div class="max-width">
<div class="section-grid">
<!-- grid items go here -->
</div>
</div>
</div>
</section>
The background for a given section (e.g., .pies { background: ... }) can now be set on the outer section element and will automatically fill the full viewport width, while its content stays capped and centered. This same base structure — section → .max-width-container → .max-width → .section-grid — is reused for the global header, the global footer, and every content section across all four pages, which is what keeps the CSS centralized and small.
A .border helper class (border: 1px solid black) is used throughout this stage purely to visualize element boundaries while building layouts, and is removed once each page’s components are implemented in the next module.
Working with Media Queries
Media queries apply rules conditionally based on properties of the device — in this course, exclusively the width of the viewport:
@media (width >= 600px) {
/* Applies only when the viewport is 600px or wider */
}
An equivalent, older syntax is also common and remains necessary for very old browsers such as Internet Explorer:
@media (min-width: 600px) {
/* Legacy syntax; behaves identically in modern browsers */
}
Only minimum-width breakpoints are used throughout this course. Additional, larger breakpoints are added as new media queries further down the stylesheet with a larger minimum width — since later rules in a stylesheet take precedence, this naturally layers rules correctly: a rule at 600px applies from 600px up to (but not including) the next breakpoint, at which point the next media query’s rules take over.
@media (width >= 600px) {
.example { grid-column: 1 / 7; }
}
@media (width >= 1000px) {
.example { grid-column: 1 / 4; } /* overrides the 600px rule at wider viewports */
}
Rather than grouping all mobile rules together, then all rules for the next breakpoint, and so on, this course keeps every breakpoint’s rules for one section located next to each other in the stylesheet — resulting in more, smaller media queries, but keeping related rules easy to find together. Browser responsive-design tools (e.g., a device toolbar/inspector toggled with a keyboard shortcut) are essential for testing every breakpoint while implementing these rules.
Implementing the About Page Layout
The About page mockup (ignoring the shared header/footer, covered next) has a page heading, a “Story” section, and a “Shop” section — each with a heading, an image, and a paragraph, mirrored left/right between the two sections on desktop.
<section class="page-heading">
<div class="max-width-container">
<div class="max-width">
<div class="section-grid">
<h1>Bethany's Pie Shop</h1>
</div>
</div>
</div>
</section>
<section class="about-story">
<div class="max-width-container">
<div class="max-width">
<div class="section-grid">
<h2>The Story</h2>
<div class="about-story__image"></div>
<p class="about-story__text">...</p>
</div>
</div>
</div>
</section>
<section class="about-shop">
<div class="max-width-container">
<div class="max-width">
<div class="section-grid">
<h2>The Shop</h2>
<div class="about-shop__image"></div>
<p class="about-shop__text">...</p>
</div>
</div>
</div>
</section>
Because the base rule spans every grid item across the full 12 columns by default, the mobile layout is complete with no additional CSS at all. The desktop layout only needs to reposition specific grid items:
@media (width >= 600px) {
.about-story__image {
grid-column: 7 / 13;
grid-row: 1 / 2; /* h2 already spans row 1 → 2, so the paragraph doesn't fall onto row 2 by default */
}
.about-story__text {
grid-column: 1 / 6;
grid-row: 2 / 3;
}
.about-shop__image {
grid-column: 1 / 7;
}
.about-shop__text {
grid-column: 8 / 13;
}
}
An important, generalizable lesson from this section: because Grid places un-positioned items in source order, an image element positioned after a heading (which itself spans row 1) will silently fall into row 2 unless the row is explicitly overridden — a common source of unexpected layout bugs when mixing explicit and implicit grid positioning.
Implementing the Global Header and Footer Layout
Footer. The footer only needs a single column, but the order of its two children (copyright text, links list) must reverse between mobile and desktop — a perfect use case for named grid areas:
<footer class="global-footer">
<div class="max-width-container">
<div class="max-width">
<div class="global-footer__grid">
<p class="global-footer__copyright">Copyright 2024, Bethany's Pie Shop</p>
<ul class="global-footer__links">
<li><a href="home.html">Home</a></li>
<li><a href="about.html">About</a></li>
<li><a href="pies.html">Pies</a></li>
<li><a href="visit-us.html">Visit Us</a></li>
</ul>
</div>
</div>
</div>
</footer>
.global-footer__grid {
display: grid;
gap: 2em;
grid-template-areas:
"copyright"
"links";
}
.global-footer__copyright { grid-area: copyright; }
.global-footer__links { grid-area: links; }
@media (width >= 600px) {
.global-footer__grid {
grid-template-areas: "links copyright";
}
}
Header. The header always shows a centered logo; it additionally shows a hamburger icon on mobile only, and a links list on desktop only. Centering the logo globally (not just within the remaining space) is most simply achieved with Grid — reserving an empty grid area on the opposite side of the hamburger button, using the . placeholder in grid-template-areas:
.global-nav {
display: grid;
grid-template-columns: 2em 1fr 2em;
grid-template-rows: 2em;
grid-template-areas: "hamburger logo .";
}
.global-nav__hamburger { grid-area: hamburger; }
.global-nav__logo {
grid-area: logo;
justify-self: center;
height: 3em;
width: 3em;
position: relative; /* lets the logo overhang the header bar */
}
.global-nav__links {
grid-area: links;
display: none; /* hidden on mobile */
}
@media (width >= 600px) {
.global-nav {
grid-template-columns: min-content 1fr;
grid-template-areas: "logo links";
}
.global-nav__hamburger { display: none; }
.global-nav__links { display: block; }
}
A subtlety worth calling out: .global-nav__links is deliberately a plain div wrapping a ul, rather than making the ul itself the flex container directly. If display: flex were set on the same element that also needs display: none on mobile, the more specific flex declaration (added later, for the desktop typography/layout pass) would override the none rule and re-show the links on mobile. Wrapping the ul in its own element keeps the layout-level display: none rule and the component-level display: flex rule on two different elements, so neither overrides the other unexpectedly.
Implementing the Visit Us Page Layout
The Visit Us page reorders four conceptual blocks (introductory text, an address block, an hours block, and two pairs of images at different sizes) very differently between mobile and desktop — different enough that treating them as four independent sections would make the mobile ordering nearly impossible to untangle. Instead, all elements live inside one section, with its grid handling the complex repositioning:
<section class="about-us">
<div class="max-width-container">
<div class="max-width">
<div class="section-grid">
<p>Visit Bethany's Pie Shop to taste what we have to offer.</p>
<section class="about-us__address">...</section>
<section class="about-us__hours">...</section>
<div class="about-us__image-small-1"></div>
<div class="about-us__image-small-2"></div>
<div class="about-us__image-large-1"></div>
<div class="about-us__image-large-2"></div>
</div>
</div>
</div>
</section>
/* Mobile: small images sit side by side */
.about-us__image-small-1,
.about-us__image-small-2 {
grid-column: span 6;
}
@media (width >= 600px) {
/* Large image + small image appear in the same row */
.about-us__image-large-1,
.about-us__image-small-1 {
grid-row: 2 / 3;
}
.about-us__image-large-1 { grid-column: 1 / 9; }
.about-us__image-small-1 { grid-column: 9 / 13; }
/* Address and hours side by side, inset one column from each edge */
.about-us__address {
grid-column: 2 / span 4;
}
.about-us__hours {
grid-column: span 4 / 12;
}
}
Since no explicit grid-template-rows is declared, un-configured rows default to auto — effectively max-content — so every element’s final height is simply whatever height its content naturally produces; the height is fine-tuned later once real component content is added.
Implementing the Home Page Layout
The home page combines a full-bleed hero section with three “stub” preview sections (About, Pies, Visit Us), each linking through to its own dedicated page.
<section class="site-hero">
<div class="max-width-container">
<div class="max-width">
<div class="section-grid">
<h1 class="visually-hidden">Bethany's Pie Shop</h1>
<p class="site-hero__slogan">"Homemade flavor in the heart of Vancouver"</p>
</div>
</div>
</div>
</section>
<section class="home-about">
<div class="max-width-container">
<div class="max-width">
<div class="section-grid">
<h2>About Bethany's Pie Shop</h2>
<img class="home-about__image" src="img/bethany.png" alt="Bethany working on pies at her counter" />
<p>Bethany has been bringing the taste of homemade pies to customers since 2007.</p>
<a class="cta-button" href="about.html">About Bethany's Pie Shop</a>
</div>
</div>
</div>
</section>
The trickiest part of this page is that, on desktop, the About preview must visually overlap the hero section, while on mobile it must appear as its own normal stacked section below the hero. The pragmatic solution — a deliberate compromise rather than an ideal one — duplicates the About stub markup: one copy lives inside .site-hero’s own grid (shown only on desktop), and another lives in its normal document position (shown only on mobile):
/* Inside the hero's own grid, position the duplicated About stub to overlap the slogan */
.site-hero .home-about {
grid-column: 8 / 13;
grid-row: 1 / 3;
}
.site-hero .site-hero__slogan {
grid-row: 1 / 2;
}
/* Show/hide the two copies per breakpoint */
.home-about { display: none; } /* the "normal" copy: hidden on mobile */
.site-hero .home-about { display: block; } /* desktop-only duplicate */
@media (width < 920px) {
main > .home-about { display: block; } /* normal copy shown on mobile */
.site-hero .home-about { display: none; } /* duplicate hidden on mobile */
}
The Pies preview section positions six thumbnail images using the span keyword against an imaginary three-row structure, and the Visit Us preview reuses the address/hours pattern already built for the Visit Us page:
/* Mobile: pie thumbnails */
.home-pies__image-1 { grid-row: span 3; grid-column: span 8; }
.home-pies__image-2 { grid-row: span 1; grid-column: span 4; }
.home-pies__image-3,
.home-pies__image-4,
.home-pies__image-5,
.home-pies__image-6 { grid-row: span 1; grid-column: span 2; }
@media (width >= 600px) {
/* Desktop: a different 2-column arrangement for the same six images */
.home-pies__image-1,
.home-pies__image-6 { grid-column: span 4; grid-row: span 2; }
.home-pies__image-2 { grid-column: 5 / 7; grid-row: 2 / 3; }
.home-pies__image-3 { grid-column: 7 / 9; grid-row: 2 / 3; }
.home-pies__image-4 { grid-column: 5 / 7; grid-row: 3 / 4; }
.home-pies__image-5 { grid-column: 7 / 9; grid-row: 3 / 4; }
}
Every “call to action” button across all three stub sections shares a cta-button class so a single, general rule can style all of them identically; each is positioned to grid-column: 4 / -4 (inset symmetrically from both edges) on mobile, and repositioned per-section on desktop.
Implementing the Pies Page Layout
The Pies page is a list of pie cards. Unlike prior pages — where content is allowed to determine each row’s height (auto) — pie cards are deliberately given a static height at this layout stage, since a defined card shape (wide rectangles on mobile, tall rectangles on desktop) is what this module cares about; laying out the card’s internal content to fit that shape is deferred to the next module.
<section class="pies-filter">
<div class="max-width-container">
<div class="max-width">
<div class="section-grid">
<div class="pies-filter__filter"></div>
</div>
</div>
</div>
</section>
<section class="pies-list">
<div class="max-width-container">
<div class="max-width">
<div class="section-grid">
<div class="pie"></div>
<div class="pie"></div>
<!-- repeated to simulate a long list -->
</div>
</div>
</div>
</section>
/* Mobile: implicit rows are 10em tall (wide rectangles, one per row) */
.pies-list .section-grid {
grid-auto-rows: 10em;
}
@media (width >= 450px) {
/* Two columns: pies span 6 of 12 columns */
.pie { grid-column: span 6; }
}
@media (width >= 650px) {
/* Three columns, taller cards for a portrait card shape */
.pies-list .section-grid {
grid-auto-rows: 25em;
}
.pie { grid-column: span 4; }
}
The exact breakpoint values (450px, 650px) and row heights were tuned empirically by dragging the browser’s responsive viewport tool until the cards’ proportions looked correct at each width — a normal and expected part of implementing a responsive layout.
Module Summary: Responsive Layouts
By the end of this stage, the entire site has:
- A robust, shared 12-column grid system used by every section on every page.
- Every stand-in element placed onto that grid.
- Media queries causing every section to reflow correctly across the full range of viewport widths.
Because the layout was defined “from the layout in,” and the layout styles are already centralized in their own section of the stylesheet, the component-level work in the next module can proceed without becoming entangled with layout code.
Module 8: Building Responsive Components
Four Categories of Responsive Components
With every page’s major sections already stubbed out and responsive to viewport width, the remaining work is to flesh out every box with real content, color, images, and typography. Because layout so far has focused entirely on width, adding real content introduces a side effect worth noticing explicitly: element heights will change to fit their content, while widths stay fixed by the grid. Every component’s height ends up being determined in one of four ways:
mindmap
root((How is a component's height determined?))
Already at full height
Fixed-size images, e.g. the header logo
Wraps with content and container width
Paragraphs of text
Filter/tag lists
Constrained by aspect ratio
Photographs placed into a width-only box
Redesigned entirely at a size threshold
Handled with container queries
| Category | Example | Handling Strategy |
|---|---|---|
| Already at full height | Header logo image | No special handling needed beyond fitting content into the fixed box. |
| Wraps as a consequence of width | Paragraphs, filter lists | Monitor at various widths; adjust font size if wrapping looks wrong. |
| Constrained by aspect ratio | Photographs | Responsive image techniques (below). |
| Must be redesigned at a given width | Address/hours cards, pie cards | Container queries (below). |
Techniques for Responsive Images
Two recurring image situations need dedicated CSS techniques.
Situation 1 — a box with a defined width but unbounded height (the image should fill the width and let height follow the aspect ratio):
img {
max-width: 100%;
display: block; /* removes the small gap left by the image's default inline display */
}
Situation 2 — a box with both a defined width and a defined height (the image must fill the box exactly without distorting):
.card img {
width: 100%;
height: 100%;
object-fit: cover; /* preserves aspect ratio while filling the box; crops overflow */
object-position: 0 32%; /* fine-tunes which part of the image is kept after cropping */
}
object-position accepts a single keyword (top, bottom, etc.) or two values (horizontal, vertical) for precise control over which part of the source image survives the crop — useful, for example, to keep the curved top edge of a pie visible after a square photo is cropped into a short, wide box.
A more advanced composite case — six images arranged into one combined visual block (one large image spanning 8 of 12 columns, one medium image spanning 4, and four small images spanning 2 columns each below them) — relies on the same width: 100%; height: 100%; object-fit: cover; combination applied to every image in the group, with the arithmetic of the column spans worked out ahead of time so the pieces reassemble into a single coherent rectangle.
Container Queries
Media queries respond to the width of the viewport. Container queries, a comparatively recent CSS feature (with cross-browser support only since February 2023), let an individual element respond to its own size instead — essential for components (like cards) that must be reused in places where the surrounding layout gives them very different amounts of space.
Using a container query requires two steps:
- Declare a container by name and type using the
containershorthand (container-name+container-type, or the combinedcontainer: name / typeshorthand).container-typeis eithersize(queries both width and height) orinline-size(queries width only) — this course only ever needsinline-size. - Write an
@containerrule referencing that name, with the same size-comparison syntax used for media queries.
.card {
container: card-name / inline-size;
}
@container card-name (width > 450px) {
.card__flex {
flex-direction: row;
}
}
Important limitation: an element cannot be restyled by a container query that it is itself the subject of — the container and the element being restyled must be different elements. The common workaround is to nest an extra wrapper element specifically for the styles that need to respond to the container’s size:
.card {
container: card / inline-size; /* this element defines the container */
}
.card__inner {
/* this element is what actually gets restyled in the @container rule */
}
@container card (width > 450px) {
.card__inner {
display: flex;
flex-direction: row;
}
}
Implementing the About Page Components
Typography. Global heading and paragraph styles are added to a dedicated TYPOGRAPHY section of the stylesheet:
h1, h2, h3 {
font-family: "bebas-neue", sans-serif;
font-size: 1.5rem;
margin-bottom: 0.75rem;
}
p {
font-family: "avory", serif;
font-size: 0.9rem;
line-height: 1.5;
margin-bottom: 1rem;
}
Content. Multi-paragraph copy that was originally marked up as a single <p> is converted to a <div> wrapping multiple <p> tags — a good example of markup evolving to fit real content without needing to touch any CSS selector, since the styling still targets the class name rather than the tag.
Images.
img {
max-width: 100%;
}
Section backgrounds.
.page-heading,
.about-shop {
background-color: #ebebeb;
}
Removing all the temporary .border helper classes on this page confirms the layout still holds once real content, images, and colors are in place.
Implementing the Global Header and Footer Components
.global-header {
background-color: #003531;
}
.global-footer {
background-color: #000;
}
.global-nav__logo {
height: 100%;
object-fit: cover; /* prevents the logo from stretching once height is forced to 100% */
}
Navigation links markup and styling:
<div class="global-nav__links">
<ul>
<li><a href="pies.html">Pies</a></li>
<li><a href="about.html">About</a></li>
<li><a href="visit-us.html">Visit Us</a></li>
</ul>
</div>
.global-nav__links ul {
display: flex;
justify-content: end;
gap: 1.5em;
}
.global-nav__links a {
color: white;
text-decoration: none;
font-family: "avory", sans-serif;
margin: auto 0; /* vertically centers the links within the header bar */
}
.global-nav__hamburger {
/* mobile-only icon */
}
As noted in the layout stage, the ul — not the .global-nav__links wrapper — is the flex container, specifically so the layout-level display: none rule on the wrapper is never overridden by this component-level display: flex rule.
Footer typography reuses the same link styling by broadening the selector rather than duplicating rules:
.global-footer__copyright {
color: white;
margin: 0 auto; /* centers horizontally using the margin trick instead of Flexbox */
}
li a {
/* shared by both header and footer link lists */
color: white;
text-decoration: none;
}
.global-footer__grid {
display: flex;
flex-direction: column;
align-items: center;
gap: 1em;
}
@media (width >= 600px) {
.global-footer__grid {
flex-direction: row;
justify-content: center;
gap: 4rem;
}
}
Implementing the Visit Us Page Components
The heading, centered intro paragraph (margin: 0 auto), and four photographs (styled with height: 100%; object-fit: cover;) are straightforward applications of patterns already introduced. The address/hours cards are the first component in the course to require container queries, since they must be reused at wildly different widths — from roughly 324px down to about 182px within the desktop grid, and up to 563px on mobile before shrinking again down to around 284px at the narrowest supported viewport.
<div class="about-us-card">
<div class="about-us-card__flex">
<h3>Address</h3>
<div>
<p>1205 5th Street</p>
<p>Vancouver, BC, Canada</p>
</div>
</div>
</div>
.about-us-card {
container: about-us-card / inline-size;
background-color: #ebebeb;
padding: 1em;
}
.about-us-card__flex {
display: flex;
}
.about-us-card__flex h3 {
flex-basis: 50%;
}
.about-us-card__flex p {
margin-bottom: 0;
}
@container about-us-card (width < 275px) {
.about-us-card__flex {
flex-direction: column;
align-items: center;
text-align: center;
}
}
The 275px threshold was determined empirically: the wide, side-by-side layout was tested by shrinking the container down until the “Hours” card began wrapping onto extra lines and looking visually cramped — the point at which it switches to the stacked, centered layout instead. Because this single container query is evaluated independently everywhere the card is reused, the net effect across the whole site is equivalent to getting several different media-query breakpoints “for free” from one rule.
Implementing the Home Page Hero Section
The hero section uses a background image plus a semi-transparent gradient overlay so the slogan text remains legible against a busy photograph:
.site-hero {
background-image:
linear-gradient(
to bottom,
rgba(0, 0, 0, 0.5) 0%,
rgba(0, 0, 0, 0.5) 33%,
transparent 33%,
transparent 100%
),
url("../img/hero-image.png");
background-size: cover;
}
The percentage marking where the gradient transitions from opaque to transparent is a function of the element’s height, which itself changes across breakpoints (constrained by the slogan and padding on mobile, but by the overlapping About section on desktop) — so this percentage has to be re-tuned at every breakpoint (33% on the base mobile breakpoint, 36% at 480px, 40% at 600px, and eventually 22% once the desktop layout is fully resolved).
h1.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.site-hero__slogan {
color: white;
justify-self: center;
font-size: 1.5rem;
}
@media (width >= 920px) {
.site-hero__slogan {
justify-self: left;
font-size: 2rem;
}
}
An h1 that exists purely for assistive technology (and is never meant to be visible) cannot simply use display: none — that would also remove it from the accessibility tree, defeating the entire purpose of adding it. A dedicated “visually hidden” pattern (as above) keeps the element in the accessibility tree while removing it from the visual layout. The breakpoint controlling when the About preview stub is allowed to sit beside the slogan (rather than covering it) was likewise adjusted empirically, moving from an initial 600px breakpoint up to 920px once it became clear the two elements needed more combined width before coexisting well.
Implementing the Home Page Pies and Visit Us Sections
.home-pies {
background-color: #ebebeb;
}
.home-pies img {
width: 100%;
height: 100%;
object-fit: cover;
}
@media (width >= 600px) {
.home-pies__text-1,
.home-pies__text-2 {
grid-column: span 6; /* side-by-side paragraphs on desktop */
}
}
The Visit Us preview stub reuses the exact address/hours markup already built for the standalone Visit Us page, with only typography differing between the two contexts:
.home-visit h3 {
font-family: "avory", sans-serif;
font-weight: 800;
font-size: 1.3rem;
}
.home-visit p {
margin-bottom: 0;
font-size: 1.2rem;
}
Implementing the Pies Page Card Layout
The pie cards are the main challenge of the Pies page, and — like the address/hours cards — are a strong candidate for container queries, since each card must adapt between a “tall” mobile shape and a “wide” desktop shape.
The card is broken into four conceptual areas (name, image, description, prices), deliberately implemented as plain divs while positioning is worked out (to avoid inheriting unrelated typography rules meant for real h3/p elements elsewhere on the page), then given real semantic elements once positioning is correct:
<div class="pie">
<div class="pie-grid">
<div class="pie-grid__name">Pie Name</div>
<div class="pie-grid__image">Image</div>
<div class="pie-grid__description">Description</div>
<div class="pie-grid__prices">Prices</div>
</div>
</div>
.pie {
container: pie-card / inline-size;
}
.pie-grid {
display: grid;
gap: 5px;
grid-template-areas:
"name"
"image"
"description"
"prices";
}
.pie-grid__name { grid-area: name; }
.pie-grid__image { grid-area: image; }
.pie-grid__description { grid-area: description; }
.pie-grid__prices { grid-area: prices; }
@container pie-card (width > 300px) {
.pie-grid {
grid-template-columns: 1fr 1fr;
grid-template-areas:
"image name"
"image description"
"prices description";
}
}
Reusing grid-template-areas for both the narrow (single-column, four-row) and wide (two-column, three-row) versions of the same card means every child element only ever needs one grid-area declaration — the meaning of each named area is what changes per breakpoint, not the markup or the item-level CSS.
Implementing the Pies Page Styling Details
With the grid skeleton in place, real elements replace the placeholder divs:
.pie-grid__name h3 {
margin-bottom: 0; /* overrides the global h3 margin-bottom */
}
.pie-grid__image img {
max-width: 150px; /* prevents an oversized source image from inflating the card */
}
.pie-grid__description p {
margin-bottom: 0;
}
The two prices are each a small “chip” containing a price and a label, and the two chips together form a flex container:
<div class="pie-grid__prices">
<div class="pie-price">
<span>$18</span>
<span>Whole</span>
</div>
<div class="pie-price">
<span>$4</span>
<span>Slice</span>
</div>
</div>
.pie-price {
display: flex;
flex-direction: column;
align-items: center;
background-color: #457779;
padding: 0.2em 0.4em;
}
.pie-grid__prices {
display: flex;
gap: 0.5em;
justify-content: center; /* narrow layout: chips shrink to content size, centered */
}
@container pie-card (width > 300px) {
.pie-price {
flex-grow: 1; /* wide layout: chips grow to fill the full column width */
}
}
.pie-price span:first-child {
color: white;
font-family: "avory", sans-serif;
font-size: 1.4rem;
}
.pie-price span:last-child {
color: white;
font-family: "bebas-neue", sans-serif;
}
Two final refinements resolve a row-height mismatch (prices appearing at different vertical levels once multiple cards sit next to each other) and padding differences between the two container-query states:
@container pie-card (width > 300px) {
.pie-grid {
grid-template-rows: auto auto 5em auto; /* reserves consistent height for the prices row */
}
.pie-grid__name,
.pie-grid__description {
padding: 0.5em;
}
}
@container pie-card (width <= 300px) {
.pie {
padding: 1em; /* narrow layout needs padding around the whole card */
}
}
Module Summary: Responsive Components
With every component implemented across all four pages, the Bethany’s Pie Shop site is complete: a fully responsive layout paired with components that adapt gracefully using media queries, container queries, and the responsive image techniques covered in this module.
Module 9: Summary and Next Steps
Course Recap
This course walked through the complete process of implementing a web layout in CSS:
- The responsive layout process — make a plan, implement a responsive layout, then implement responsive components — as a repeatable framework for approaching any web project, even if the boundaries blur together with experience.
- Planning in two different contexts — a large organization with defined product-team roles, and an independent developer working directly with a client — both converging on the same goal: high-fidelity, validated mockups (with content, design, and responsive breakpoints already figured out) before writing code, to avoid the wasted effort of designing in code.
- Clean code and accessibility — semantic HTML, ARIA, and consistent CSS naming (such as BEM) as habits to build in from day one rather than retrofit later.
- Flexbox and Grid, revisited — a fluent, functional understanding of both systems, including the align/justify property model and when to reach for each tool.
- Building Bethany’s Pie Shop — first the responsive layout, then the responsive components, as a complete vertical slice through real-world responsive web development.
Topics for Further Study
A few adjacent topics were intentionally left out of scope to keep this material focused, but are natural next steps:
- Responsive image loading — this course covered scaling images to fit their containers, but not serving appropriately-sized image files so a page’s actual network weight scales down for smaller viewports.
- Hamburger menu implementation — the icon was placed in the layout, but the interactive menu behavior itself requires additional markup, ARIA, and JavaScript beyond the scope of a layout-focused course.
- Making the pie filter menu functional — this requires client-side JavaScript (or a front-end framework) to filter a real data set of pies; only the static button markup and styling were covered here.
- Front-end frameworks (React, Angular, Vue, etc.) — becoming comfortable with at least one is a natural next step for any front-end developer beyond static layout work.
Summary
Building a reliable, responsive website comes down to treating layout as a first-class, sequential concern, not something to figure out incidentally while writing component code. The core principles from this material:
- Plan before coding. Validate ideas with cheap, low-fidelity tools (sketches, wireframes) before writing markup or CSS. It is always cheaper to discard a rough idea than a fully coded one.
- Separate layout from components. Get every section of every page reflowing correctly at every breakpoint first, using a single shared grid system; only then fill in typography, images, and colors.
- Prefer semantic HTML. The right element (
main,nav,header,footer, heading levels) gives accessibility “for free” via implicit ARIA roles, and gives CSS selectors real meaning. - Keep CSS clean and consistent. Sensible section grouping, modular classes, meaningful naming (BEM or an equivalent), and generous white space keep a growing stylesheet maintainable.
- Use Grid for layout, Flexbox for content-driven arrangement. Grid excels at defining a structure that content is placed into; Flexbox excels at letting content dictate the structure that emerges, and nests well for smaller, one-dimensional problems.
- Respond to more than just the viewport. Media queries handle page-level breakpoints; container queries let individual, reusable components (like cards) adapt to their own available space regardless of where they’re placed.
- Handle images deliberately. Choose between
max-width: 100%(aspect-ratio-preserving, unbounded height) andwidth/height: 100%withobject-fit: cover(fills a fixed box, cropping as needed) based on whether the containing box has a defined height.
Quick Reference: Key CSS Properties
| Property | Used On | Purpose |
|---|---|---|
display: flex / grid | Container | Activates Flexbox or Grid layout |
gap | Container (both) | Minimum space between items |
justify-content / justify-items | Container | Main-axis distribution (clump / per-item) |
align-content / align-items | Container | Cross-axis distribution (clump / per-item) |
justify-self / align-self | Item | Per-item override of the container’s rule |
flex (grow shrink basis) | Flex item | Controls growth/shrink behavior along the main axis |
grid-template-columns / -rows | Grid container | Defines explicit track sizes |
grid-template-areas | Grid container | Names grid areas for readable, per-breakpoint positioning |
grid-column / grid-row / grid-area | Grid item | Positions an item onto explicit or named grid lines/areas |
grid-auto-rows / -columns | Grid container | Sizes implicitly created tracks |
@media (width >= …) | Stylesheet | Applies rules based on viewport width |
container / @container | Element / stylesheet | Applies rules based on a specific element’s own width |
object-fit / object-position | img | Controls cropping/positioning of an image inside a fixed box |
Responsive Layout Checklist
- Requirements and mockups clarified for technical feasibility, responsiveness, and corner/null states before writing code.
- Mockup dissected into semantic elements (
header,nav,main,section, heading levels) before markup is written. - A single, shared grid system defined once and reused across every page.
- Every section’s layout implemented and verified at the narrowest and widest supported viewports, plus everything in between.
- Components categorized by how their height is determined (fixed / wraps / aspect-ratio / redesigned) and handled accordingly.
- Images given an explicit strategy (
max-width: 100%vs.object-fit: cover) rather than left unstyled. - Reusable components that appear in multiple contexts use container queries instead of (or alongside) page-level media queries.
- CSS organized into clear sections (Global, Layout, Typography, Components) with a consistent naming convention.
Search Terms
css · layouts · html · web · fundamentals · frontend · development · layout · page · responsive · components · grid · clean · flexbox · pies · designing · home · study · visit · accessibility · card · case · depth · footer