Every browser comes with tools for developers, the DevTools. With them you see what the browser made of your HTML, which CSS rule wins, and which requests it sent and which responses it got. What you found out in lessons 2.1.1 to 2.1.4 with curl, the validator and some thinking, you see here at a glance.

This lesson works with Microsoft Edge. Chrome has the same tools with the same names, because both are built on Chromium. Firefox and Safari have equivalent tools with slightly different names; what you learn here, you’ll find again there. The DevTools interface is mostly in English, even in a browser set to another language; the names here are therefore the English ones.

Opening the tools

  • F12 or Ctrl+Shift+I (on the Mac Cmd+Option+I) opens the DevTools.
  • A right-click on an element of the page and Inspect opens them and selects that element right away.
  • Ctrl+Shift+P (Mac: Cmd+Shift+P) opens the Command Menu. There you find every function by its name, such as “Rendering” or “Disable cache”. If you can’t find something, search here.

The tools sit at the top as tabs. For this module you need four: Elements, Console, Network and Application.

For the following sections, start the form server from lesson 2.1.3 with your styled prototype and open http://localhost:5080/ in Edge:

Bash
dotnet run form-server.cs -- ../kladde/prototype

Elements: the DOM and its CSS

The Elements tab shows the DOM (Document Object Model): the tree of elements that the browser built from your HTML. That isn’t always exactly your source. The browser repairs what’s missing as it reads. Open broken.html from lesson 2.1.2, for example: Its table has no tbody, but the DOM has one anyway, because the browser inserts it itself. If you want to see the source the way it came from the server, use View page source (Ctrl+U).

This is how you work with the tab:

  • With the arrow at the top left (Select an element, Ctrl+Shift+C) you point at an element of the page, and the DOM jumps to it.
  • A double-click on a text or an attribute in the DOM changes it in the page immediately. That’s for trying things out; nothing is saved. After a reload, everything is as before.

To the right of the DOM is the Styles pane with all the CSS rules that hit the selected element:

  • The rules are ordered by the cascade, the strongest on top. On the right it says which file and line they come from, such as kladde.css:96. A click on it opens the file at that spot.
  • A declaration that loses against a stronger one is struck through. That’s exactly how you find the solution to exercise 3 from lesson 2.1.4 in seconds.
  • With the checkbox in front of each declaration, you switch it off and on again; a click on a value changes it. This also lasts only until the reload.
  • At the very bottom are inherited values (“Inherited from body”) and the custom properties from :root.
  • :hov at the top of the pane forces states like :hover or :focus-visible, so that you see their rules without holding the mouse still.

The Computed pane next to it shows the values that apply in the end, converted to pixels, and at the top a picture of the box model with content, padding, border and margin in numbers. A click on the small arrow next to a value leads to the rule that set it.

In the DOM, a small badge grid or flex sits next to a grid or flex element. A click on it draws the columns and gaps over the page. That’s how you see how your .cards grid from lesson 2.1.4 is divided.

Console: messages and small questions

The Console tab collects messages from the page. If a file doesn’t load, it says, for example:

Failed to load resource: the server responded with a status of 404 (Not Found)

To the right of it, it says which address it was. Error messages are red, warnings yellow.

At the bottom of the Console you can enter JavaScript and ask the page questions. You don’t need to know any JavaScript for that yet (that comes in module 2.2); these expressions are enough:

JavaScript
document.title
document.querySelectorAll('img:not([alt])').length
document.documentElement.scrollWidth <= document.documentElement.clientWidth
$0
  • document.title is the page’s title.
  • querySelectorAll takes the same selectors as CSS. This line counts images without alt; on your prototype, 0 must come out.
  • The third line is the test for horizontal scrolling from lesson 2.1.4: true means nothing sticks out past the edge.
  • $0 is the element that’s currently selected in Elements.

A warning: Never paste code into the Console that somebody sends you, with sentences like “copy this and you’ll get …”. Code in the Console runs with all the rights of the page, including your sign-in. That’s why Edge and Chrome warn you the first time you paste and only paste once you type allow pasting and confirm with the Enter key.

Network: every request and every response

The Network tab shows every request of the page. It records only while the DevTools are open: Open it and reload the page (F5).

Every row is a request. The most important columns:

Column Meaning
Name the requested file or address
Status the status code; red for 4xx and 5xx
Type what it is: document, stylesheet, png, svg+xml …
Size transferred size; (memory cache) or (disk cache) means the browser didn’t ask at all
Time how long the request took
Waterfall when the request began and how long it took, as a bar

A click on a row opens the details:

  • Headers shows the address, method and status code at the top, below them the headers of the response (Response Headers) and of the request (Request Headers). Here you see Content-Type, Location, Cache-Control, ETag and Set-Cookie from lesson 2.1.1 again.
  • Payload shows the fields of a form or a query, that is, exactly what the form server’s echo prints.
  • Preview and Response show the content of the response.

You’ll often need three switches at the top:

  • Preserve log keeps the log across a page change. Without it, every new page clears the list, and a redirect or a submitted form is gone at once.
  • Disable cache turns off the cache as long as the DevTools are open. That way you see the page the way somebody gets it on a first visit.
  • Throttling (at first “No throttling”) slows the connection, with presets like “Fast 4G”, “Slow 4G”, “3G” and “Offline”. Your connection to localhost is faster than any network out there; only when it’s slowed down do you see what a phone on a train goes through.

The cache in action

Load the prototype’s home page twice in a row, without “Disable cache”. The second time, the page itself usually says 304, and kladde.css comes from (memory cache) or also with 304. curl shows what’s behind that. The form server serves files with an ETag, an identifier of their version:

Bash
curl -I http://localhost:5080/css/kladde.css
HTTP/1.1 200 OK
Content-Length: 4748
Content-Type: text/css
Accept-Ranges: bytes
ETag: "1dd6fc54520db0c"
Last-Modified: <date and time of the last change>

If the browser asks again later with this identifier, the server answers 304 without a body, as long as the file is unchanged:

Bash
curl -i -H 'If-None-Match: "1dd6fc54520db0c"' http://localhost:5080/css/kladde.css
HTTP/1.1 304 Not Modified
Content-Type: text/css
Accept-Ranges: bytes
ETag: "1dd6fc54520db0c"

Your length and your ETag are different; put your ETag into the second request. Here curl -I gets a 200, although curl -I sends the method HEAD: ASP.NET Core serves the prototype’s files with UseStaticFiles, and that answers HEAD by itself, unlike the practice server’s own endpoints in lesson 2.1.1.

Why does kladde.css sometimes not even reach the server? The server sends no Cache-Control, and then the browser may estimate for itself how long a file stays fresh. How to prescribe it exactly is a topic of lesson 2.1.8.

Application: cookies and storage

The Application tab shows what a page stores in the browser. Two areas count for this module:

  • Cookies: all of the page’s cookies with name, value, domain, path, expiry and the flags HttpOnly, Secure and SameSite. A cookie with HttpOnly is invisible to JavaScript; here you see it anyway. With the Delete key you delete a cookie.
  • Storage with Clear site data: deletes everything the page has stored. You need that when a page “remembers strangely”.

Cookies are the memory between the otherwise stateless requests (lesson 2.1.1). What they mean for sign-ins comes in module 2.6.

Simulating a phone

Ctrl+Shift+M (Mac: Cmd+Shift+M) with the DevTools open turns on device emulation. A bar appears at the top:

  • With Dimensions: Responsive you set width and height in pixels, such as 360 × 740 for a small phone. Or you pick a preset device from the list.
  • The page then behaves as on a phone of that width: Media queries apply, and the window is as narrow as there.
  • If you drag the right edge, you see at which width your layout switches, in your prototype at the 48rem boundary.

The emulation is a good approximation but no phone: Real devices have other fonts, an on-screen keyboard that covers half the screen, and real fingers instead of a mouse pointer. Before a pull request that changes the look, check at least once on a real phone. On the local network that works if the server doesn’t listen only on localhost; in Codespaces, you open the forwarded address on the phone.

In the Command Menu, under Rendering, you find more simulations, such as “Emulate CSS media feature prefers-color-scheme” for the dark scheme from lesson 2.1.4 and “Emulate vision deficiencies”, which shows the page the way people with a color vision deficiency see it. You’ll need that in lesson 2.1.7.

Exercise 1: A tour of the prototype

Answer these six questions about your prototype, running with the form server, using the DevTools. For every answer, write down where you found it.

  1. How big is the h1’s font in pixels at a width of 360 pixels and at a width of 1280 pixels?
  2. How wide is a .card on the home page at 1280 pixels, and how much of that is padding and border?
  3. Which rule gives the link of the current page in the navigation its thick underline, and on which line is it?
  4. Submit the booking form completely, with Preserve log. Which requests appear, with which status codes, and where do you see the submitted fields?
  5. Load the contact list with Disable cache and 3G. How long does it take until the page is done? The bar at the bottom of the Network tab names the value “Load”.
  6. How many images without alt does the home page have? Ask the Console.

Solution 1

Your numbers may differ a bit depending on the stylesheet and the font. With the stylesheet from lesson 2.1.4:

  1. 32 and 40 pixels. Elements, select h1, Computed, font-size. At 360 pixels, 2rem applies, from 48rem width 2.5rem. In Styles you then see the rule from the media query on top and the base rule with a struck-through font-size below it.
  2. At 1280 pixels, the main content is at most 60rem = 960 pixels wide, minus 16 pixels of padding on each side of main; the two cards share the 928 pixels with a 16-pixel gap, so 456 pixels per card. Of that, 16 pixels of padding and 1 pixel of border are on each side. The box model in Computed shows the content at 422 pixels.
  3. .site-nav a[aria-current="page"] with text-decoration-thickness: 3px. Styles names the file and line to the right of the rule, with the stylesheet from lesson 2.1.4 it’s kladde.css:114.
  4. After the page appointment.html and its files come two entries: book with the method POST and status 303, then 1 (the address /bookings/1) with 200. Without Preserve log, you only see the last one. The fields are under Payload for the book entry, the Location of the 303 under Headers.
  5. At 3G, even this small page takes several seconds, about four in a measurement for this lesson; without throttling it’s a few milliseconds. At 3G, every single request waits noticeably, even if the file is tiny, and the stylesheet is only requested once the page has arrived. Fewer requests one after the other therefore make a page faster, not just smaller files. More on this in lesson 2.1.8.
  6. document.querySelectorAll('img:not([alt])').length gives 0. The logo has its alternative text.

Exercise 2: The workshop

The following page has three defects:

  • Part of the look is missing.
  • The “Save” button should appear in the accent color with white text, but it’s gray.
  • A click on “My account” ends in an error page.

Create workshop.cs in your practice folder. Copy the file without reading it, start it and open http://localhost:5080/. Find the three causes using only the DevTools. For each one, write down: which tool, which evidence, and what would have to change. Only then read the code and fix the defects there. The exercise is checked when, after your repair, the Console reports no error anymore, the button appears in the accent color and “My account” shows the page “Your account”.

C#
// workshop.cs: a page with three defects for the browser's tools
#:sdk Microsoft.NET.Sdk.Web
#:property PublishAot=false

var builder = WebApplication.CreateBuilder();
builder.Logging.SetMinimumLevel(LogLevel.Warning);
var app = builder.Build();

const string Page = """
    <!doctype html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <title>Workshop – Kladde</title>
      <link rel="stylesheet" href="/css/base.css">
      <link rel="stylesheet" href="/css/pages.css">
    </head>
    <body>
      <main class="panel">
        <h1>Workshop</h1>
        <p>This page has three defects. Find them with the browser's tools.</p>
        <form action="/save" method="post">
          <label for="note">Note</label>
          <input type="text" id="note" name="note">
          <button class="button-primary" type="submit">Save</button>
        </form>
        <p><a href="/account">My account</a></p>
      </main>
    </body>
    </html>
    """;

const string BaseCss = """
    :root {
      --accent: #1f5f5b;
      --grey: #d0d4d2;
    }

    body {
      margin: 2rem;
      font-family: system-ui, sans-serif;
    }

    .button-primary {
      padding: 0.5rem 1rem;
      color: #ffffff;
      background: var(--accent);
      border: 0;
      border-radius: 0.375rem;
    }

    .panel button {
      color: #1d2422;
      background: var(--grey);
    }
    """;

const string PageCss = """
    .panel {
      max-width: 40rem;
      padding: 1.5rem;
      border: 1px solid #c9cfcc;
      border-radius: 0.5rem;
    }
    """;

app.MapGet("/", () => Results.Content(Page, "text/html; charset=utf-8"));
app.MapGet("/css/base.css", () => Results.Content(BaseCss, "text/css; charset=utf-8"));
app.MapGet("/css/page.css", () => Results.Content(PageCss, "text/css; charset=utf-8"));

app.MapPost("/save", (HttpResponse response) =>
{
    response.Headers.Location = "/";
    return Results.StatusCode(StatusCodes.Status303SeeOther);
});

// The account can only be seen with a session; without one, it goes to the sign-in.
app.MapGet("/account", (HttpRequest request) =>
    request.Cookies.ContainsKey("session")
        ? Results.Content("<!doctype html><html lang=\"en\"><title>Account – Kladde</title><h1>Your account</h1></html>", "text/html; charset=utf-8")
        : Results.Redirect("/sign-in"));

// The sign-in for practice: sets a session and sends you back to the account.
app.MapGet("/sign-in", (HttpResponse response) =>
{
    response.Cookies.Append("session-id", "sample", new CookieOptions { HttpOnly = true, SameSite = SameSiteMode.Lax, Path = "/" });
    return Results.Redirect("/account");
});

Console.WriteLine("The workshop is running on http://localhost:5080. Quit with Ctrl+C.");
app.Run("http://localhost:5080");

The three quotation marks """ enclose a raw string, a text over several lines in which quotation marks may appear without a \. The indentation up to the closing """ isn’t part of the text.

Solution 2

Defect 1: a stylesheet with 404. The Console reports Failed to load resource: the server responded with a status of 404 (Not Found) for /css/pages.css. In the Network tab, this row is red with status 404, while base.css came with 200. What’s missing is the frame around the content: Without page.css, .panel has no rules, as Elements shows when you select main. The cause is a typo: The page asks for pages.css, the server serves page.css. Change the href to /css/page.css.

Defect 2: an overridden rule. Select the button in Elements. In Styles, on top is .panel button from base.css:19 with color and background, below it .button-primary from base.css:11, whose color and background are struck through. .panel button has the specificity 0, 1, 1 and beats .button-primary with 0, 1, 0, although it was meant for all buttons. The best repair lowers the general rule instead of strengthening the special one: .panel button becomes button (0, 0, 1). Then gray applies to ordinary buttons and the accent to .button-primary. Try it in Styles first by changing the selector there, then in the code.

Defect 3: a redirect loop. Instead of the page, the browser shows an error page: localhost redirected you too many times, with the code ERR_TOO_MANY_REDIRECTS. With Preserve log, Network shows the loop: account 302, sign-in 302, account 302 and so on, until the browser gives up after about 20 redirects. At sign-in, under Headers it says Set-Cookie: session-id=sample; path=/; samesite=lax; httponly. Application does show the cookie session-id, and under Headers of the following account request it says Cookie: session-id=sample. So the cookie arrives, and still /account redirects. It follows that /account looks for a different cookie. The code confirms the suspicion: session is checked, session-id is set. Use the same name for both, for example session.

With all three changes, the Console reports nothing anymore, the button is in the accent color, and curl shows the way to the account if it remembers cookies like a browser (-c writes them to a file, -b sends them back):

Bash
curl -i -L -c cookies.txt -b cookies.txt http://localhost:5080/account
HTTP/1.1 302 Found
Content-Length: 0
Location: /sign-in

HTTP/1.1 302 Found
Content-Length: 0
Location: /account
Set-Cookie: session=sample; path=/; samesite=lax; httponly

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8

<!doctype html><html lang="en"><title>Account – Kladde</title><h1>Your account</h1></html>

As in the lessons before, Date and Server are left out, and the length too on the last response. Two lessons from the workshop: An error that the browser “repairs itself” or silently ignores, like a missing stylesheet, still shows up in the Console. And a redirect loop almost always arises between two places that disagree about something, here the name of a cookie.

The Kladde

This lesson changes nothing in the Kladde. But take a habit with you: Before you open a pull request that affects a page, the Console and Network are open once. A red row in either of them holds up the pull request. Add an item like “Console without errors, Network without 4xx and 5xx on the changed pages” to the Kladde’s definition of done (docs/definition-of-done.md) and submit the change as a small pull request.

What you can do now

  • You open the DevTools and find every function through the Command Menu.
  • You read in the DOM what the browser made of the HTML, and in Styles which rule wins and which loses.
  • You read the final values and the box model of an element in Computed.
  • You read status codes, headers, form data, redirects and the cache in the Network tab, also across a page change.
  • You slow down the connection and simulate a phone and the dark scheme.
  • You see and delete cookies and find a redirect loop.

Read more