Skip to content
Chapter 10Lesson 3

DevTools: the four panels you use daily

A tour of Chromium DevTools, the panels you reach for to inspect the DOM, network requests, console state, and storage on every web app you build.

The last two lessons mapped the request pipeline, using the Network panel in passing. DevTools is far bigger than that one tab, and you’ll work inside the rest of it for the entire course.

DevTools is how you read the result of every change you make. Edit a class and reload to see the new style, click a button and watch the request go out, suspect a stale cookie and read the cookie jar. This lesson tours the four panels a web engineer reaches for daily, each answering one question you’ll ask constantly:

  • Elements: what’s actually rendered right now?
  • Network: what did the server send back?
  • Console: what does the page think it knows?
  • Application: what’s in storage right now?

The goal is the habit of reaching for the right panel without thinking: you skim a bug report and know which one to open. The layout is here; the depth comes from using each panel on real work.

Open a well-styled site in a new tab now, with DevTools docked to the side or bottom; react.dev works well. We’ll switch between panels as the lesson goes.

The course teaches against Chromium DevTools, and any Chromium browser ships it. Firefox DevTools makes a good cross-browser sanity check: the layout differs but the concepts transfer. Safari DevTools is for the iOS-specific work the deployment unit covers later.

Before the React unit ships its first component, you need one extension: React DevTools. It adds two tabs to DevTools, Components and Profiler, that read the React tree directly rather than the DOM. You won’t use them in this lesson, so install it now and it’s ready when the React unit arrives.

  1. Open the Chrome Web Store listing for React Developer Tools and click Add to Chrome. The same listing works for any Chromium-based browser.

  2. Open a page that uses React, such as react.dev, with DevTools open. Two new tabs labelled Components and Profiler appear in the DevTools tab strip. That confirms the install.

  3. If you’re on Firefox, install the same extension from Firefox Add-ons. Same UI, same two tabs.

The Elements panel shows the live DOM . The source HTML the server originally sent is a different thing, which you read with View Source (Ctrl/Cmd+U), and on a React app the two routinely disagree. That disagreement is how React works.

The right pane shows the cascade for whatever element you’ve selected: every CSS rule that targeted it, listed in cascade order, with overridden rules struck through. How the cascade itself resolves, including specificity and why one rule wins, gets its own chapter later when Tailwind lands. Here you’re only learning to read it.

Elements
Console
Sources
Network
Application
Performance
<!DOCTYPE html>
<html lang = "en" >
<head> </head>
<body>
<header class = "site-nav" >…</ header>
<main>
<section class = "hero" >
<h1> Build for the web </h1>
<button class = "cta" > Get started </button>
</section>
</main>
</body>
</html>
Styles
Computed
Layout
Event Listeners
Filter :hov .cls +
element.style { }
.cta {
background-color : oklch(0.62 0.18 250);
color : #fff;
padding : 12px 20px;
}
button {
background-color : #e5e7eb;
border-radius : 6px;
}
1 DOM tree — the live in-memory tree, after every JS mutation.
2 Styles pane — rules in cascade order; overridden ones struck through.
3 Computed tab — the resolved final value per property.
4 :hov button — force pseudo-states without using the mouse.
Elements panel — live DOM on the left, the cascade on the right. Struck-through rules lost the cascade; the Computed tab resolves every rule down to one final value per property. The `:hov` button forces pseudo-states without the mouse.

Five moves carry the panel’s daily value:

  • Inspect to find. Cmd+Shift+C on macOS, Ctrl+Shift+C elsewhere, toggles the inspector cursor. Click any pixel on the page and the corresponding DOM node lights up in the tree. This is what you do when a user says “this thing isn’t styled right”: you start at the pixel, not at the source file.

  • Read the cascade in the Styles pane. Rules are listed top-down by which one won, with overridden rules struck through. The struck-through line tells you why your rule didn’t apply, usually specificity or order.

  • Edit styles live, with no reload. Click any rule’s value to edit it in place, and the page updates as you type. If it works in DevTools, it’ll work in the code: make the change here first, confirm it does what you want, then go write it.

  • Toggle pseudo-states explicitly. Click :hov in the Styles pane to force :hover, :focus-visible, :active, or :focus-within on the selected element. Hovering the element directly seems easier, but moving the mouse to the panel drops the hover state. Forcing the toggle holds the state still.

  • Computed tab for the final value. When the cascade gets dense and you stop caring why a value won, the Computed tab shows the single resolved value the browser used, plus which selector won it. Use it when the question is “what is the color, regardless of where it came from.”

Try it now. Open the page in front of you, inspect any button, and click :hov in the Styles pane to toggle :hover. The styles change without your mouse touching the button. Then edit the background-color value in the Styles pane directly: set it to red, or anything else. The button updates with no reload. That is the edit-and-check loop tightened from minutes to milliseconds.

The Network panel logs every request the page makes (Document, Fetch/XHR, JS, CSS, Images, Fonts, WebSockets) as a row with its timing, headers, payload, and response. The four network stages used the Protocol column and the Timing breakdown, a thin slice of this panel. The rest, the toolbar checkboxes, the throttle, and the request-detail tabs, is where you’ll spend real time debugging a web app.

Elements
Console
Sources
Network
Application
Performance
Throttling: Slow 4G
All Fetch/XHR Doc CSS JS Font Img Media WS Wasm Other
Name
Status
Method
Type
Size
Time
Waterfall
/
200
GET
document
14.2 kB
84 ms
app.css
200
GET
stylesheet
9.1 kB
42 ms
client.js
200
GET
script
78.4 kB
121 ms
/api/me
401
GET
fetch
63 ms
hero.webp
200
GET
image
26.8 kB
55 ms
inter.woff2
200
GET
font
32.0 kB
47 ms
Headers
Payload
Preview
Response
Initiator
Timing
Cookies
▾ General
Request URL: https://app.example.com/api/me
Request Method: GET
Status Code: 401 Unauthorized
▾ Response Headers
content-type: application/json
cache-control: no-store
www-authenticate: Bearer
1 Preserve log + Disable cache — keep the log across navigations, force every reload to hit the network.
2 Throttling — simulate "Fast 4G", "Slow 4G", "3G", or "Offline" to test loading states.
3 Request-type filters — drop the noise; Fetch/XHR for API calls, Doc for navigations.
4 Request-detail tabs — Headers, Payload, Preview, Response, Initiator, Timing, Cookies.
Network panel — every request, every header, every payload. The toolbar checkboxes and the right-pane tabs carry the daily workflow.

Seven moves carry the daily workflow:

  • Open the panel before the action that triggers the request. Network only captures while it’s open, so a request fired before you opened it is already gone. Open it first, then click.

  • Turn on Preserve log. By default a redirect or full-page navigation wipes the log; with Preserve log on, every request stays visible across navigations and reloads. Set it as a permanent default in your DevTools settings.

  • Tick Disable cache while DevTools is open. Otherwise a cached request returns in 1ms and you conclude your change worked when nothing went over the wire, while the next user on a cold cache still sees the old behavior. Keep the cache off whenever the panel is open.

  • Throttle to Slow 4G or 3G when testing loading states. When something looks fine on localhost, flip to Slow 4G, reload, and watch the skeleton states actually appear. A per-request override (right-click a row → Override request → Throttle) lets you slow one API call against an otherwise fast page.

  • Filter by type to drop the noise. Doc shows page navigations, Fetch/XHR shows your application’s API calls. Most debugging happens in Fetch/XHR, and the chip cuts the waterfall from hundreds of rows to the dozen that matter.

  • Read the right pane in order: Headers, Payload, Response, Timing. Headers gives you auth, content type, and status; Payload is what your client sent; Response is what the server sent back, with the Preview tab pretty-printing JSON; Timing shows where the time went, mapping onto the four stages covered in The four network stages. Any other order wastes clicks.

  • Right-click → Copy → Copy as fetch / Copy as cURL. This turns a captured request into a reproduction: paste the fetch into the Console, or the cURL into a terminal, and re-run the exact request the browser sent, ready to edit. The full fetch API gets its own chapter later in this unit.

Open Network on the page in front of you. Tick Preserve log and Disable cache, then reload. Find the document request row at the top, right-click it, and choose Copy → Copy as fetch. Paste it into the Console (covered properly in the next section). What you pasted is the same request the browser made, as JavaScript you can edit and re-run.

The Console is a REPL inside the running page. Anything in the global scope is reachable, anything a script logs with console.log lands here, and anything you type runs in the page’s own JavaScript context. Most people call console.log() and stop there, but the Console has a whole vocabulary beyond it, and learning that vocabulary pays off more than anything else on this tour.

Elements
Console
Sources
Network
Application
Performance
Filter
Default levels: All levels
Live expression
[auth] token expires in 42 seconds — refreshing auth.ts:118
TypeError: Cannot read properties of null (reading 'email') profile.tsx:24
console.table(users) VM:1
(index) id name email role
0 1 "Ada Lovelace" "ada@hooli.dev" "owner"
1 2 "Grace Hopper" "grace@hooli.dev" "admin"
2 3 "Linus Pauling" "linus@hooli.dev" "member"
 
$0.getBoundingClientRect() « DOMRect { x: 24, y: 80, width: 132, height: 40, ... }
1 Log-level filter — hide everything but errors when a page floods the output.
2 Prompt input — REPL inside the page; live-evaluation preview as you type.
3 console.table() output — array of objects rendered as a real columned table.
Console — a REPL inside the page, with log-level filtering, a `console.table` rendered as a real table, and the `$0` element reference at the prompt.

Seven moves to internalize:

  • Log levels and filtering. console.log is the default, while console.info, console.warn, and console.error print at their named levels. The dropdown at the top of the panel lets you hide everything but errors when a page floods the output. For debugging output you want to keep, use console.warn and console.error so the important lines stay filterable amid the noise.

  • console.table(arrayOfObjects). Renders a real table with one column per object key. Use it instead of console.log for any array of objects: it saves you from expanding [Object, Object, Object, ...] rows one at a time.

  • console.dir(domNode). Prints the full JavaScript property tree of a DOM node, including its properties, methods, and internals, rather than the rendered HTML you get from console.log(node). Reach for it when the question is “what JavaScript properties does this node carry?” rather than “what does it look like in the tree?”

  • console.trace(). Prints the stack trace at the call site. Use it when the question is “who called this?” and you don’t want to set a breakpoint to find out.

  • $0, $1, $2, $3, $4. References to the last five elements you selected in the Elements panel. Click a node in Elements, switch to Console, and type $0.getBoundingClientRect(). This bridge between the Elements and Console panels is easy to miss.

  • copy(value). Copies any value to your clipboard. copy($0.outerHTML) grabs the live rendered markup of the inspected element. copy(JSON.stringify(state, null, 2)) copies any complex value as pretty JSON, ready to paste into a bug report or test fixture.

  • Live evaluation as you type. Start typing document.queryS… and DevTools shows the result of the current expression in a faded preview before you hit Enter. This is handy for refining a selector against the live DOM before pasting it into your code.

$0 through $4, copy, and $_ for the last evaluated expression are console utilities . DevTools injects them into the Console’s global scope, so they don’t exist in your application code: call copy() in a script file and you get a ReferenceError. When something works in the Console but not in a .js file, the utilities are the usual reason.

The vocabulary, in five lines:

console.table(users); // an array of objects rendered as a real table
console.dir($0); // full JS property tree of the inspected element
console.trace(); // stack at this line
copy($0.outerHTML); // live rendered markup → clipboard
copy(JSON.stringify(state)); // snapshot any value as pretty JSON

Try the bridge right now. Switch to Elements and inspect any element on the page. Switch to Console and type $0; the element prints. Type console.dir($0) to see its full JavaScript surface unfold. Type copy($0.outerHTML) and paste anywhere; the live rendered markup is on your clipboard.

The Application panel is the storage and identity inspector. Every place the page persists data lives here, Cookies, Local Storage, Session Storage, IndexedDB, Cache Storage, and Service Workers, and every entry can be inspected, edited, or deleted in place.

For day-to-day web app work, two surfaces matter: Cookies, where the session cookie lives once auth is wired up, and Local/Session Storage, where client-state and URL-state tooling keep their working values. The rest you’ll open once or twice a project, so they’re named here just so you know where to find them.

Elements
Console
Sources
Network
Application
Performance
Application
Manifest
Service Workers
Storage
Storage
Local Storage
Session Storage
IndexedDB
Cookies
https://app.example.com
Cache Storage
Background services
Background Fetch
Push Messaging
Cookies for https://app.example.com
Clear site data
Name
Value
Domain
Path
Expires
Size
HttpOnly
Secure
SameSite
session
eyJhbGciOiJIUzI1NiIsInR5...
app.example.com
/
2026-06-04T09:12:43Z
312
Lax
csrf
a91b3fde4c2e1f...
app.example.com
/
Session
64
Strict
theme
dark
.example.com
/
2027-01-01T00:00:00Z
9
Lax
_analytics
GA1.2.778431...
.example.com
/
2026-11-28T00:00:00Z
34
None
4 cookies — double-click any cell to edit, ⌫ to delete the row
1 Sidebar sections — Manifest, Service Workers, Storage (with its sub-items), Background Services.
2 Clear site data — one button wipes every persistence surface for the current origin.
3 Cookies row — selected origin under Storage drives the right pane.
4 Attribute columnsHttpOnly, Secure, SameSite, Expires, Domain, Path.
Application panel — every storage surface and identity slot the page touches, all editable from here. The Cookies row's attribute columns are where most auth bugs live.

Five moves to know:

  • Cookies, the auth surface. Expand Cookies under Storage and click the origin you care about. The right pane lists every cookie with its full attribute set, including the auth-relevant HttpOnly, Secure, and SameSite. Edit a value in place, double-click any attribute to flip it, or delete a row with the Delete key. This is where you read the session cookie, and when auth fails, the cookie’s SameSite and Secure flags are worth checking before the server logs. A later chapter in this unit covers what those attributes mean and why they block a request.

  • Local Storage and Session Storage. The same edit-and-clear surface, keyed by origin. Local Storage persists across tabs and reloads; Session Storage is per-tab and clears when the tab closes. Values your client-state tooling stashes show up here.

  • IndexedDB. A persistent client-side database. Reach for it when offline state matters, but it’s off the daily path on this stack, since the platform’s data fetching covers most of what you’d otherwise use it for.

  • Service Workers and Cache Storage. Service workers register under their own sidebar entry, and whatever they cache shows in Cache Storage. This stack doesn’t ship one, since Server Components and the platform’s data fetching cover the use cases; they’re named here so you can find them.

  • Clear site data , the reset button. One button at the top of the Storage section wipes cookies, every kind of storage, cache, and service workers for the current origin. It’s the fix for “works in incognito but not here”: your local state has drifted from the server’s expectations, and clearing it is faster than diffing it. Reach for it freely.

Try the cookie inspector now. Open Application → Storage → Cookies on the page in front of you, click your origin, and read SameSite, Secure, and HttpOnly on any cookie. The wrong combination is why an authenticated request can quietly drop its cookie on a cross-site fetch; the Application panel is where you’ll diagnose that when it happens.

Each scenario maps to one panel, and some panels come up more than once.

For each production-shaped scenario, click the panel you would open first. Click an item on the left, then its match on the right. Press Check when done.

The API call returns 401, but the rest of the page renders fine
Network — check the response and the request headers for the missing or wrong auth
A class is in the DOM but the style isn’t applying
Elements — read the cascade in the Styles pane; the rule is struck through
A session cookie is set but the next request doesn’t send it
Application — inspect the cookie’s SameSite and Secure attributes
A redirect fires before you can read its response body
Network — turn on Preserve log, repeat the action
A user reports localStorage data they shouldn’t have
Application — inspect Local Storage; clearing it fixes the symptom
You want to copy the live rendered HTML of one node
Consolecopy($0.outerHTML) after inspecting in Elements
You want to know who called a function without setting a breakpoint
Consoleconsole.trace() at the call site
The user sees a stale response after you shipped a fix
Network — check cache headers and verify Disable cache is on while you test