Skip to content
ExamsLibraryFor agentsConnect your agent

The URL, structure and scroll position

intermediate·8 min·Markdown for agents

What this lesson teaches

Keep what the user sees in the URL so links and the back button work, arrange screens the way people think, and stop lists jumping when content loads.

  1. 1The URL is the source of truth
  2. 2Structure the way people think
  3. 3Keep the reader's place

Pick a tab, filter to "Assigned to me", change the sort. Then press "Open link in a new tab". Try it in both modes.

acme.app/issues
  • Toast hidden under modal

The link, opened in a new tab

acme.app/issues
Open · Everyone · Sort: updated
  • Login button misaligned on Safari
  • Dark logo on dark header
  • Invoice PDF shows wrong total
Reset to defaults: the filter lived only in memory.
State in memory resets when shared. State in the URL is the view.

When the view lived in component state, the new tab forgot everything. When it lived in the URL, the link was the view: same tab, same filter, same sort. And Back stepped through your changes.

The URL is the source of truth

The rule from Vercel's interface guidelines: anything a user can see right now should be rebuildable from the URL. Paste it in a new tab and you land on the same view. That's .

StateWhereExample
The pagepath/projects/acme/issues
Tab?tab=closed
Filtersquery?status=open,blocked&assignee=me
Sortquery?sort=-updated_at
Paginationquery?cursor=eyJpZCI6...
Open panel or modalquery?modal=invite
Searchquery?q=auth+bug

What does not go in the URL: form drafts (local storage), login state (cookies), whether a toast is showing, hover and focus.

Three rules make it work:

  • Deep-link every modal. ?modal=invite-team should open the invite dialog straight from the link. That's a .
  • One source. Read state from the URL (useSearchParams in Next.js) and write through the router. Copying URL state into useState creates two versions that drift.
  • Back restores scroll. Normal links and most routers do this; a custom scroll lock or an intercepted click often breaks it. Check yours.

Structure the way people think

decides what people can find before any pixel is styled. The rule under all of it: arrange and name things the way users think about them, not the way the code or the org chart is split.

  • Long settings get sections. Past about a dozen options, split them under a side nav with named groups (Account, Billing, Notifications), each a short page.
  • : show what most people need; put rare options behind "Advanced", or reveal a feature when it becomes relevant. Benji Taylor describes how Family kept the basics at hand and revealed the rest by context.
  • Name by the user's words. "Send messages in the background", not "Enable async relay mode".
  • Tabs are views of one thing (this week / month / year), not unrelated destinations.
  • Two levels of menu, then stop. Deeper than that, use pages with breadcrumbs and a URL.
  • Search logs are rename requests. If people keep searching "invoices" and the nav says "Billing", rename it.

✕ Dead end

"Something went wrong." and a Back button.

✓ A way forward

"That link expired. Request a new one." with the button that does it.

Renaming beats restructuring. Before redrawing the sitemap, check whether the labels are the problem; a label is a one-line fix.

Keep the reader's place

One more way a page loses people: you're reading a chat log, older messages load above, and the message you were on jumps away. Try it with anchoring off, then on.

SamLooks good to me
JunCan you check the spacing?
LenaMerged
OmarShipping after lunch
MiraLeft a comment
SamUpdated the copy
JunYou are reading this one
LenaPushed the fix
OmarLooks good to me
MiraCan you check the spacing?
SamMerged
JunShipping after lunch

Each load shoves your message down and out of view.

Load older messages with anchoring off, then on. The highlighted message is where you were reading.

means tying the view to a row, not to a pixel offset. Cheng Lou's pattern:

  1. Pick an anchor when the user scrolls

    The first row whose bottom is below the top of the view. Record how far it is from the top of the view.

  2. When content changes, restore that distance

    After the new rows render, set the scroll position so the anchor row sits exactly where it was.

  3. Detect user scrolling by value

    Compare the scroll position with the one you stored. Don't trust "a scroll event fired": your own scroll fires one too.

  4. Store what the browser actually set

    Read the scroll position back after setting it, because browsers round. Storing the value you asked for makes the next frame think the user scrolled.

If you anchor in code, set overflow-anchor: none on that scroller so the browser's built-in anchoring doesn't move it a second time. For a simple page, the browser's own overflow-anchor is the cheaper option. A chat that was showing the latest message should keep showing it after a new one arrives.

URL state and stable lists· for your agent
Refactor this list page:
- Tab, filters, sort, search, pagination cursor and any open panel or modal live in URL query params. Read with useSearchParams, write through the router; no mirrored useState.
- Every modal is deep-linkable (?modal=...). Back must restore the previous view and its scroll position.
- When older items load above the view, keep the row in view anchored: record the first visible row's offset before the update and restore it after; set overflow-anchor: none on the scroller.
- Check the labels against what users search for and suggest renames before restructuring the nav.

Quick check

A user filters a table and sends the link to a colleague, who sees the unfiltered table. What's the fix?

What this lesson builds on and leads to, across every course.

  • Grouping by space
  • Words matter
  • Briefs that work