In this tutorial, we'll look at how to create checkbox and checkbox group components in Vue.js.
In Vue, UI components can be developed using the Options API or the newer Composition API.
The Options API uses options such as data, methods, and computed to define the UI component. The Composition API, on the other hand, comes with a set of APIs that allow us to build components using imported functions instead of declaring options. In this tutorial, we'll be using the Composition API.
We'll also use the SFC (Single File Components) approach for component development.
An SFC file has three parts: the script, the template, and the style. The script section is where all the JavaScript code goes. The HTML code will be put in the template section. In the style section, the styles for the HTML will be written.
Here is the typical structure of an SFC file:
1<script setup>
2// javascript code
3</script>
4
5<template>
6 <!-- html code -->
7 <div>Welcome to SFC</div>
8</template>
9
10<style lang="css">
11 /* css code */
12</style>
13
As you can see, all of the UI code is contained in one place. This eliminates the need to switch between files frequently while developing UI components.
Let’s get started by creating a checkbox.vue file and defining some props under the script section.
1const props = defineProps({
2 value: {
3 type: String,
4 default: "",
5 },
6 label: {
7 type: String,
8 default: "",
9 },
10 checked: {
11 type: Boolean,
12 default: false,
13 },
14 id: {
15 type: String,
16 default: "",
17 },
18 disabled: {
19 type: Boolean,
20 default: false,
21 },
22});
23
24
We've set up five properties:
- value - value of the checkbox
- label - label for the checkbox
- checked-boolean checked state of the checkbox
- id-unique id of the checkbox. We’ll generate unique IDs with the nanoid library.
- disabled-boolean disabled state of the checkbox
defineProps returns a props object, which can be used to access all of the props in the SFC file's script and template sections.
Next, we define emits using defineEmits. Emits are used to define custom events in Vue. A parent or host will usually listen to the event and take in the data that the child provides. In our case, the checkbox group is the parent component that will listen to this event.
1
2const emit = defineEmits({
3 onChange: {
4 type: "change",
5 default: () => {},
6 },
7});
8
The next step is to create two computed properties. Most of the time, computed properties are used to calculate values when any reactive dependencies change.In our case, the computed properties will monitor the checked props and generate class names accordingly.
The first computed property (wrapperClass) is applied to the checkbox wrapper element.
1const wrapperClass = computed(() => {
2 const { checked } = props;
3 return {
4 "check-box": true,
5 "check-box--checked": checked,
6 };
7});
8
When the checked state is true, the string "check-box–checked" is included in the wrapperClass property.
Before we set up the next computed property, let's quickly see how to use SVG icons, as we'll need two different icons to represent the checked and unchecked states. If you're developing with vite, you can use the vite-svg-loader library to use SVG files directly in your project.
Here are some examples of how to use vite-svg-loader to load SVG files in vue.
After setting up the icon configuration, let's write a computed property that uses the checked prop to show the checked or unchecked icon.
1
2const iconClass = computed(() => {
3 const { checked } = props;
4 return {
5 "check-box__icon": true,
6 "check-box__icon--checked": checked,
7 };
8});
9
Now that we have the computed properties ready, it's time to set up an event handler for the click event.
1const handleClick = () => {
2 emit("onChange", props.id);
3};
4
When the checkbox is clicked, the handler emits the onChange event that we denied earlier via the definEmits and passes the checkbox's id.
In a short while, we will see how to capture this event in the parent component and execute some action based on the id passed. But first, some HTML.
Now that we've set up the properties and event handlers, let's go ahead and create some basic HTML.
In an SFC file, all HTML code should be wrapped in a template tag. Let's take a closer look at how the parent div will look.
1<template>
2 <div
3 :class="wrapperClass"
4 tabindex="0"
5 role="checkbox"
6 :aria-labelledby="`label-${props.id}`"
7 :aria-checked="props.checked"
8 @click="handleClick"
9 >
10 . . .
11 </div>
12</template>
13
14
We've defined a number of attributes for the div, so let's look at what each one means.
class: Class names are attached to HTML elements using the class attribute (note, unlike className in React). The ":" in front of the class indicates that the value of the class will be bound to a property defined in the script section. In our case, the class is bound to the computed property wrapperClass that we created earlier.
tabindex: tabindex is an HTML property that allows you to focus on an HTML element using the keyboard.
role: The role attribute is used to describe the type of HTML element. This is mostly used by screen readers, and in our case, we set the value to checkbox.
aria-checked: The ARIA attribute aria-checked describes the current state of the checkbox.(ARIA attributes are HTML attributes that make the user interface more accessible, particularly for screen reader users.) The value has been bound to the checked prop in this case. Every time the state of the checkbox changes, the value of this attribute changes to true or false.
Let's take a look at the div's contents. Within the div, we will show two elements.
- An Icon to signify the state of the checkbox
- A Label for the checkbox
Let’s run through them.
1
2<span :class="iconClass">
3 <Square v-if="!props.checked" />
4 <CheckedSquare v-if="props.checked" />
5</span>
6
7
Here, the span element's class property is bound to the iconClass computed property we defined earlier.
As previously stated, we'll use two SVG icons to represent the checked and unchecked states.
- Square Icon: Unchecked state
- Checked Square: Checked state
The tutorial makes use of SVG icons from the Feathers icon set. You can choose icons from the same library or from your favorite icon set.
v-if is a Vue.js directive that conditionally renders an HTML element based on a value or prop. When the checkedpropis true, we display the CheckedSquare icon, and when it is false, we show the Square icon.
We use a span to display the label. The id for the span is generated on the fly using the id prop. This ID is important since it will be how the parent group component knows which boxes are checked.
1
2<span
3 :id="`label-${props.id}`"
4 class="label"
5>
6 {{ props.label }}
7</span>
8
9
This is how the entire template should look.
1
2<template>
3 <div
4 :class="wrapperClass"
5 tabindex="0"
6 role="checkbox"
7 :aria-checked="props.checked"
8 @click="handleClick"
9 >
10 <span :class="iconClass">
11 <Square v-if="!props.checked" />
12 <CheckedSquare v-if="props.checked" />
13 </span>
14 <span
15 :id="`label-${props.id}`"
16 class="label"
17 >
18 {{ props.label }}
19 </span>
20 </div>
21</template>
22
23
Finally, let's add some style to the checkbox to make it look nicer.
1<style scoped lang="css">
2.check-box {
3 align-items: center;
4 border: 1px solid transparent;
5 cursor: pointer;
6 display: flex;
7 justify-content: flex-start;
8 padding: 0.5rem;
9 user-select: none;
10}
11
12.label {
13 padding-left: 0.5rem;
14}
15
16.check-box__icon {
17 display: block;
18 height: 1rem;
19 width: 1rem;
20
21 svg {
22 height: 100%;
23 width: 100%;
24 }
25}
26</style>
27
28
Styles are scoped locally, and you don’t have to worry about class names colliding.
We can quickly test the checkbox component that we created with the following code.
1
2<script setup>
3import CheckBoxGroup from "check-box.vue";
4</script>
5
6<template>
7 <Checkbox
8 id="123"
9 label="Option 1"
10 value="option1"
11 />
12</template>
13
14
If all goes well, you should have the checkbox rendered.
Beyond using a single checkbox in your form, you may want to present your users with a list of options and allow them to select a subset of those options. It's called a checkbox group in UI/UX terms, and that's what we'll build next.
Let's start by defining the properties, emits, and refs for the checkbox group.
We will define a single prop called “items” of type Array. The items prop represents a collection of checkboxes. Each item contains the id, name, value, and checked state of the checkbox.
1const props = defineProps({
2 items: {
3 type: Array,
4 default: () => [],
5 },
6});
7
Next, we need to use defineEmits to set up the onChange event. This will be used to send out a change event whenever one of the checkboxes changes its state.
1const emit = defineEmits({
2 onChange: {
3 type: "change",
4 default: () => {},
5 },
6});
7
Refs are reactive data sources. Whenever the ref data is updated, the view bound to the ref is automatically re-rendered to reflect the new data state.
The checkbox collection passed via the items prop is transformed and stored in itemsRef.
1const itemsRef = ref(
2 props.items.map((item) => {
3 return {
4 ...item,
5 id: nanoid(),
6 };
7 })
8);
9
While storing the array in its initial state, we also include a unique id for every item with the help of nanoid.
The checkbox group should process the change events originating from each checkbox and update the state accordingly. For this purpose, we need an event handler.
1const handleOnChange = (id) => {
2 const newValue = itemsRef.value.map((item) => ({
3 ...item,
4 checked: item.id === id ? !item.checked : item.checked,
5 }));
6 itemsRef.value = newValue;
7 emit("onChange", newValue);
8};
9
The event handler takes an id as a parameter and toggles the checkbox's state.
The next line from the above snippet takes the data from the checkboxes and turns it into a new array with the checked state turned on for the item with the matching id.
1const newValue = itemsRef.value.map((item) => ({
2 ...item,
3 checked: item.id === id ? !item.checked : item.checked,
4}));
5
The reactive data source is then updated with the following snippet.
1itemsRef.value = newValue;
2
Finally, we emit the onChange event with the updated array.
1emit("onChange", newValue);
2
Next, we need to create the HTML for the checkbox group.
1<template>
2 <div class="checkbox-group-wrapper">
3 <Checkbox
4 v-for="item in itemsRef"
5 :id="item.id"
6 :key="item.id"
7 :label="item.label"
8 :value="item.value"
9 :checked="item.checked"
10 @on-change="handleOnChange"
11 />
12 </div>
13</template>
14
In the above code, the outer div serves as a wrapper for all the checkboxes.
The v-for directive is a Vue.js directive that is used to render collections. In our case, we're rendering a list of checkboxes. Along with that, we also need to pass down the properties of each checkbox item.
The following lines pass the properties down to the checkbox component.
1:id="item.id"
2:key="item.id"
3:label="item.label"
4:value="item.value"
5:checked="item.checked"
6
The properties of the checkbox component are id, label, value, and checked. The additional key property is important for rendering lists efficiently, and the value for it should be unique. For this reason, “id” is passed as the value for :key.
The main purpose of the key property is to help Vue's virtual DOM algorithm find vnodes when comparing the new list of nodes to the old list. Vue uses these keys to figure out which HTML elements it needs to remove, update, or add to the list.
Finally, we wire up the handleOnChange with the following code.
1 @on-change="handleOnChange"
2
Whenever a change event is emitted by any of the checkbox components, the handleOnChange method gets invoked and sets the state of the checkboxes appropriately.
Now that we have both the checkbox and checkbox-group ready, let's see how the finished code for both the checkbox and checkbox-group looks.
checkbox.vue
1//checkbox.vue
2<script setup>
3import { computed } from "vue";
4import CheckedSquare from "../icons/check-square.svg?component";
5import Square from "../icons/square.svg?component";
6
7const emit = defineEmits({
8 onChange: {
9 type: "change",
10 default: () => {},
11 },
12});
13
14const props = defineProps({
15 value: {
16 type: String,
17 default: "",
18 },
19 label: {
20 type: String,
21 default: "",
22 },
23 checked: {
24 type: Boolean,
25 default: false,
26 },
27 id: {
28 type: String,
29 default: "",
30 },
31 disabled: {
32 type: Boolean,
33 default: false,
34 },
35});
36
37const wrapperClass = computed(() => {
38 const { checked } = props;
39 return {
40 "check-box": true,
41 "check-box--checked": checked,
42 };
43});
44
45const iconClass = computed(() => {
46 const { checked } = props;
47 return {
48 "check-box__icon": true,
49 "check-box__icon--checked": checked,
50 };
51});
52
53const handleClick = () => {
54 emit("onChange", props.id);
55};
56</script>
57
58<template>
59 <div
60 :class="wrapperClass"
61 tabindex="0"
62 role="checkbox"
63 :aria-checked="props.checked"
64 @click="handleClick"
65 >
66 <span :class="iconClass">
67 <Square v-if="!props.checked" />
68 <CheckedSquare v-if="props.checked" />
69 </span>
70 <span
71 :id="`label-${props.id}`"
72 class="label"
73 >
74 {{ props.label }}
75 </span>
76 </div>
77</template>
78
79<style scoped lang="css">
80.check-box {
81 align-items: center;
82 border: 1px solid transparent;
83 cursor: pointer;
84 display: flex;
85 justify-content: flex-start;
86 padding: 0.5rem;
87 user-select: none;
88}
89
90.label {
91 padding-left: 0.5rem;
92}
93
94.check-box__icon {
95 display: block;
96 height: 1rem;
97 width: 1rem;
98
99 svg {
100 height: 100%;
101 width: 100%;
102 }
103}
104</style>
105
checkbox-group.vue
1<script setup>
2import { nanoid } from "nanoid";
3import { ref } from "vue";
4import Checkbox from "./check-box.vue";
5
6const props = defineProps({
7 items: {
8 type: Array,
9 default: () => [],
10 },
11});
12
13const emit = defineEmits({
14 onChange: {
15 type: "change",
16 default: () => {},
17 },
18});
19
20const itemsRef = ref(
21 props.items.map((item) => {
22 return {
23 ...item,
24 id: nanoid(),
25 };
26 })
27);
28
29const handleOnChange = (id) => {
30 const newValue = itemsRef.value.map((item) => ({
31 ...item,
32 checked: item.id === id ? !item.checked : item.checked,
33 }));
34 itemsRef.value = newValue;
35 emit("onChange", newValue);
36};
37</script>
38
39<template>
40 <div class="checkbox-group-wrapper">
41 <Checkbox
42 v-for="item in itemsRef"
43 :id="item.id"
44 :key="item.id"
45 :label="item.label"
46 :value="item.value"
47 :checked="item.checked"
48 @on-change="handleOnChange"
49 />
50 </div>
51</template>
52
53<style lang="css">
54.checkbox-group-wrapper {
55 padding: 0.5rem;
56}
57</style>
58
Now that we've created the checkbox group, let's put it to use.
1<script setup>
2import CheckBoxGroup from "./components/check-box-group.vue";
3const onChange = (val) => {
4 console.log(val);
5};
6</script>
7
8<template>
9 <CheckBoxGroup
10 :items="[
11 {
12 label: 'Option 1',
13 value: '1',
14 },
15 {
16 label: 'Option 2',
17 value: '2',
18 },
19 {
20 label: 'Option 3',
21 value: '3',
22 },
23 ]"
24 @on-change="onChange"
25 />
26</template>
27
In this code, we have imported the checkbox group within the script tag and used it under the template section like any other HTML tag. An array of options is passed to the component via the items prop.
If all goes well, then you should see the checkbox group rendered.
For reference and learning, the entire Vue project is available on codesandbox.
In this tutorial, we used Vue's new composition API to build a checkbox and a checkbox group. Along the way, we learned how to use the brand-new API to expose props and emit events, as well as how to structure vue components using the SFC concept.
Questions? Comments? Tweet us.
Reader