Getting Started with Chrome DevTools and the Elements Panel: DOM, Styles, Box Model and Inspecting Angular Components
When a style rule does not apply, the Elements panel shows the matching rules, their specificity and the computed value; in Angular the cause is often ViewEncapsulation locking styles with the _ngcontent attribute. The Command Menu, Device Mode, :hov, the box model, Flexbox and anchor positioning tools, ng.getComponent($0) and a CSS custom property instead of ::ng-deep, with real outputs from Chrome 156 and Angular 22.
Version note: The examples in this post were run with Chrome 156 (156.0.8078.80), Angular 22.2 and .NET 10. Console and style outputs were captured from the same Chrome version in headless mode through the Chrome DevTools Protocol (CDP); panel steps and shortcuts come from the Chrome DevTools documentation.
Predict First
You want prices on the Bookstore book list to be bold. The list component is BookList, and each row is drawn by BookCard. The price lives in BookCard's template, but you put the style on the list:
@Component({
selector: 'app-book-list',
imports: [BookCard],
template: `
<input class="filter" placeholder="Filter books" [value]="query()" (input)="query.set($any($event.target).value)" />
<p class="count">{{ visible().length }} books</p>
@for (book of visible(); track book.id) {
<app-book-card [book]="book" (addToCart)="cart.add($event)" />
}
`,
styles: `
.price { font-weight: 700; }
`,
})
export class BookList { /* ... */ }
You open the page and the prices are still normal weight. The CSS (Cascading Style Sheets) is loaded, the selector is spelled correctly, and the .price class is on the element. Why do you think the rule did not apply? And if you "fix" it by adding the rule by hand in the DevTools Styles pane and then reload the page, what happens? Make your guess before looking at the outputs further down; the Elements panel gives you the answer in two clicks.
This series looks at the Chrome DevTools panels through the eyes of an Angular developer. The first meeting with DevTools was in the JavaScript Basics series post "Your First JavaScript Script: The script Tag, defer, async and DevTools"; here we put the tool at the center of daily work. The examples run against a local Angular 22 app and its ASP.NET Core 10 API (Application Programming Interface).
Opening DevTools, Docking It and the Command Menu
There are three ways to open DevTools, and each one takes you somewhere different. F12 or Ctrl+Shift+I (Cmd+Option+I on a Mac) opens the panel you used last. Ctrl+Shift+J (Cmd+Option+J) goes straight to the Console. Ctrl+Shift+C (Cmd+Shift+C) turns on inspect mode: the element under the pointer is highlighted, and clicking selects it in the Elements panel. That last one is the shortcut you will use most.
You choose where DevTools sits with Dock side in the three-dot menu at the top right (Customize and control DevTools): right, bottom, left or a separate window. Ctrl+Shift+D (Cmd+Shift+D) toggles between the last two positions. When you are working on a responsive layout, a separate window is a good choice, because docked DevTools narrows the page's viewport and triggers media queries you did not mean to test.
The real accelerator is the Command Menu: Ctrl+Shift+P (Cmd+Shift+P). It is the same idea as the Visual Studio Code command palette. Type "dock" to change the layout, "screenshot" to capture a full-page screenshot, "disable JavaScript" to try the page without scripts, or "show rendering" to open the Rendering tab. Delete the leading > and the same box switches to opening files; : goes to a line, @ to a symbol, and ? lists the other modes. Once you get used to this box instead of clicking between panels, the time you spend in DevTools drops noticeably.
Esc toggles the Drawer: whatever panel you are in, you can keep secondary tools such as Console, Rendering, Network conditions and Sensors open in the bottom half. Inspecting styles in Elements with the Console open below is the layout this series uses most.
Device Mode: Emulating a Mobile View
Ctrl+Shift+M (Cmd+Shift+M) opens Device Mode. You pick a preset device from the toolbar, or drag the width in Responsive mode. In Chrome 153–154 the device list was grouped by form factor: Mobile, Foldables, Tablets & Desktops and Smart Displays. Old definitions were removed and current presets such as iPhone 16, Pixel 9, Pixel 10, Galaxy Z Fold 6 and iPad Pro 13" were added. In Chrome 155–156 each emulated device keeps its own zoom preferences, and the Save-Data override survives toggling the device toolbar.
It matters to know what Device Mode is not: it imitates viewport size, device pixel ratio, user agent and touch events, but not the phone's CPU (Central Processing Unit), GPU (Graphics Processing Unit) or real network. A list that scrolls smoothly on a desktop CPU can stutter on a mid-range phone. For performance questions, add CPU throttling (in the Performance panel, post 06 of this series), and make the final call on a real device with remote debugging over USB (Universal Serial Bus).
The Elements Panel: Reading and Editing the Live DOM
First, a distinction: View Source shows the HTML (HyperText Markup Language) the server sent, while the Elements panel shows the browser's current DOM (Document Object Model). In an Angular app the difference is huge. The body of the index.html the server sends is just this:
<body>
<app-root></app-root>
<noscript>Please enable JavaScript to continue using this application.</noscript>
<script src="main.js" type="module"></script></body>
After Angular renders, the first app-book-card element looks like this in the live DOM (from CDP's DOM.getOuterHTML, split into lines by us):
<app-book-card _ngcontent-ng-c1407352934="" _nghost-ng-c1135625248="">
<article _ngcontent-ng-c1135625248="" class="card">
<a _ngcontent-ng-c1135625248="" class="title" href="/books/1">Compilers Volume 1</a>
<span _ngcontent-ng-c1135625248="" class="author">Alan Turing</span>
<span _ngcontent-ng-c1135625248="" class="price">$21.00</span>
<button _ngcontent-ng-c1135625248="" type="button">Add to cart</button>
</article>
</app-book-card>
With a node selected, double-click to edit an attribute or text, F2 (Fn+F2 on a Mac) opens Edit as HTML, H hides the element (it adds visibility: hidden), Delete removes it, and Ctrl+Z undoes. You can drag nodes to move them; since Chrome 153–154 that also works across shadow DOM boundaries. Ctrl+F searches the panel and accepts not only text but CSS selectors and XPath: type app-book-card:nth-child(5) .price and you land on the fifth card's price. Clicking an arrow while holding Ctrl+Alt (Option on a Mac) expands the whole subtree.
Remember two things from a node's context menu: Copy > Copy selector gives you a ready-made CSS selector for a test or the console, and if you need a JavaScript (JS) expression, Copy JS path sits next to it; Break on > subtree modifications pauses JavaScript when that subtree changes. We will use the latter in the Sources post for the question "which code is changing this element?".
Styles: The Cascade, Specificity and Why a Rule Did Not Apply
The Styles pane lists the rules matching the selected element in cascade order: the inline style on top, then winners by specificity and source order, user agent styles at the bottom, and inherited ones under "Inherited from" headings. A struck-through property has been overridden by another rule. Hovering over a selector shows its specificity. When we select the price span from Predict First and ask CDP for the same information (CSS.getMatchedStylesForNode), this is the only rule that comes back:
regular .price[_ngcontent-ng-c1135625248] [0,2,0] MATCH { color: #0a7d4f; }
The only .price rule matching the element is BookCard's. Specificity (0,2,0): one class and one attribute selector. So where is BookList's rule? The <style> elements on the page answer that:
.price[_ngcontent-ng-c1407352934] {
font-weight: 700;
}
/*# sourceMappingURL=/book-list.css.map */
.price[_ngcontent-ng-c1135625248] {
color: #0a7d4f;
}
Angular's default ViewEncapsulation.Emulated mode locks each component's styles to the elements in that component's template. The lock is the [_ngcontent-ng-c...] attribute added to the selector at compile time. BookList's .price rule only matches elements carrying _ngcontent-ng-c1407352934; the price span comes from BookCard's template, so it carries _ngcontent-ng-c1135625248. The rule is not wrong, it is looking at another component's elements. The Computed tab says the same: font-weight: 400, color: rgb(10, 125, 79).
The Computed tab answers "what is the final value and where did it come from". Expand a property and you see which rule in which file and line produced the value; with "Show all" off, only properties that were actually set are listed. In Styles, the info icon next to a property is the inactive CSS hint: the rule matches but has no effect. The classic example is width on an inline element. Our card has .author { width: 160px; }, and .author is a span; yet the box is 160 pixels wide and the computed display is block. The reason: .card is a flex container, and flex items are blockified. DevTools showing no hint is correct here; when you do see the hint, look at the parent's layout first. Chrome 153–154 added a setting that keeps rules which used to match, but no longer do, visible with an inactive marker; 155–156 extended the inactive display to @starting-style rules and inherited rules.
:hov, .cls and the Box Model for State and Size
When you want to inspect a hover style, the style disappears the moment you move the pointer off the button. The :hov button in the Styles toolbar forces states such as :hover, :focus, :focus-visible and :active. Doing the same with CDP's CSS.forcePseudoState changed the button's background like this:
button background before :hover force: rgb(239, 239, 239) after: rgb(56, 189, 248)
The orange dot next to the element shows that a state is forced; turn it off when you are done, or half an hour later you will be debugging yourself over "why is this button always blue". The .cls button next to it adds and removes classes; since Chrome 155–156 its suggestions skip classes the element already has.
The box model diagram at the top of the Computed tab shows content, padding, border and margin layer by layer, and you can double-click values to edit them. Measuring the card with CDP's DOM.getBoxModel, the content width was 1244 pixels and 1264 with padding: padding: 6px 10px adds 10 pixels on each side. In Styles, select a numeric value and use the arrow keys to change it by 1, with Alt (Option on a Mac) by 0.1, with Shift by 10; when a designer says "give it a bit more room", this is how you tune the value by eye and then write it into the code.
Flexbox, Grid and Anchor Positioning Tools
In the Elements tree, elements with display: flex or grid get flex and grid badges. Clicking a badge draws an overlay on the page: tracks, gaps and line numbers become visible. The icon next to the display: flex line in Styles opens the Flexbox editor, where you try flex-direction, flex-wrap, align-items and justify-content values with visual buttons. Chrome 155–156 added the wrap-reverse option to this editor.
The newer CSS anchor positioning got tools as well. In Chrome 155–156, clicking the small swatch next to a position-area declaration in Styles opens a visual grid for choosing regions. With an anchor-positioned element selected, the viewport overlay draws the margin, padding, border and content boxes of the inset-modified containing block (IMCB). Right-clicking a popover badge in the DOM tree lists the element's implicit anchors and lets you jump to one. If you are moving a dropdown or tooltip in Angular to anchor positioning, the answer to "why does it open in the wrong corner" is now in this overlay.
Inspecting Angular Components: _nghost, _ngcontent and ng.getComponent
Read the DOM output above again. app-book-card carries two attributes: _nghost-ng-c1135625248 says it is the host of its own component, and _ngcontent-ng-c1407352934 says it sits in BookList's template. Every element inside carries BookCard's _ngcontent value. So in the Elements panel you can read from these attributes which component's template an element came from.
The faster route is the Console. The node you select in Elements is available in the console as $0. In a development build, Angular exposes debugging functions under window.ng:
$0.tagName
// "APP-BOOK-CARD"
ng.getComponent($0).constructor.name
// "_BookCard"
ng.getComponent($0).book()
// {"id":1,"title":"Compilers Volume 1","author":"Alan Turing","price":21,"stock":1}
ng.getOwningComponent($0).constructor.name
// "_BookList"
getComponent returns the component instance of a host element, and getOwningComponent returns the component whose template contains the element. book is a signal input, so we read its value by calling it like a function. The leading underscore in the class name comes from the development build's compiled output and means nothing in your code. The full ng global is the topic of the next post.
Now back to the price problem. The first fix that comes to mind is ::ng-deep:
styles: `
:host ::ng-deep .price { font-weight: 700; }
`,
The compiled rule and its measured effect:
[_nghost-ng-c1407352934] .price {
font-weight: 700;
}
app-book-list attributes: _nghost-ng-c1407352934
BookCard .price font-weight: 700 | any other .price under BookList: 700
The price is bold now, but the rule matches every .price under BookList: an unrelated span.price we added to the list as a test also became 700. Without :host, the rule leaks into the whole application. The Angular documentation keeps ::ng-deep only for backward compatibility and discourages its use. A cleaner way is for the child component to open up its own style through a CSS custom property:
/* BookCard: the knob it exposes, default 400 */
.price { color: #0a7d4f; font-weight: var(--book-price-weight, 400); }
/* BookList: only supplies the value */
app-book-card { --book-price-weight: 700; }
font-weight: 700, --book-price-weight on host: "700"
Custom properties cross the encapsulation boundary through inheritance, but BookCard decides which values can be set from outside. The Elements panel shows this clearly too: hovering over var(--book-price-weight, 400) in Styles shows the resolved value and the rule that defines it.
Production Notes
There is no ng global in a production build. When we served the ng build output of the same app from ASP.NET Core static files and checked the console, typeof ng was "undefined". If you need to inspect a component in a deployed environment, either produce a separate debugging build with optimization: false or work with source maps in the Sources panel (post 03 of this series).
The _ngcontent-ng-c... values are generated by Angular. In our test they were identical in the development and production builds, but they can change when the component's content changes. Do not use these attributes in global CSS or in E2E (end-to-end) test selectors; put your own attribute such as data-testid on the element for tests.
Every style and DOM change you make in DevTools lives only in that tab's memory. It disappears when you reload, when Angular's dev server hot-reloads, or when Angular re-creates an @if block. Move the value you found into the code right away; we will set up Workspaces in the Sources post so DevTools can write to your source files.
Extensions can add their own styles and DOM elements to the page. When something is "broken for me, fine for my teammate", the first experiment is opening the page in a profile without extensions or in an Incognito window.
Common Pitfalls
- Fixing in DevTools and reloading: this is the answer to the second question in Predict First; the rule you added in Styles is gone after the reload, and nothing changed in the source.
- Reading View Source and thinking you are reading the DOM: in an Angular app, View Source only shows an empty
<app-root>. - Trying to solve an encapsulation problem with
!important: if the rule does not match the element at all, raising its priority changes nothing; check whether the rule appears in Styles. - Writing
::ng-deepwithout:host: the rule becomes global and breaks other pages. - Leaving a forced
:hovstate on and misreading the behavior. - Taking smoothness in Device Mode for real device performance.
A Small Experiment
★☆☆ On the Bookstore list, turn on inspect mode (Ctrl+Shift+C) and click a price. Find the rule in Styles, click the color value and change it with the color picker, then reload and watch the change disappear. For the same element, expand color in the Computed tab and read which file the value came from.
★★☆ Add .title:hover { text-decoration: underline; } to BookCard. Force hover with :hov, then confirm in the Console with ng.getOwningComponent($0) which component the title belongs to. Notice that $0 changes when you select another node in Elements.
★★★ Solve the price weight without ::ng-deep, using the --book-price-weight custom property, and set the same card's weight to a different value somewhere other than BookList, for example on the cart page. On both pages, hover over the var() value in Styles and check which rule it resolves from.
You noticed that everything you see in Elements can also be queried from the console: $0 and ng.getComponent are the tip of the iceberg. In the next post we open up the Console panel in depth: from console.table to Live Expressions, from getEventListeners to the whole of Angular's ng globals.