VueJS

Build Advanced Components in Vue 3 using $attrs

Simone Cuomo
7 min read
Build Advanced Components in Vue 3 using $attrs
This article was written over 18 months ago and may contain information that is out of date. Some content may still be relevant, but please refer to official documentation for the latest information.

In the third major release of Vue js, we have seen many new features and improvements land on our remote working computers.

In this article, we are going to cover the $attrs attribute. We will explain what it is used for, how its implementation differs from Vue 2's (former $attrs, class, @listener), and build a code example to help understand its power.

Understanding this feature can really support your skills in developing easy to use and scalable components advanced components.

What is $attrs?

The definition of $attrs, varies between the two major versions of the framework, but in this article, we are going to mainly cover Vue 3, where @attrs can be seen as:

A component property that holds all of the attribute, property, and custom events that are not currently defined within the component.

$attrs can also be seen as a safety net, that captures anything that you may not have declared within a component. Let's consider a component that has just a single property and event handler, like the following example:

<template>
  <h1 @click="$emit('custom', 'change title')>{{title}}</h1>
</template>
<script>
export default {
  name: 'Example',
  props: ['title'],
  emits: ['custom'] //This is the new way to declare custom events in vue 3.
}
</script>

If we would instantiate our component like so:

<example 
  id="myId" 
  class="myClass" 
  data-cy="cypress-testing" 
  @blur="...." 
  title="this was expected"
  description="non declared props"
  @custom="..."
/>

Our example component would have access to a $attrs property with the following information:

$attrs = {
  id: "myId" 
  class: "myClass" 
  data-cy: "cypress-testing" 
  @blur: "...." 
  description: "non declared props"
}

If the above is not yet making sense, it is absolutely fine. In the next few sections, we will cover, in more granular details, how to actually make use of this feature.

$attrs V3 vs $attrs V2

Even if I do not want this article to be a comparison between V2 and V3, it is essential that we touch base on the differences that the $attrs feature offers betweent the two major versions.

If you have used VueJs in the past (version 2), there is a significant chance that you have already used $attrs before. The main reason is that almost all the attributes included in the $attrs property were already present in the previous version of the framework. Just split it into different properties.

If we take into consideration the example proposed above, the $attrs object is going to appear as follows:

vue 2 vs v3 $attrs

Main differences to notice in V2 are:

  • custom events go into a @listerner bucket
  • the class is not available (using class in this way requires you to set a property).

Most of the content provided in the following chapter can still be applied in V2, as long as we adhere to the above differences and define the extra properties ($listners and a class property).

Real Life example

As with most of my content, I like to always cover a real life example. Building something from the bottom up, can really help in understanding the reason beind a feature, and help you introduce its usage within your codebase.

In the following sections, we are going to build a nice slider (or more precisely a few of them). The complete code can be found on Stackblitz following the following link (stackblitz-vue-example).

Let's start from scratch

The first step requires us to create a simple component. This is going to be plain Vue and will have nothing to do with the $attrs feature (yet).

// Slider-1.vue

<template>
  <input 
    class="slider__input"
    type="range" 
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)" />
</template>

<script>
export default {
  name: "Slider",
  props: {
    modelValue: [Number, String]
  }
};
</script>

<style lang="scss">
  // The style of this slider have been taken from the W3School website. Link available here: https://www.w3schools.com/howto/howto_js_rangeslider.asp
  // See the Stackblitz link for the full CSS.
</style>

The above code will create a slider that includes single HTML element at the root, and a simple two way binding for a property called value.

To use the above component we would do something like this:

    <Slider-1 v-model="value" />

The result should be something like this: vue-attrs-1

Let's add some attributes

The above "hello world" example, would never stand the real web development industry.

As we know, our components are always full of requirements and specifications, and are never this simple. So to make it a bit more realistic, let's add a couple of attributes (min, max, class, id, data-cy, @keydown and aria-label).

    <Slider-1
      v-model="value"
      min="0"
      max="50"
      class="blue_slider"
      id="special_id"
      data-cy="cypress-slider"
      @keydown="() => true"
      aria-label="Example slider"
    />

If we would run the app with the above changes, we would see that all changes take effect. In fact, analysing the app will show the following UI and HTML:

vue-$attrs-2

As we can notice, all the information has already been applied to our HTML.

WAIT A SECOND... Why did I make such a big introduction to $attrs, when all the "non property/event" attributes are already automatically applied to the inner HTML element?

Do not worry, I have not wasted your time.. In the next section, we will shake things a bit, because as we know, requirements always change.. :)

Change request: Add a title and value

In this section we are going to apply some further changes to our component. More precisely, our product owner has assigned us the following ticket:

As a user of the slider, I would like to be able to see a title, and its value in numerical form being shown on screen.

The modified component will look like this:

// Slider-2.vue

<template>
  <div class="slider">
    <h1>{{ title }}</h1>
    <input 
      type="range" 
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
      class="slider__input" />
      <div>Value: {{ value }}</div>
  </div>
</template>

<script>
export default {
  name: "Slider-2",
  props: {
    modelValue: [Number, String]
    title: [String],
    value: [Number, String],
  }
};
</script>

vue-attrs-3

At first glance, everthing seemed to work, but if we look closely, we can see that something is not right.

First, the slider is not blue. Second, the value is going way over 50, and lastly if we look at the html, we'll notice that all of our extra attributes (min, max, data-cy) are assigned to the root element, and not our input element anymore!

The best way to solve the above problem would be to find a way to "apply" all the properties, classes, arguments, and events directly to the input field, without them needing to manually declare them- something like a "bucket" of data.. !$ATTRS!

Let's jump in the next section and see how we can use $atts to accomplish our goals.

$attrs to the rescue

At the start of this article, we introduced $attrs as a bucket of information. It is a place that holds all the "undeclared" properties and events, and this is precisely what we need to solve our issue.

To use this feature, we can just apply the $attrs property to one or more HTML element, using the v-bind operator:

<template>
  <div class="slider">
    <h1>{{ title }}</h1>
    <input 
      type="range"  
      v-bind="$attrs"
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
      class="slider__input" />
      <div>Value: {{ modelValue }}</div>
  </div>
</template>

As we can see, the above change will make things much better. The use of attrs in our component will act as bridge that copies all out attributes (class, attribute, property and custom events) to one or more elements:

vue-attrs-4

The slider thumb is back to being blue. The max value is set to 100, and our extra attributes are set correctly... almost.

There is only one problem- our extra attributes have not only been assigned to the input element, but also to the root element!

In this case, there is no visual change to show us this issue (and usually there isn't one in real life either. That is why I have not created any). But these extra variables can really create some side effects. Let's fix this.

inheritAttrs: false

By default, any extra argument being passed to a component is automatically applied to the root element (and to all elements that have the $attrs binding).

To switch this feature off, and get control of what elements receive this extra attribute, we can use a flag called inheritAttrs, and set it to false.

Our script tag will look like this:

export default {
  name: "Slider",
  inheritAttrs: false,
  props: {
    title: [String],
    modelValue: [Number, String],
  }
};

After this change, our HTML is nice and clean. All the extra properties are applied to the Input element only.

Conclusion

Before we wrap up, there are a couple of extra points that I want to share, so that you can make furhter use of this feature and understand it more deeply.

  1. I prefer not to use the v-model so that I can actually also omit the update:modelValue event (you can see at the file name Slider.vue in the stackblitz code)
  2. You can access, and play with the individual properties of the $attrs. So for example, you could apply the $attrs.class to one element, and the $attrs['aria-label]' to another.
  3. It is always best to declare property and events, and just use this when you have an element that emits a native event and/or accepts many attributes (like video tag, or all Input fields).

Time to say goodbye

I have personally taken some time to fully understand this feature, but I really hope that this article may help you in understanding this feature, and helps you define complex but very readable components.

About the author

Simone Cuomo

Simone Cuomo

VP of Delivery

Software Architect, Mentor, Speaker and Writer. Passionate about all aspect of web development, with a special love for VueJs

Keep reading

View all posts →

Awesome 3D experience with VueJS and TresJS: a beginner's guide

Unleash the power of 3D in your Vue.js projects with Tres.js! The future of immersive web experiences is here. #Vuejs #3Ddevelopment...

Mattia Magi5 mins
Vue3D

3 VueJS Component Libraries Perfect for Beginners

For developers checking out VueJS for the first time, the initial steps are overwhelming, particularly when setting up projects from square one. But don’t worry! The VueJS ecosystem offers a plethora of remarkable component libraries, easing this early obstacle. These three libraries are pre built toolkits, providing beginners with the means to kickstart their VueJS projects effortlessly. Let’s take a look! Quasar Quasar is among the most popular open source component libraries for Vue.js, offering a comprehensive set of ready to use UI components and tools for building responsive web applications and websites. Designed with performance, flexibility, and ease of use in mind, Quasar provides developers with a wide range of customizable components, such as buttons, forms, dialogs, and layouts, along with built in support for themes, internationalization, and accessibility. With its extensive documentation, active community support, and seamless integration with Vue CLI and Vuex, Quasar empowers developers to rapidly prototype and develop high quality Vue.js applications for various platforms, including desktop, mobile, and PWA (Progressive Web Apps). PrimeVue PrimeVue is a popular Vue.js component library offering a wide range of customizable UI components designed for modern web applications. Developed by PrimeTek, it follows Material Design guidelines, ensuring responsiveness and accessibility across devices. With features like theming, internationalization, and advanced functionalities such as lazy loading and drag and drop, PrimeVue provides developers with the tools to create elegant and high performing Vue.js applications efficiently. Supported by clear documentation, demos, and an active community, PrimeVue is an excellent choice for developers seeking to streamline their development process and deliver polished user experiences. Vuetify Vuetify is a powerful Vue.js component library that empowers developers to create elegant and responsive user interfaces with ease. Built according to Google's Material Design guidelines, Vuetify offers a vast collection of customizable UI components, ranging from buttons and cards to navigation bars and data tables. Its comprehensive set of features includes themes, typography, layout grids, and advanced components like dialogues and sliders, enabling developers to quickly build modern web applications that look and feel polished. With extensive documentation, active community support, and ongoing development, Vuetify remains a top choice for Vue.js developers seeking to streamline their workflow and deliver visually stunning user experiences. For newcomers venturing into Vue.js, the initial setup might seem daunting. Thankfully, Vue.js offers a variety of component libraries to simplify this process. Quasar, PrimeVue, and Vuetify are standout options, each providing pre built tools to kickstart projects smoothly. Whether you prefer Quasar's extensive UI components, PrimeVue's Material Design inspired features, or Vuetify's responsive interfaces, these libraries cater to diverse preferences and project requirements. With their clear documentation and active communities, these libraries empower developers to start Vue.js projects confidently and efficiently, enabling Vue developers to create polished user experiences....

2 mins
VuetifyVueQuasar

Understanding Vue.js's <Suspense> and Async Components

In this blog post, we will delve into how <Suspense> and async components work, their benefits, and practical implementation strategies to make your Vue.js applications more efficient and user-friendly...

Jesús Padrón6 mins
VueWeb PerformanceUXJavaScript

Getting started with Vitepress

Create your static blog site using Vitepress. Setup and configure your site in minutes by following this step-by-step tutorial....

Simone Cuomo12 mins
VueViteVitepress