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:

<script setup>
// javascript code
</script>

<template>
  <!-- html code -->
  <div>Welcome to SFC</div>
</template>

<style lang="css">
  /* css code */
</style>

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.

Checkbox

Let’s get started by creating a checkbox.vue file and defining some props under the script section.

const props = defineProps({
  value: {
    type: String,
    default: "",
  },
  label: {
    type: String,
    default: "",
  },
  checked: {
    type: Boolean,
    default: false,
  },
  id: {
    type: String,
    default: "",
  },
  disabled: {
    type: Boolean,
    default: false,
  },
});

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.

 
const emit = defineEmits({
  onChange: {
    type: "change",
    default: () => {},
  },
});

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.

const wrapperClass = computed(() => {
  const { checked } = props;
  return {
    "check-box": true,
    "check-box--checked": checked,
  };
});

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.

 
const iconClass = computed(() => {
  const { checked } = props;
  return {
    "check-box__icon": true,
    "check-box__icon--checked": checked,
  };
});

Now that we have the computed properties ready, it's time to set up an event handler for the click event.

const handleClick = () => {
  emit("onChange", props.id);
};

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.

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.

<template>
  <div
    :class="wrapperClass"
    tabindex="0"
    role="checkbox"
    :aria-labelledby="`label-${props.id}`"
    :aria-checked="props.checked"
    @click="handleClick"
  >
	. . .
  </div>
</template>

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.

  1. An Icon to signify the state of the checkbox
  2. A Label for the checkbox

Let’s run through them.

Icon


<span :class="iconClass">
   <Square v-if="!props.checked" />
   <CheckedSquare v-if="props.checked" />
</span>

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 checked prop is true, we display the CheckedSquare icon, and when it is false, we show the Square icon.

Label

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.


<span
  :id="`label-${props.id}`"
  class="label"
>
  {{ props.label }}
</span>

This is how the entire template should look.


<template>
  <div
    :class="wrapperClass"
    tabindex="0"
    role="checkbox"
    :aria-checked="props.checked"
    @click="handleClick"
  >
    <span :class="iconClass">
      <Square v-if="!props.checked" />
      <CheckedSquare v-if="props.checked" />
    </span>
    <span
      :id="`label-${props.id}`"
      class="label"
    >
      {{ props.label }}
    </span>
  </div>
</template>

Styles

Finally, let's add some style to the checkbox to make it look nicer.

<style scoped lang="css">
.check-box {
  align-items: center;
  border: 1px solid transparent;
  cursor: pointer;
  display: flex;
  justify-content: flex-start;
  padding: 0.5rem;
  user-select: none;
}
 
.label {
  padding-left: 0.5rem;
}
 
.check-box__icon {
  display: block;
  height: 1rem;
  width: 1rem;
 
  svg {
    height: 100%;
    width: 100%;
  }
}
</style>

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.


<script setup>
import CheckBoxGroup from "check-box.vue";
</script>
 
<template>
  <Checkbox
    id="123"
    label="Option 1"
    value="option1"
  />
</template>
 

If all goes well, you should have the checkbox rendered.

Checkbox group

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.

Properties

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.

const props = defineProps({
  items: {
    type: Array,
    default: () => [],
  },
});

Emits

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.

const emit = defineEmits({
  onChange: {
    type: "change",
    default: () => {},
  },
});

Refs

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.

const itemsRef = ref(
  props.items.map((item) => {
    return {
      ...item,
      id: nanoid(),
    };
  })
);

While storing the array in its initial state, we also include a unique id for every item with the help of nanoid.

Event handler

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.

const handleOnChange = (id) => {
  const newValue = itemsRef.value.map((item) => ({
    ...item,
    checked: item.id === id ? !item.checked : item.checked,
  }));
  itemsRef.value = newValue;
  emit("onChange", newValue);
};

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.

const newValue = itemsRef.value.map((item) => ({
  ...item,
  checked: item.id === id ? !item.checked : item.checked,
}));

The reactive data source is then updated with the following snippet.

itemsRef.value = newValue;

Finally, we emit the onChange event with the updated array.

emit("onChange", newValue);

HTML

Next, we need to create the HTML for the checkbox group.

<template>
  <div class="checkbox-group-wrapper">
    <Checkbox
      v-for="item in itemsRef"
      :id="item.id"
      :key="item.id"
      :label="item.label"
      :value="item.value"
      :checked="item.checked"
      @on-change="handleOnChange"
    />
  </div>
</template>

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.

:id="item.id"
:key="item.id"
:label="item.label"
:value="item.value"
:checked="item.checked"

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.

 @on-change="handleOnChange"

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

//checkbox.vue
<script setup>
import { computed } from "vue";
import CheckedSquare from "../icons/check-square.svg?component";
import Square from "../icons/square.svg?component";
 
const emit = defineEmits({
  onChange: {
    type: "change",
    default: () => {},
  },
});
 
const props = defineProps({
  value: {
    type: String,
    default: "",
  },
  label: {
    type: String,
    default: "",
  },
  checked: {
    type: Boolean,
    default: false,
  },
  id: {
    type: String,
    default: "",
  },
  disabled: {
    type: Boolean,
    default: false,
  },
});
 
const wrapperClass = computed(() => {
  const { checked } = props;
  return {
    "check-box": true,
    "check-box--checked": checked,
  };
});
 
const iconClass = computed(() => {
  const { checked } = props;
  return {
    "check-box__icon": true,
    "check-box__icon--checked": checked,
  };
});
 
const handleClick = () => {
  emit("onChange", props.id);
};
</script>
 
<template>
  <div
    :class="wrapperClass"
    tabindex="0"
    role="checkbox"
    :aria-checked="props.checked"
    @click="handleClick"
  >
    <span :class="iconClass">
      <Square v-if="!props.checked" />
      <CheckedSquare v-if="props.checked" />
    </span>
    <span
      :id="`label-${props.id}`"
      class="label"
    >
      {{ props.label }}
    </span>
  </div>
</template>
 
<style scoped lang="css">
.check-box {
  align-items: center;
  border: 1px solid transparent;
  cursor: pointer;
  display: flex;
  justify-content: flex-start;
  padding: 0.5rem;
  user-select: none;
}
 
.label {
  padding-left: 0.5rem;
}
 
.check-box__icon {
  display: block;
  height: 1rem;
  width: 1rem;
 
  svg {
    height: 100%;
    width: 100%;
  }
}
</style>

checkbox-group.vue

<script setup>
import { nanoid } from "nanoid";
import { ref } from "vue";
import Checkbox from "./check-box.vue";
 
const props = defineProps({
  items: {
    type: Array,
    default: () => [],
  },
});
 
const emit = defineEmits({
  onChange: {
    type: "change",
    default: () => {},
  },
});
 
const itemsRef = ref(
  props.items.map((item) => {
    return {
      ...item,
      id: nanoid(),
    };
  })
);
 
const handleOnChange = (id) => {
  const newValue = itemsRef.value.map((item) => ({
    ...item,
    checked: item.id === id ? !item.checked : item.checked,
  }));
  itemsRef.value = newValue;
  emit("onChange", newValue);
};
</script>
 
<template>
  <div class="checkbox-group-wrapper">
    <Checkbox
      v-for="item in itemsRef"
      :id="item.id"
      :key="item.id"
      :label="item.label"
      :value="item.value"
      :checked="item.checked"
      @on-change="handleOnChange"
    />
  </div>
</template>
 
<style lang="css">
.checkbox-group-wrapper {
  padding: 0.5rem;
}
</style>

Checkbox group in action

Now that we've created the checkbox group, let's put it to use.

<script setup>
import CheckBoxGroup from "./components/check-box-group.vue";
const onChange = (val) => {
  console.log(val);
};
</script>
 
<template>
  <CheckBoxGroup
    :items="[
      {
        label: 'Option 1',
        value: '1',
      },
      {
        label: 'Option 2',
        value: '2',
      },
      {
        label: 'Option 3',
        value: '3',
      },
    ]"
    @on-change="onChange"
  />
</template>

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.

To sum it up...

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.