useSearchParams
Reactive, schema-validated URL search params for Svelte/SvelteKit
useSearchParams provides a reactive, type-safe, and schema-driven way to manage URL search
parameters in Svelte/SvelteKit apps. It supports validation, default values, compression,
debouncing, and history control.
Demo
Note
Requirements
@sveltejs/kitmust be installed in your project.- Uses Standard Schema for schema validation and type inference.
Usage
Define schema:
// schemas.ts
import { z } from "zod";
// Version 3.24.0+
export const productSearchSchema = z.object({
page: z.coerce.number().default(1),
filter: z.string().default(""),
sort: z.enum(["newest", "oldest", "price"]).default("newest")
});
In your svelte code:
<script lang="ts">
import { useSearchParams } from "runed/kit";
import { productSearchSchema } from './schemas';
const params = useSearchParams(productSearchSchema);
// Access parameters directly
const page = $derived(params.page); // number (defaults to 1)
const sort = $derived(params.sort); // 'newest' | 'oldest' | 'price'
// Update parameters directly
params.page = 2; // Updates URL to include ?page=2
// Updates URL to include ?page=3&sort=oldest
params.update({ page: 3, sort: 'oldest' });
// Resets all parameters to their default values
params.reset();
// Returns URLSearchParams object with all current parameter values
params.toURLSearchParams();
/**
* You can watch for changes to the URLSearchParams object
* For example:
* watch(() => params.toURLSearchParams(), () => {
* // Do something whenever the URLSearchParams object changes
* })
*/
</script>
<-- Great for binding to input fields -->
<input type="text" bind:value={params.filter} />
In your load function:
import { validateSearchParams } from "runed/kit";
import { productSearchSchema } from "./schemas";
export const load = ({ url, fetch }) => {
// Get validated search params as URLSearchParams object
// If you use a custom compressedParamName in useSearchParams, provide it here too:
const { searchParams } = validateSearchParams(url, productSearchSchema);
// Use URLSearchParams directly with fetch
const response = await fetch(`/api/products?${searchParams.toString()}`);
return {
products: await response.json()
};
};
Features
- Schema Validation: Define types, defaults, and structure for each param.
- Default Values: Missing params are auto-filled with defaults.
- Type Safety: All values are parsed and validated to the schema.
- Compression: Store all params in a single compressed
_dataparam for cleaner URLs. - Debounce: Optionally debounce updates for smoother UX.
- History Control: Choose between push/replace state.
- In-memory Mode: Params are kept in memory, not in the URL.
- Invalid Param Handling: Invalid values are replaced with defaults and removed from the URL.
useSearchParams
Hook to create a reactive search params object with property access
This client-side hook automatically updates the URL when parameters change. It provides type-safe access to URL search parameters through direct property access.
Parameters:
schema: A validation schema compatible with StandardSchemaV1options: Configuration options that affect URL behavior
Returns:
- A reactive object for working with typed search parameters
Available options:
showDefaults(boolean): When true, parameters with default values will be shown in the URL. When false (default), parameters with default values will be omitted from the URL.debounce(number): Milliseconds to delay URL updates when parameters change. Useful to avoid cluttering browser history when values change rapidly (default: 0, no debounce).pushHistory(boolean): Controls whether URL updates create new browser history entries. If true (default), each update adds a new entry to the browser history. If false, updates replace the current URL without creating new history entries.compress(boolean): When true, all parameters are compressed into a single parameter using lz-string compression. This helps reduce URL length and provides basic obfuscation (default: false). Use validateSearchParams with the same compressedParamName option when handling compressed URLs server-side.compressedParamName(string): The name of the parameter used to store compressed data when compression is enabled. Customize this to avoid conflicts with parameters in your schema. Default is_data.updateURL(boolean): When true (default), the URL is updated when parameters change. When false, only in-memory parameters are updated.noScroll(boolean): When true, the scroll position is preserved when the URL is updated. This prevents the page from jumping to the top on URL changes. Default is false.
Example with Zod:
import { z } from "zod";
const productSearchSchema = z.object({
page: z.number().default(1),
filter: z.string().default(""),
sort: z.enum(["newest", "oldest", "price"]).default("newest")
});
const params = useSearchParams(productSearchSchema);
// Access parameters directly
const page = $derived(params.page); // number (defaults to 1)
const sort = $derived(params.sort); // 'newest' | 'oldest' | 'price'
Example with options:
// Show default values in URL, debounce updates by 300ms,
// don't create new history entries, and compress params
const params = useSearchParams(schema, {
showDefaults: true,
debounce: 300,
pushHistory: false,
compress: true,
compressedParamName: '_compressed' // Custom name to avoid conflicts
});
// Great for binding to input fields (updates URL without cluttering history)
<input type="text" bind:value={params.search} />
// Resulting URL will be something like: /?_compressed=N4IgDgTg9g...
Example with Valibot:
import * as v from "valibot";
const productSearchSchema = v.object({
page: v.optional(v.fallback(v.number(), 1), 1),
filter: v.optional(v.fallback(v.string(), ""), ""),
sort: v.optional(v.fallback(v.picklist(["newest", "oldest", "price"]), "newest"), "newest")
});
const params = useSearchParams(productSearchSchema);
Example with Arktype:
import { type } from "arktype";
const productSearchSchema = type({
page: "number = 1",
filter: 'string = ""',
sort: '"newest" | "oldest" | "price" = "newest"'
});
const params = useSearchParams(productSearchSchema);
Or with our built-in schema creator (no additional dependencies)
createSearchParamsSchema
Creates a simple schema compatible with Standard Schema without requiring external validation libraries.
This is a lightweight alternative to using full schema validation libraries like Zod, Valibot, or Arktype. Use this when you need basic type conversion and default values without adding dependencies.
Limitations:
- For 'array' type: supports basic arrays, but doesn't validate array items
- For 'object' type: supports generic objects, but doesn't validate nested properties
- No custom validation rules or transformations
- No granular reactivity: Changes to nested properties require reassigning the entire value
- ❌
params.config.theme = 'dark'(won't trigger URL update) - ✅
params.config = {...params.config, theme: 'dark'}(will trigger URL update)
- ❌
For complex validation needs (nested validation, refined rules, etc.), use a dedicated validation library instead.
const productSearchSchema = createSearchParamsSchema({
// Basic types with defaults
page: { type: "number", default: 1 },
filter: { type: "string", default: "" },
sort: { type: "string", default: "newest" },
// Array type with specific element type
tags: {
type: "array",
default: ["new"],
arrayType: "" // Specify string[] type
},
// Object type with specific shape
config: {
type: "object",
default: { theme: "light" },
objectType: { theme: "" } // Specify { theme: string } type
}
});
URL storage format:
- Arrays are stored as JSON strings:
?tags=["sale","featured"] - Objects are stored as JSON strings:
?config={"theme":"dark","fontSize":14} - Dates are stored as ISO8601 strings:
?createdAt=2023-12-01T10:30:00.000Z(orYYYY-MM-DDwith date-only format) - Primitive values are stored directly:
?page=2&filter=red
Date Format Support
You can control how Date parameters are serialized in URLs using two approaches:
Option 1: Using dateFormat property in schema
const schema = createSearchParamsSchema({
// Date-only format (YYYY-MM-DD) - great for birth dates, event dates
birthDate: {
type: "date",
default: new Date("1990-01-15"),
dateFormat: "date"
},
// Full datetime format (ISO8601) - great for timestamps
createdAt: {
type: "date",
default: new Date(),
dateFormat: "datetime"
},
// No format specified - defaults to 'datetime'
updatedAt: {
type: "date",
default: new Date()
}
});
const params = useSearchParams(schema);
// URL: ?birthDate=1990-01-15&createdAt=2023-01-01T10:30:00.000Z&updatedAt=2023-12-25T18:30:00.000Z
Option 2: Using dateFormats option (works with any validator)
// Works with Zod, Valibot, Arktype, or createSearchParamsSchema
const params = useSearchParams(zodSchema, {
dateFormats: {
birthDate: "date", // YYYY-MM-DD
createdAt: "datetime" // ISO8601
}
});
Date Format Details:
'date'format: Serializes asYYYY-MM-DD(e.g.,2025-10-21)- More readable in URLs
- Perfect for calendar dates, birth dates, event dates
- Parsed as Date object with time set to midnight UTC
'datetime'format (default): Serializes as full ISO8601 (e.g.,2025-10-21T18:18:14.196Z)- Preserves exact time information
- Perfect for timestamps, created/updated times
- Full precision date and time
Practical Example:
<script lang="ts">
import { useSearchParams, createSearchParamsSchema } from "runed/kit";
const schema = createSearchParamsSchema({
eventDate: {
type: "date",
default: new Date("2025-01-01"),
dateFormat: "date"
},
createdAt: {
type: "date",
default: new Date(),
dateFormat: "datetime"
}
});
const params = useSearchParams(schema);
</script>
<label>
Event Date:
<input
type="date"
value={params.eventDate.toISOString().split("T")[0]}
oninput={(e) => (params.eventDate = new Date(e.target.value))} />
</label>
<label>
Created At:
<input
type="datetime-local"
value={params.createdAt.toISOString().slice(0, 16)}
oninput={(e) => (params.createdAt = new Date(e.target.value))} />
</label>
<!-- URL will be: ?eventDate=2025-01-01&createdAt=2025-10-21T18:18:14.196Z -->
validateSearchParams
A utility function to extract, validate and convert URL search parameters to URLSearchParams
This function makes it easy to use the same schema validation in both client-side components (via useSearchParams) and server-side load functions. Unlike `useSearchParams, this function doesn't
modify the URL - it only validates parameters and returns them as a new URLSearchParams object.
Handles both standard URL parameters and compressed parameters (when compression is enabled).
Parameters:
url: The URL object from SvelteKit load functionschema: A validation schema (createSearchParamsSchema, Zod, Valibot, etc.)options: Optional configuration (like customcompressedParamNameanddateFormats)
Returns:
- An object with
searchParamsanddataproperties,searchParamsbeing the validatedURLSearchParamsanddatabeing the validated object
Available options:
compressedParamName(string): Custom name for compressed parameter (default:_data)dateFormats(object): Map of field names to date formats ('date'or'datetime')
Example with SvelteKit page or layout load function:
import { validateSearchParams } from "runed/kit";
import { productSchema } from "./schemas";
export const load = ({ url, fetch }) => {
// Get validated search params as URLSearchParams object
// If you use a custom compressedParamName in useSearchParams, provide it here too:
const { searchParams } = validateSearchParams(url, productSchema, {
compressedParamName: "_compressed",
dateFormats: {
birthDate: "date", // Serialize as YYYY-MM-DD
createdAt: "datetime" // Serialize as ISO8601
}
});
// Use URLSearchParams directly with fetch
const response = await fetch(`/api/products?${searchParams.toString()}`);
return {
products: await response.json()
};
};
Advanced: Custom Serialization with Zod Codecs
For advanced use cases where you need full control over how values are converted between URL strings and JavaScript types, you can use Zod codecs (Zod v4.1.0+). Codecs allow you to define custom bidirectional transformations that work seamlessly with URL parameters.
Why Use Codecs?
While the built-in dateFormats option works well for common cases, codecs give you complete
control over serialization. Use codecs when you need to:
- Custom date formats: Store dates as Unix timestamps, custom date strings, or other formats
- Complex type conversions: Convert between incompatible types (e.g., number IDs ↔ full objects)
- Data transformation: Apply transformations during serialization (e.g., normalize, encrypt)
- Legacy API compatibility: Match existing URL parameter formats from other systems
- Optimization: Use more compact representations (e.g.,
1234567890instead of2009-02-13T23:31:30.000Z)
How Codecs Work
A Zod codec defines two transformations:
decode: Converts URL string → JavaScript type (when reading from URL)encode: Converts JavaScript type → URL string (when writing to URL)
import { z } from "zod";
// Example 1: Unix timestamp codec (stores Date as number)
const unixTimestampCodec = z.codec(
z.coerce.number(), // Input: number from URL string
z.date(), // Output: Date object in your app
{
decode: (timestamp) => new Date(timestamp * 1000), // number → Date
encode: (date) => Math.floor(date.getTime() / 1000) // Date → number
}
);
// Example 2: Date-only codec (stores Date as YYYY-MM-DD)
const dateOnlyCodec = z.codec(
z.string(), // Input: string from URL
z.date(), // Output: Date object in your app
{
decode: (str) => new Date(str + "T00:00:00.000Z"), // "2025-01-15" → Date
encode: (date) => date.toISOString().split("T")[0] // Date → "2025-01-15"
}
);
// Example 3: Product ID codec (stores number as base36 string for shorter URLs)
const compactIdCodec = z.codec(
z.string(), // Input: base36 string from URL
z.number(), // Output: number in your app
{
decode: (str) => parseInt(str, 36), // "abc123" → 225249695
encode: (num) => num.toString(36) // 225249695 → "abc123"
}
);
Using Codecs in Your Schema
import { z } from "zod";
const searchSchema = z.object({
// Regular fields work as before
query: z.string().default(""),
page: z.coerce.number().default(1),
// Unix timestamp - more compact than ISO datetime
createdAfter: unixTimestampCodec.default(new Date("2024-01-01")),
// Date-only format - cleaner for calendar dates
birthDate: dateOnlyCodec.default(new Date("1990-01-15")),
// Compact product IDs
productId: compactIdCodec.optional()
});
const params = useSearchParams(searchSchema);
Real-World Example: Event Search
<script lang="ts">
import { z } from "zod";
import { useSearchParams } from "runed/kit";
// Define reusable codecs
const unixTimestamp = z.codec(z.coerce.number(), z.date(), {
decode: (ts) => new Date(ts * 1000),
encode: (date) => Math.floor(date.getTime() / 1000)
});
const dateOnly = z.codec(z.string(), z.date(), {
decode: (str) => new Date(str + "T00:00:00.000Z"),
encode: (date) => date.toISOString().split("T")[0]
});
// Schema with multiple codec types
const eventSearchSchema = z.object({
query: z.string().default(""),
// Date-only for event date (more readable in URL)
eventDate: dateOnly.default(new Date()),
// Unix timestamp for filters (more compact)
createdAfter: unixTimestamp.optional(),
updatedSince: unixTimestamp.optional()
});
const params = useSearchParams(eventSearchSchema);
// Work with Date objects in your app
$inspect(params.eventDate); // Date object
$inspect(params.createdAfter); // Date object or undefined
</script>
<!-- Bind to native date inputs -->
<label>
Event Date:
<input
type="date"
value={params.eventDate.toISOString().split("T")[0]}
oninput={(e) => (params.eventDate = new Date(e.target.value))} />
</label>
<label>
Created After:
<input
type="date"
value={params.createdAfter?.toISOString().split("T")[0] ?? ""}
oninput={(e) =>
(params.createdAfter = e.target.value ? new Date(e.target.value) : undefined)} />
</label>
<!-- Clean URLs:
Without codecs: ?eventDate=2025-01-15T00:00:00.000Z&createdAfter=2024-01-01T00:00:00.000Z
With codecs: ?eventDate=2025-01-15&createdAfter=1704067200
-->
Codec Benefits Summary
| Feature | Built-in dateFormats | Zod Codecs |
|---|---|---|
| Setup complexity | Simple | More configuration needed |
| Date formats | date and datetime | Any custom format (Unix, relative, custom string) |
| URL size | Standard | Can be optimized (e.g., Unix timestamps) |
| Type conversions | Date only | Any type (numbers, objects, arrays, etc.) |
| Validation | Basic | Full Zod validation + transformation |
| Reusability | Per-field config | Create reusable codec definitions |
| Legacy compatibility | Limited | Full control over format |
| Works with validators | All (Zod, Valibot, etc.) | Zod only (v4.1.0+) |
| Server-side usage | Use dateFormats option | Automatic with validateSearchParams |
When to use dateFormats: Most applications with standard date handling needs
When to use codecs: When you need custom formats, compact representations, or complex type conversions
Server-Side Usage with Codecs
Codecs work automatically with validateSearchParams:
// +page.server.ts
import { validateSearchParams } from "runed/kit";
import { eventSearchSchema } from "./schemas"; // Schema with codecs
export const load = ({ url }) => {
// Codecs are automatically applied during validation
const { searchParams, data } = validateSearchParams(url, eventSearchSchema);
// data.eventDate is a Date object (decoded from URL string)
// searchParams contains properly encoded values for API calls
return {
events: await fetchEvents(searchParams)
};
};
Reactivity Limitations
Understanding Reactivity Scope
useSearchParams provides top-level reactivity only. This means:
✅ Works (Direct property assignment):
<script>
const params = useSearchParams(schema);
// These trigger URL updates
params.page = 2;
params.filter = "active";
params.config = { theme: "dark", size: "large" };
params.items = [...params.items, newItem];
</script>
❌ Doesn't work (Nested property mutations):
<script>
const params = useSearchParams(schema);
// These DON'T trigger URL updates
params.config.theme = "dark"; // Nested object property
params.items.push(newItem); // Array method
params.items[0].name = "updated"; // Array item property
delete params.config.oldProp; // Property deletion
</script>
Why This Design Choice
useSearchParams was designed to prioritize simplicity, type safety, and ease of use over deep
reactivity. This design choice offers several benefits:
✅ What You Get
- Simple, predictable API:
params.page = 2always works - Full TypeScript support: Perfect autocomplete and type checking
- Clean URLs: Objects serialize to readable JSON strings
- Performance: No overhead from tracking deep object changes
- Reliability: No edge cases with complex nested proxy behaviors
Type Definitions
interface SearchParamsOptions {
/**
* If true, parameters set to their default values will be shown in the URL.
* If false, parameters with default values will be omitted from the URL.
* @default false
*/
showDefaults?: boolean;
/**
* The number of milliseconds to delay URL updates when parameters change.
* This helps avoid cluttering browser history when values change rapidly
* (like during typing in an input field).
* @default 0 (no debounce)
*/
debounce?: number;
/**
* Controls whether URL updates create new browser history entries.
* If true (default), each update adds a new entry to the browser history.
* If false, updates replace the current URL without creating new history entries.
* @default true
*/
pushHistory?: boolean;
/**
* Enable lz-string compression for all parameters.
* When true, all parameters are compressed into a single parameter in the URL.
* This helps reduce URL length and provides basic parameter obfuscation.
* @default false
*/
compress?: boolean;
/**
* The name of the parameter used to store compressed data when compression is enabled.
* You can customize this to avoid conflicts with your schema parameters.
* For example, if your schema already uses '_data', you might want to use '_compressed' or another unique name.
* @default '_data'
*/
compressedParamName?: string;
/**
* Controls whether to update the URL when parameters change.
* If true (default), changes to parameters will update the URL.
* If false, parameters will only be stored in memory without updating the URL.
* Note: When false, compress option will be ignored.
* @default true
*/
updateURL?: boolean;
/**
* If true, the page will not scroll to the top when the URL is updated.
* This is useful if you want to maintain the user's scroll position during parameter changes.
* @default false
*/
noScroll?: boolean;
/**
* Specifies which date fields should use date-only format (YYYY-MM-DD) instead of full ISO8601 datetime.
*
* Map field names to their desired format:
* - 'date': Serializes as YYYY-MM-DD (e.g., "2025-10-21")
* - 'datetime': Serializes as full ISO8601 (e.g., "2025-10-21T18:18:14.196Z")
*
* Example:
* { dateFormats: { birthDate: 'date', createdAt: 'datetime' } }
*
* @default undefined (all dates use datetime format)
*/
dateFormats?: Record<string, "date" | "datetime">;
}
type ReturnUseSearchParams<Schema extends StandardSchemaV1> = {
[key: string]: any; // Typed, reactive params
/**
* Update multiple parameters at once
*
* This is more efficient than setting multiple parameters individually
* because it only triggers one URL update or one in-memory store update.
*
* @param values An object containing parameter key-value pairs to update.
* For example: params.update({ page: 1, sort: 'newest' })
*/
update(values: Partial<StandardSchemaV1.InferOutput<Schema>>): void;
/**
* Reset all parameters to their default values
*
* This method removes all current URL parameters or in-memory parameters
* and optionally sets parameters with non-default values back to their defaults.
*
* @param showDefaults Whether to show default values in the URL or in-memory store after reset.
* If not provided, uses the instance's showDefaults option.
*/
reset(showDefaults?: boolean): void;
/**
* Convert the current schema parameters to a URLSearchParams object
* This includes all values defined in the schema, regardless of their presence in the URL
* @returns URLSearchParams object containing all current parameter values
*/
toURLSearchParams(): URLSearchParams;
};
/**
* Schema type for createSearchParamsSchema
* Allows specifying more precise types for arrays, objects, and dates
*/
type SchemaTypeConfig<ArrayType = unknown, ObjectType = unknown> =
| { type: "string"; default?: string }
| { type: "number"; default?: number }
| { type: "boolean"; default?: boolean }
| { type: "array"; default?: ArrayType[]; arrayType?: ArrayType }
| { type: "object"; default?: ObjectType; objectType?: ObjectType }
| { type: "date"; default?: Date; dateFormat?: "date" | "datetime" };