Build Shareable Search and Filter URLs with JavaScript
A visitor spends five minutes finding the right products, selecting two categories and sorting by price. They copy the page URL to a colleague. The colleague opens it and sees the default list, not the filtered results.
The interface kept its state only in JavaScript. The URL never changed.
For search results, documentation filters, product catalogs and many design tools, a shareable URL is a small feature with a big usability payoff. Users can bookmark a useful view, return after a reload, or send someone a link that reproduces their selections.
The browser already has most of the pieces: URL, URLSearchParams, and the History API. The challenge is choosing what belongs in the address bar and implementing the behavior without losing parameters or leaking sensitive information.
Choose which state deserves a URL
Good candidates are values that explain what the visitor is looking at: a search term, a page number, a category, sort order or selected public tags. If changing a setting produces a meaningfully different view, making it bookmarkable is often helpful.
Not everything belongs there. Keep passwords, access tokens, private notes, unpublished content, email addresses and other sensitive data out of query strings. URLs can appear in browser history, server logs, analytics, screenshots and referrer information. A URL is not a private storage location.
A typical searchable catalog might expose:
https://example.com/resources?q=css&tag=layout&tag=accessibility&sort=newest
The two tag keys are intentional. Many applications use repeated parameters to represent several selected filters. Web2Generators’ URL encoder, decoder, and query editor lets you inspect and adjust repeated parameters before writing application code. Its introduction and examples also explain why a plus sign can be misread as a space.
Read parameters without splitting strings by hand
A common mistake is to parse query strings with .split("&") and .split("="). That overlooks percent-encoding, repeated keys, empty values, and Unicode characters. Use the browser’s URL APIs instead:
const url = new URL(window.location.href);
const params = url.searchParams;
const query = params.get("q") ?? "";
const tags = params.getAll("tag");
const sort = params.get("sort") ?? "relevance";
get() returns the first matching value (or null); getAll() returns every matching value in order. The values are decoded for you. If the URL contains tag=css&tag=html, calling getAll("tag") returns ['css', 'html'].
The address bar is user-controlled input. Validate it even if the parameters were produced by your own application. For example, use a fixed list of allowed sort values and cap the number and length of filters:
function readFilters(url = new URL(window.location.href)) {
const params = url.searchParams;
const allowedSorts = new Set(["relevance", "newest", "title"]);
const requestedSort = params.get("sort") ?? "relevance";
return {
q: Array.from((params.get("q") ?? "").trim()).slice(0, 100).join(""),
sort: allowedSorts.has(requestedSort) ? requestedSort : "relevance",
tags: params.getAll("tag")
.filter(tag => /^[a-z0-9-]{1,30}$/.test(tag))
.slice(0, 8)
};
}
The search limit counts Unicode code points rather than UTF-16 code units, so it does not cut a surrogate pair in half. These are example product rules, not universal limits. A search tool might permit other languages and longer phrases, while a product catalog might recognize tag IDs rather than names. Validate parsed values according to the application’s real schema.
Write changes using URLSearchParams
To update a filter, create a URL from the current page and modify its searchParams object. Do not concatenate a raw user-entered value directly into the URL:
function buildFilterUrl({ q, sort, tags }) {
const url = new URL(window.location.href);
url.searchParams.delete("q");
url.searchParams.delete("sort");
url.searchParams.delete("tag");
if (q) url.searchParams.set("q", q);
if (sort !== "relevance") url.searchParams.set("sort", sort);
for (const tag of tags) url.searchParams.append("tag", tag);
return url;
}
append() adds a new pair even when that key already exists. set() replaces existing entries of the same name with a single value. Pass validated state to this function. It deletes its own filter keys before rebuilding them, so repeated tags remain intentional and unrelated parameters can stay in place. Their values survive, but serialization can change their encoded spelling.
Be careful with unrelated parameters, though. A URL containing a cryptographic signature may become invalid if its query string is reconstructed, reordered or re-encoded. Signed URLs should generally be treated as opaque unless their signing specification says otherwise.
The plus sign trap
In form-style query strings, a bare + is interpreted as a space. A literal plus sign in a search for C++ needs encoding, typically %2B. URLSearchParams takes care of this when you call set("q", "C++"); do not pre-encode that value with encodeURIComponent() and then pass it to set(), or you risk double-encoding the percent signs.
Test representative inputs with the Web2Generators URL encoder and decoder: spaces, +, ampersands, accented characters, emoji, and repeated keys. The tool is useful for visual inspection, not a replacement for the URL APIs in an application.
Decide between replaceState and pushState
The History API updates the visible address without immediately reloading the page:
const nextUrl = buildFilterUrl({
q: "css grids",
sort: "newest",
tags: ["layout", "responsive"]
});
history.replaceState(history.state, "", nextUrl);
Choose replaceState() when a small adjustment should update the current history entry, for example while someone types into a search box. Choose pushState() when a deliberate navigation step deserves a Back button entry, for example clicking “Apply filters” or moving to a new result page.
Neither method automatically fetches data, redraws your results or fires popstate. Your code must render the new state and handle navigation back to older entries. Both methods require a same-origin URL. If a framework router owns navigation, use its API rather than overwriting its history state.
The following standalone example uses readFilters() and buildFilterUrl() from above. Load the script after the form, or use a module script:
<form id="filter-form">
<label for="search">Search resources</label>
<input id="search" name="q" type="search">
<button type="submit">Apply search</button>
</form>
const form = document.querySelector("#filter-form");
const search = document.querySelector("#search");
function restoreFromUrl() {
const filters = readFilters();
search.value = filters.q;
// Render results using filters.q, filters.sort and filters.tags here.
}
form.addEventListener("submit", (event) => {
event.preventDefault();
const candidate = new URL(location.href);
candidate.searchParams.set("q", search.value);
const nextUrl = buildFilterUrl(readFilters(candidate));
if (nextUrl.href !== location.href) {
history.pushState(null, "", nextUrl);
}
restoreFromUrl();
});
window.addEventListener("popstate", restoreFromUrl);
restoreFromUrl();
This example demonstrates URL synchronization, not a full search implementation. In a real application, restore the entire filter state and call the same rendering or data-loading function after submission and popstate.
Avoid creating an unusable browser history
If every keystroke calls pushState, a user who typed a twelve-letter word may need to press Back twelve times. For live editing, replaceState() avoids that buildup; for explicit submissions, the example above pushes only when the URL changes. Replacing during typing also overwrites the previous view: pushing the same URL on Apply will not bring that old view back. Choose the history checkpoints you want to preserve before combining these approaches. Debounce live URL writes and data requests together, and discard stale responses so the results match the current URL.
Decide what happens when a user deletes all filters. Usually, removing unnecessary keys produces a cleaner URL than keeping sort=relevance&q=&page=1. It also makes the link easier to read and share.
Test the URL as an independent entry point
Do not test only by clicking around inside an already loaded app. Copy the final URL into a fresh tab or private window. Does it reconstruct the right view? Does it work when someone isn’t logged in? Does it produce a useful error if a filter is obsolete?
Check browser Back and Forward, refresh, direct linking, mobile layouts, and combinations with other query parameters. The server must serve the application at the shared path; pushState() does not create a server route. Use the Network inspection tutorial to verify that filter changes request only the intended data. If your filters call an API, the cURL Command Builder can help reproduce the resulting GET requests during debugging.
A final security check matters too: a URL parameter is never permission to access a resource. The server must still authorize private data requests. Render search text as text, not unchecked HTML. For the distinction between safe display and rich-text rendering, see HTML Escaping vs Sanitizing.
When shareable links are especially useful
Filter URLs are only one example. Design systems can encode a selected palette, documentation sites can link directly to language or version choices, and product lists can preserve their sorting. Web2Generators’ Color Studio introduction is an example of a design tool that offers shareable palette links, while the Avatar Generator also illustrates the value of returning to a chosen result later.
Those tools store their share codes in the fragment after #, rather than in query parameters. Fragments are not sent in the HTTP request, but page scripts can read them and copied links still reveal them. They are not a place for secrets.
The key is making URL state predictable, limited and safe to share. When a link faithfully describes the current view, the browser’s familiar bookmarking and navigation features start working for your interface instead of against it.
Further reading
Before shipping your next filter interface, try the Web2Generators URL encoder, decoder, and query editor with a real example link to spot encoding and duplicate-parameter mistakes.