Best VueUse Composables

VueUse has a massive collection of fantastic composables.

But with so many, it can be overwhelming to look through them all.

That’s why we’ve put together this list, to showcase some of the best Vue composables that deserve some extra attention. We’ll cover what each composable does and why it’s useful. I’ve also included a live demo for each, to show them in action.

Here are the VueUse composables we’ll be covering in this article:

  1. onClickOutside
  2. useFocusTrap
  3. useHead
  4. useStorage
  5. useVModel
  6. useImage
  7. useDark

1. onClickOutside

Demo of VueUse onClickOutside composable

Detecting a click is pretty straightforward. But detecting when a click happens outside of an element? That’s a little trickier. Unless, of course, you’re using the onClickOutside composable from VueUse.

This is what it looks like:

<script setup>
import { ref } from 'vue'
import { onClickOutside } from '@vueuse/core'

const container = ref(null)
onClickOutside(container, () => alert('Good. Better to click outside.'))
</script>

<template>
  <div>
    <p>Hey there, here's some text.</p>
    <div class="container" ref="container">
      <p>Please don't click in here.</p>
    </div>
  </div>
</template>

We create a ref for the container element that we want to track:

const container = ref(null);

Then we turn that into a template ref with the ref attribute on the element:

<div class="container" ref="container">
  <p>Please don't click in here.</p>
</div>

Now that we have our container, we pass it to the onClickOutside composable along with a handler:

onClickOutside(
  container,
  () => alert('Good. Better to click outside.')
)

This composable is useful for managing windows or dropdowns. When the user clicks outside of the dropdown menu you can close it.

Modals also typically show this behaviour.

Check out the docs for onClickOutside here.

Go here to see the demo in action.


2. useFocusTrap

Demo of VueUse useFocusTrap composable

Managing focus properly is important in order to have accessible applications.

There’s nothing worse than accidentally tabbing behind a modal, and being unable to get the focus back into the modal. That’s exactly where focus traps come in.

They lock the keyboard focus to a specific DOM element. Instead of looping through the entire page, then the browser itself, your keyboard focus only loops through that DOM element.

Here’s an example of using the useFocusTrap from VueUse:

<script setup>
import { ref } from 'vue'
import { useFocusTrap } from '@vueuse/integrations/useFocusTrap'

const container = ref(null)
useFocusTrap(container, { immediate: true })
</script>

<template>
  <div>
    <button tab-index="-1">Can't click me</button>
    <div class="container" ref="container">
      <button tab-index="-1">Inside the trap</button>
      <button tab-index="-1">Can't break out</button>
      <button tab-index="-1">Stuck here forever</button>
    </div>
    <button tab-index="-1">Can't click me</button>
  </div>
</template>

With immediate set to true, the focus will be placed inside the container element as soon as the page loads. And then it’s impossible to tab outside of that container.

Once you reach the third button, hitting tab again brings you back to the first button.

Just like with onClickOutside, we first set up our template ref for the container:

const container = ref(null)
<div class="container" ref="container">
  <button tab-index="-1">Inside the trap</button>
  <button tab-index="-1">Can't break out</button>
  <button tab-index="-1">Stuck here forever</button>
</div>

Then we pass this template ref to the useFocusTrap composable:

useFocusTrap(container, { immediate: true });

The immediate option will automatically set focus to the first focusable element inside the container.

  • must install focus-trap because this wraps that
  • focus traps can be nested, automatically handles pauses and unpauses

Check out the docs for useFocusTrap here.

Go here to see the demo in action.


3. useHead

Demo of VueUse useHead composable

VueUse gives us an easy way to update the head section of our app — the page title, scripts, and other things that might go in there.

The useHead composable requires us to set up a plugin first:

import { createApp } from 'vue'
import { createHead } from '@vueuse/head'
import App from './App.vue'

const app = createApp(App)
const head = createHead()

app.use(head)
app.mount('#app')

Once we’re using the plugin, we can update the head section however we want. In this example, we’ll inject some custom styles on a button press:

<script setup>
import { ref } from 'vue'
import { useHead } from '@vueuse/head'

const styles = ref('')
useHead({
  // Inject a style tag into the head
  style: [{ children: styles }],
})

const injectStyles = () => {
  styles.value = 'button { background: red }'
}
</script>

<template>
  <div>
    <button @click="injectStyles">Inject new styles</button>
  </div>
</template>

First, we create a ref to represent the styles we’ll be injecting. We’ll leave this empty for now:

const styles = ref('');

Second, we’ll set up useHead to inject the styles into the page:

useHead({
  // Inject a style tag into the head
  style: [{ children: styles }],
})

Then we add the method that will inject these styles:

const injectStyles = () => {
  styles.value = 'button { background: red }'
}

All we’re doing here is updating the value of our styles ref. But reactivity is wonderful, and useHead will automatically update the styles that are injected every time this value changes.

Of course, we aren’t limited to injecting styles. We can add any of these to our <head>:

  • title
  • meta tags
  • link tags
  • base tag
  • style tags
  • script tags
  • html attributes
  • body attributes

Check out the docs for useHead here.

Go see the demo in action here.


4. useStorage

The useStorage composable is really cool, because it will automatically sync your ref to local storage.

Here’s a basic example:

<script setup>
import { useStorage } from '@vueuse/core'
const input = useStorage('unique-key', 'Hello, world!')
</script>

<template>
  <div>
    <input v-model="input" />
  </div>
</template>

When you first load the app, the input will display “Hello, world!”. But after that it will show the last thing that you typed into it, because it’s saved to local storage.

This composable also works with session storage:

const input = useStorage('unique-key', 'Hello, world!', sessionStorage)

You can actually provide any storage system you want, as long as it implements the StorageLike interface:

export interface StorageLike {
  getItem(key: string): string | null
  setItem(key: string, value: string): void
  removeItem(key: string): void
}

Check out the docs for useStorage here.

Go see the demo in action here.


5. useVModel

The v-model directive is some nice syntactic sugar that makes two-way data binding easier.

But the useVModel composable goes a step further, and gets rid of a bunch of boiler-plate code that no one really wants to write:

<script setup>
import { useVModel } from '@vueuse/core'

const props = defineProps({
  count: Number,
})
const emit = defineEmits(['update:count'])

const count = useVModel(props, 'count', emit)
</script>

<template>
  <div>
    <button @click="count = count - 1">-</button>
    <button @click="count = 0">Reset to 0</button>
    <button @click="count = count + 1">+</button>
  </div>
</template>

In this example, we first define the prop we want to attach to the v-model:

const props = defineProps({
  count: Number,
})

Then we emit an event that uses the v-model naming convention of update:<propName>:

const emit = defineEmits(['update:count'])

Now we can use the useVModel composable to bind the prop and event to a ref:

const count = useVModel(props, 'count', emit)

This count ref will change whenever the prop changes. But whenever it’s changed from this component, it will emit the update:count event to trigger the update through the v-model directive.

We can use this Input component like this:

<script setup>
import { ref } from 'vue'
import Input from './components/Input.vue'

const count = ref(50)
</script>

<template>
  <div>
    <Input v-model:count="count" />
    {{ count }}
  </div>
</template>

The count ref here is synced to the count ref inside of the Input component through the v-model binding.

Check out the docs for useVModel here.

Go see the demo in action here.


6. useImage

Demo of VueUse useImage composable

Images in web apps are getting a lot fancier over time. We’ve got responsive images with srcset, progressive loading libraries, and libraries that will only load an image once it scrolls into the viewport.

But did you know we can also access loading and error states on the image itself?

I’ve done this before by listening to the onload and onerror events that every HTML element emits, but VueUse gives us a simpler way with the useImage composable:

<script setup>
import { useImage } from '@vueuse/core'

// Change this to a non-existent URL to see the error state
const url = 'https://source.unsplash.com/random/400x300'
const { isLoading, error } = useImage(
  {
    src: url,
  },
  {
    // Just to show the loading effect more clearly
    delay: 2000,
  }
)
</script>

<template>
  <div>
    <div v-if="isLoading" class="loading gradient"></div>
    <div v-else-if="error">Couldn't load the image :(</div>
    <img v-else :src="url" />
  </div>
</template>

First, we set up the composable with our image URL:

const { isLoading, error } = useImage({ src: url })

We’ll grab the isLoading and error refs that it returns so we can track the state. This composable uses useAsyncState internally, so the values it returns are the same as that composable.

Once we do that, useImage loads our image and attaches event handlers to it.

All we need to do is use that image in our template by using the same URL. Since the browser will reuse any cached image, it will reuse the one loaded by useImage:

<template>
  <div>
    <div v-if="isLoading" class="loading gradient"></div>
    <div v-else-if="error">Couldn't load the image :(</div>
    <img v-else :src="url" />
  </div>
</template>

Here, we set up a basic loading and error state handler. While the image is loading we show a placeholder with an animated gradient. If there’s an error, we display an error message. Otherwise we can render the image.

I want to re-iterate this point though: because the image is already loaded by useImage, as soon as that img tag is added to the DOM, it will render the image.

This composable has some other great features, too! If you want to make it a responsive image, it has support for the srcset and sizes attributes, which are passed along to the img element behind the scenes.

There is also a renderless component if you prefer to keep everything inside of your template. It works in the same as the composable does:

<template>
	<UseImage src="https://source.unsplash.com/random/401x301">
    <template #loading>
			<div class="loading gradient"></div>
		</template>
    <template #error>
			Oops!
		</template>
  </UseImage>
</template>

Check out the docs for useImage here.

Go see the demo in action here.


7. Dark mode with useDark

Demo of VueUse useDark composable

Every website and app seems to have dark mode these days.

The hard part is the styling changes. But once you’ve got that, toggling back and forth is pretty straightforward.

If you’re using Tailwind, you only need to add the dark class to the html element to enable it for the whole page:

<html class="dark"><!-- ... --></html>

However, there are a few things to consider when toggling between dark mode and light mode. First, we want to take the user’s system settings into account. Second, we want to remember if they’ve overridden that choice.

The useDark composable from VueUse wraps up all of these things together for us. By default it looks to the system settings, but any changes are persisted to localStorage so the settings are remembered:

<script setup>
import { useDark, useToggle } from '@vueuse/core'

const isDark = useDark()
const toggleDark = useToggle(isDark)
</script>

<template>
  <div class="container">
    Changes with dark/light mode.

    <button @click="toggleDark()">
			Enable {{ isDark ? 'Light' : 'Dark' }} Mode
		</button>
  </div>
</template>

We’ll also include these dark mode styles:

.dark .container {
  background: slategrey;
  color: white;
  border-color: black;
}

.dark button {
  background: lightgrey;
  color: black;
}

.dark body {
  background: darkgrey;
}

If you’re not using Tailwind, you can completely customize how dark mode is applied by passing in an options object. Here is what the default Tailwind would look like:

const isDark = useDark({
  selector: 'html',
  attribute: 'class',
  valueDark: 'dark',
  valueLight: '',
})

You can also provide an onChanged handler so you can write whatever Javascript you need. These two methods allow you to make it work with whatever styling system you already have.

Check out the docs for useDark here.

Go see the demo in action here.


Wrapping Up

VueUse has an immense library of fantastic composables, and we only covered a fraction of them here. I highly recommend taking some time to explore the docs and see all of the things that are available. It’s a fantastic resource, and will save you from lots of boiler-plate code and re-inventing the wheel constantly. And while VueUse is a great library, there are plenty of times you’ll want to create your own composables. Do you know how? We walk you through it in Vue Mastery’s Coding Better Composables course.

Download the cheatsheets

Save time and energy with our cheat sheets, that give you all the essential syntax at your fingertips.

  • Vue 3 Composition API
  • Vue Essentials
  • Nuxt
  • Pinia
  • Vue Router
  • Vue 3 Migration Guide