Type-safe search params state for Vue 3 and Nuxt. A port of nuqs
that uses Vue idioms: writable refs, v-model, reactive objects and a plugin.
📖 Documentation: https://iamsomraj.github.io/vue-qs/
⭐ Listed in Awesome Vue under State Management.
const page = useQueryState('page', parseAsInteger.withDefault(1));
page.value++; // ?page=2Filters, search terms, tabs, pagination and sort orders are state your users expect to share, bookmark and restore with the Back button. Storing that state in the URL makes it all work, but parsing and serializing query strings by hand is tedious and error-prone.
vue-qs gives you URL state that feels like regular Vue state: a ref you can bind with
v-model, typed by parsers, and kept in sync with the router. It follows
nuqs, the popular React library, so its docs, patterns and ideas apply
here too.
- 🧩 The nuqs API: the same parsers, options and behaviour, adapted to Vue
- 🔒 Type-safe: typed state from parsers, with
nullremoved by.withDefault() - ✍️ Writable refs: assign
.valueor usev-model;.set()for options and promises - 📦 Batched updates: changes in the same tick produce a single URL update
- ⏱️ Throttle & debounce: state is instant, URL writes are rate-limited
- 🧭 Adapters: Vue Router, Nuxt module, History API, testing, custom
- 🖥️ Server utilities:
createLoaderandcreateSerializershare your parsers - 🧪 Testable: an in-memory testing adapter
npm install vue-qsRequires Vue 3.3+. vue-router (4.2+ or 5) is an optional peer dependency.
Install the plugin with an adapter:
import { createApp } from 'vue';
import { createVueQs } from 'vue-qs';
import { createVueRouterAdapter } from 'vue-qs/adapters/vue-router';
// Without a router: import { createHistoryAdapter } from 'vue-qs/adapters/history'
createApp(App)
.use(router)
.use(createVueQs({ adapter: createVueRouterAdapter(router) }))
.mount('#app');For Nuxt, add the module, which also auto-imports the composables and parsers:
// nuxt.config.ts
export default defineNuxtConfig({ modules: ['vue-qs/nuxt'] });See the adapters guide for options.
<script setup lang="ts">
import { parseAsInteger, useQueryState } from 'vue-qs';
const name = useQueryState('name'); // Ref<string | null>
const count = useQueryState('count', parseAsInteger.withDefault(0)); // Ref<number>
</script>
<template>
<input v-model="name" placeholder="Your name" />
<button @click="count++">Count: {{ count }}</button>
<button @click="count = 0">Reset</button>
<!-- the default clears ?count -->
</template>await count.set((c) => c + 1, { history: 'push', scroll: true });
await count.set(null); // removes ?count, count.value === 0import { toRefs } from 'vue';
import { parseAsFloat, useQueryStates } from 'vue-qs';
const coordinates = useQueryStates(
{ lat: parseAsFloat.withDefault(0), lng: parseAsFloat.withDefault(0) },
{ urlKeys: { lat: 'y', lng: 'x' } }
);
coordinates.lat = 48.85; // ?y=48.85
await coordinates.$set({ lat: 1, lng: 2 }); // one URL update
const { lat, lng } = toRefs(coordinates); // refs for v-modelparseAsString, parseAsInteger, parseAsFloat, parseAsHex, parseAsIndex,
parseAsBoolean, parseAsTimestamp, parseAsIsoDateTime, parseAsIsoDate,
parseAsStringEnum, parseAsStringLiteral, parseAsNumberLiteral, parseAsArrayOf,
parseAsNativeArrayOf, parseAsJson (with a validator or Standard Schema), and
createParser for your own. See the parsers guide.
| Option | Default | Description |
|---|---|---|
history |
'replace' |
'push' creates a history entry |
scroll |
false |
Scroll to top after the update |
clearOnDefault |
true |
Remove the key when the value equals the default |
limitUrlUpdates |
throttle(50) |
throttle(ms) or debounce(ms) |
shallow |
true |
Forwarded to custom adapters |
See the options guide.
import { createLoader, createSerializer, parseAsInteger, parseAsString } from 'vue-qs/server';
const searchParams = { q: parseAsString.withDefault(''), page: parseAsInteger.withDefault(1) };
const load = createLoader(searchParams);
load(getQuery(event)); // { q: string; page: number }
const serialize = createSerializer(searchParams);
serialize('/search', { q: 'vue', page: 2 }); // '/search?q=vue&page=2'import { createVueQs } from 'vue-qs';
import { createTestingAdapter } from 'vue-qs/adapters/testing';
const onUrlUpdate = vi.fn();
mount(Component, {
global: {
plugins: [
createVueQs({ adapter: createTestingAdapter({ searchParams: '?count=1', onUrlUpdate }) }),
],
},
});vue-qs/testing provides isParserBijective, testParseThenSerialize and
testSerializeThenParse to unit-test custom parsers. See the
testing guide.
0.2 replaces queryRef / queryReactive and codecs with useQueryState /
useQueryStates and parsers. See the
migration guide.
vue-qs is an open-source project, and feedback shapes where it goes next.
- 💬 Questions: ask in Q&A
- 💡 Ideas and API feedback: share them in Ideas
- 🙌 Using vue-qs? Show it in Show and tell
- 🐛 Bugs: open an issue with a minimal reproduction
- 🛠️ Contributing: start with a good first issue and read the contributing guide
Everyone taking part is expected to follow the Code of Conduct.
If vue-qs is useful to you, a ⭐ on GitHub helps other Vue developers find it.
vue-qs is a port of nuqs by François Best.
MIT © Somraj Mukherjee

