AlgoInsight

Free lesson · Frontend System Design

You've used the BOM a thousand times. You probably can't name it.

Medium10 min readfrontendsystem-design

Every time you've read window.location.href, redirected with window.location.assign, checked navigator.onLine, pushed a route with history.pushState, or read window.innerWidth to handle a resize - you were using the BOM. The Browser Object Model. And yet if an interviewer asks "what's the difference between the DOM and the BOM," a startling number of otherwise-strong engineers go blank, because they learned all these APIs individually without ever being told they form a category with a name.

This lesson fixes that, and it's worth fixing for two reasons. First, it's a clean, common interview question with a crisp answer, so it's free points. Second - and more importantly - understanding the DOM/BOM split clarifies where the browser ends and your document begins, which is the exact mental boundary you need for the next lesson on single-page apps, where client-side routing is really just "using the BOM to fake navigation without touching the server." So let's draw the map.

The window is the whole world

Start at the top. In a browser, the global object is window. Everything lives under it. When you write document, you're really writing window.document. When you write setTimeout, it's window.setTimeout. The window object is the global scope, and it contains two broad families of things:

graph TD
    Window[window<br/>the global object] --> Document[document<br/>= the DOM]
    Window --> Location[location]
    Window --> Navigator[navigator]
    Window --> History[history]
    Window --> Screen[screen]
    Window --> More[innerWidth, innerHeight,<br/>setTimeout, localStorage...]
    Document --> DOMNote[The page content:<br/>elements, text, the tree]
    Location --> BOMNote[The browser environment<br/>around the document = BOM]
    Navigator --> BOMNote
    History --> BOMNote
    Screen --> BOMNote
This diagram shows the window object as the global root in a browser. Under window sits document, which is the DOM. Also under window sit location, navigator, history, screen, and various utilities like innerWidth, setTimeout, and localStorage. The document branch leads to the page content: elements, text, and the tree. The location, navigator, history, and screen branches lead to the browser environment around the document, which collectively is the BOM, the Browser Object Model.

That diagram is the whole lesson in one picture. window.document is the DOM - the content of the page. Everything else hanging off window that relates to the browser itself - the URL, the history, the user's browser identity, the screen - is the BOM.

The DOM: the document's content

You know this one well by now. The DOM is document and everything under it: the tree of elements, their attributes, their text, and the APIs to read and manipulate them (getElementById, querySelector, createElement, addEventListener on nodes, and so on). The DOM is standardized by the W3C/WHATWG, it's well-specified, and it's specifically about the content rendered on the page. If it's about an element, a node, or the structure of what's displayed, it's the DOM.

The BOM: the browser around the document

The BOM is everything the browser exposes about itself and its environment, as opposed to the page content. There's no single tidy specification called "The BOM" - it's a loose, historically-accreted collection of objects that browsers provide - but the pieces are concrete and you use them daily:

DOM BOM
Concerned with Page content The browser environment
Rooted at document window (location, navigator, history, screen)
Standardized? Yes, well-specified Loosely; a historical collection
Example APIs querySelector, createElement location.href, history.pushState, navigator.language
You use it to Read/change what's displayed Read/change URL, history, browser info

Note: A clean way to hold the boundary: if it's about the content of the page, it's the DOM; if it's about the browser or the environment the page runs in, it's the BOM. "Change this element's text" → DOM. "What's the current URL / what language is the browser / go back one page" → BOM. The dividing line is content-versus-environment.

Why this boundary actually matters

This isn't just taxonomy for a quiz. The DOM/BOM split maps directly onto real frontend concerns, and naming them helps you reach for the right tool.

Routing lives in the BOM. Client-side routing - the thing that makes a single-page app change "pages" without a server round trip - is built on history.pushState and location. The router updates the URL via the BOM, listens for the popstate event (BOM), reads location.pathname (BOM) to decide what to show, and then updates the page content via the DOM. Routing is literally BOM drives DOM. We'll see this in action next lesson.

Feature detection and adaptation live in the BOM. Reading navigator.language to pick a locale, navigator.onLine to show an offline banner, navigator.connection to adapt to slow networks, window.matchMedia to react to dark mode or viewport - these are all BOM, and they're how a frontend adapts to the user's actual conditions, which (callback to the very first lesson) is the entire frontend game.

Storage is BOM-adjacent. localStorage and sessionStorage hang off window, and they're how you persist client state across reloads. We have a whole module on client storage; the point here is just knowing where these APIs live in the object hierarchy.

Interview: The question "DOM vs BOM" is usually a warm-up, and the way to turn a warm-up into a signal is to connect it to something real: "The DOM is the page content rooted at document; the BOM is the browser environment rooted at window - location, history, navigator, screen. The reason I think about the distinction is that client-side routing is essentially the BOM driving the DOM: the router manipulates history and location to change the URL without a reload, then re-renders the content through the DOM." That last sentence shows you don't just know the definition, you know why it matters.

A subtle point: window vs document sizing

One concrete place the DOM/BOM distinction trips people up is measurement, and it's worth a moment because it causes real bugs. window.innerWidth is the viewport width - the visible area of the browser, a BOM concept. document.documentElement.clientWidth is the width of the document's root element - a DOM concept - and it excludes the scrollbar, so it can differ from innerWidth by a few pixels. screen.width is the physical screen width, which is different again and usually not what you want for layout. Three "widths," three different meanings, sitting in different parts of the object model. When a responsive layout is off by the scrollbar's width, this confusion is often why. Knowing which object you're reading from - environment (BOM) versus content (DOM) - tells you which number you're actually getting.

The History API in depth: how a router is actually built

Because client-side routing is the single most important practical use of the BOM, let's actually build the mental model of how a router works on top of history and location, end to end. This is both genuinely useful and a common follow-up question - "how does client-side routing work under the hood?"

A router has three jobs, and each maps to a BOM capability:

  1. Change the URL without reloading. When the user clicks an in-app link, the router calls history.pushState(state, '', '/about'). This updates the address bar and adds an entry to the session history - without triggering a request to the server. The page doesn't reload; only the URL string changed. (replaceState does the same but overwrites the current entry instead of adding one - used for redirects you don't want in the back-button history.)

  2. React to the back and forward buttons. When the user hits back, the browser fires a popstate event on window. The router listens for it, reads the new location.pathname, and re-renders the matching view. Without this listener, the back button would change the URL but not the content - a classic broken-SPA bug.

  3. Decide what to render from the URL. On every navigation, the router reads location.pathname (and maybe location.search for query params or location.hash for fragments), matches it against its route table, and renders the corresponding component.

sequenceDiagram
    participant U as User
    participant R as Router
    participant H as history (BOM)
    participant W as window (BOM)
    U->>R: clicks in-app link /about
    R->>H: pushState(null, '', '/about')
    Note over H: URL changes, NO reload
    R->>R: read location.pathname → render <About>
    U->>W: presses Back button
    W->>R: popstate event fires
    R->>R: read new location.pathname → render previous view
This sequence diagram shows how a client-side router uses the BOM. The user clicks an in-app link to /about. The router calls pushState on the history object, which changes the URL with no reload. The router reads location.pathname and renders the About view. Later the user presses the back button, the window fires a popstate event, the router catches it, reads the new location pathname, and renders the previous view. Throughout, the router is manipulating the BOM's history and location to simulate navigation while the DOM handles the actual rendering.

That's the whole trick, and it's why last lesson's framing - the BOM drives, the DOM renders - is so useful. Every routing library you've used (React Router, Vue Router, and the routers baked into Next.js and friends) is fundamentally this loop wrapped in a nicer API. There's also an older, simpler approach called hash routing that uses location.hash (the #/about style) instead of pushState. Because the fragment never gets sent to the server and changing it never triggers a reload, hash routing works without any server configuration - but it produces uglier URLs and slightly worse SEO. Knowing both, and when you'd accept hash routing (static hosting with no server-side route handling), is a nice depth point.

Adapting to the user through navigator

The other major BOM use is adaptation - reading the user's environment to tailor the experience, which (one more callback to lesson one) is the essence of frontend work. A few concrete, real patterns:

Warning: There's a right and wrong way to use navigator for adaptation. Feature detection - checking whether an API exists before calling it - is robust and future-proof. User-agent sniffing - parsing navigator.userAgent to guess the browser and branch on it - is fragile, easily spoofed, and historically the cause of "works in everything except the browser you forgot to special-case" bugs. Reach for "does this capability exist?" not "which browser is this?" Interviewers notice the difference.

Working with location and URLs without footguns

location is the BOM object you'll reach for most, and it has a few sharp edges worth knowing because they cause real bugs and occasionally come up as a "gotcha" question.

First, there are three distinct ways to send the user to a new URL, and they're not interchangeable:

The distinction between assign and replace mirrors the pushState/replaceState distinction from routing, and for the same reason: it's about whether you want this navigation to be reversible via the back button. Getting it wrong produces the maddening "I keep getting bounced back to the page I just left" bug.

Second, don't parse URLs by hand. People still reach for string operations - splitting on ?, slicing the # - to pull apart a URL, and it's error-prone. The platform gives you the URL and URLSearchParams objects for exactly this:

const url = new URL(window.location.href);
url.pathname;                    // "/products"
url.searchParams.get('category'); // "shoes"  (no manual ?-splitting)
url.searchParams.set('page', '2');
url.hash;                        // "#reviews"
window.history.pushState(null, '', url); // update the URL cleanly

This matters in practice because query parameters are a legitimate place to store UI state - the current filter, the active tab, the search query - so that the state survives a reload and is shareable via the URL. Reading and writing those params correctly through URLSearchParams, rather than fragile string surgery, is the difference between robust deep-linkable state and a pile of edge-case bugs around encoding, missing params, and multiple values. This connects forward to the state-management module, where "where should this piece of state live?" sometimes has the answer "in the URL" - and the URL is BOM territory.

Tip: A clean mental model for the three "locations" of client state you'll juggle: component-local state for ephemeral UI, a store for shared app state, and the URL (via location and history) for state that should be bookmarkable, shareable, and survive a reload - filters, tabs, search terms, pagination. The URL is an underused state container, and reaching for it deliberately is a mark of a thoughtful frontend engineer.

What you should carry out of this lesson

window is the global root. Under it, document is the DOM - the page's content, well-standardized, all about elements and the tree. Everything else about the browser itself - location, history, navigator, screen - is the BOM, a looser collection that's all about the environment the page runs in. The dividing line is content versus environment.

The reason this earns a lesson rather than a footnote is that the BOM is where adaptation and routing live - reading the user's conditions and changing the URL without a reload. And that second capability, changing the URL without asking the server, is the precise trick that makes single-page applications possible. So with the DOM/BOM map in hand, we're ready for the architecture question that shapes nearly every modern frontend: should this be a single-page app at all, or a multi-page one? That's next.

Back to Frontend System Design