Nuxt Icon

Nuxt Icon is a module that allows Nuxt developers to directly access hundreds of thousands of everyday icons within their app components. It presents a fast, elegant, and performant-friendly way to add icons to our app’s components. The module greatly simplifies the developer experience of working with icons by giving instant access to icons in a simple way rather than manually downloading SVG icons and adding them to your app.

Nuxt Icon pulls its icons from Iconify’s API which is an open-source repository for icons in general. This allows us to access a large number of icons that are constantly increasing based on the open-source contributions.

Installation

Run this command in the terminal to install Nuxt-Icon with npm :

npm install --save-dev nuxt-icon

If you’re using yarn, make sure to run this instead:

yarn add --dev nuxt-icon

Next, we’ll have to add nuxt-icon to our nuxt.config.ts file to initialize it within our app.

import { defineNuxtConfig } from 'nuxt'
export default defineNuxtConfig({
  modules: ['nuxt-icon']
})

Usage

To use the icons in our app, we’d have to use the <Icon/> component. This component has three main props: name, color, and size.

  • The name prop represents the icon’s name. You can find the name of your intended icon inside iconify’s site.
  • We use color to define the literal color of the icon.
  • size describes the height and width of the icon

Iconify site showing icons

After installing Nuxt-Icon you can use it by just adding the <Icon/> component. You now add the props above to the component to display the correct icon.

For example, here we add the name and color props to the <Icon/> component.

<Icon name="mingcute:add-fill" color="black" />

You can also convert an emoji into an SVG icon by using the emoji as the icon’s name

<Icon name="🚀" />

Custom Icon

Finally, we can use a custom icon within our app. It just has to be within the components/global/ folder.

For example, we have a custom icon called CustomIcon.

<Icon name="CustomIcon" />

In this case, CustomIcon is imported from components/global/CustomIcon.vue

// CustomIcon.vue

<template>
  <svg width="47.63" height="32" viewBox="0 0 256 172"><path fill="#80EEC0" d="M112.973 9.25c-7.172-12.333-25.104-12.333-32.277 0L2.524 143.66c-7.172 12.333 1.794 27.749 16.14 27.749h61.024c-6.13-5.357-8.4-14.625-3.76-22.576L135.13 47.348L112.973 9.25Z" /><path fill="#00DC82" d="M162.505 38.733c5.936-10.09 20.776-10.09 26.712 0l64.694 109.971c5.936 10.091-1.484 22.705-13.357 22.705H111.167c-11.872 0-19.292-12.614-13.356-22.705l64.694-109.971Z" /></svg>
</template>

Config

We can create a configuration in our app for Nuxt-Icon. This config is a set of rules for the default state of our <Icon/> and its props. This can be helpful when building a large project where you need to abstract certain parts of the component to make sure there’s not a lot of repetition, allowing you to stick to the DRY (Don’t Repeat Yourself) principle.

Now, let’s define a single configuration that’s applicable everywhere in our app. To do that, create an app.config.ts file at the root of the project and set up nuxtIcon.

export default defineAppConfig({
  nuxtIcon: {
    size: '20px', 
    class: 'app-icon', 
    aliases: {
      'mLogo': 'logos:medium',
    }
  }
})

In the config above, we’re defining the size of all <Icon> components in our app as 20px and setting the CSS class to app-icon. The aliases object allows us to assign a name to the actual icon name as mentioned earlier; usually a simpler and intuitive name. In the config above, we’re replacing logos:medium with mLogo.

Please note that without writing a custom config, the default size of all <Icon> will remain 1em and the default class, icon.

An additional object you can add to the config file is iconifyApiOptions. It contains a property called url which is only useful if you have a self-hosted version of iconify; you can add the URL endpoint of your version. In this case, you can also add a publicApiFallback property that’s a boolean to determine whether to fall back to the public Iconify API if the self-hosted API doesn’t work. This fallback only works for the <Icon> component, not for the <IconCSS> component (we’ll discuss what the <IconCSS> component is in a bit).

export default defineAppConfig({
  nuxtIcon: {
    // ...
    iconifyApiOptions: {
      url: 'https://<your-api-url>',
      publicApiFallback: true 
    }
  }
})

You can find more details about the config and all its available properties here.

Render Function

You can display an <Icon> in your Nuxt component using the render function. First, you import it like this:

import { Icon } from '#components'

Then assign it to a variable called CustomIcon. Keep in mind that while assigning it to a variable, we have to define the props. The name prop is defined as logos:medium and color is defined as black.

const CustomIcon = h(Icon, { name: 'logos:medium', color: 'black' })

Go ahead to use it as a component within our app’s component as <CustomIcon />.

<script setup>
import { Icon } from '#components'

const CustomIcon = h(Icon, { name: 'logos:medium', color: 'black' })
</script>

<template>
  <p><CustomIcon /></p>
</template>

Let’s go back to what <IconCSS/> is. It uses the icon as a mask-image and uses <span/> to eventually render the icon on your browser. It’s mostly for performance reasons than anything because besides the component name, every other thing is the same.

<template>
  <IconCSS name="logos:medium" />
</template>

P.S: <IconCSS/> is currently experimental with Nuxt UI and is constantly changing.

Now that we’ve gone through Nuxt Icon and how to use it within our regular applications, let’s now replicate Medium’s onboarding screen using Nuxt Icon.

Building Our Onboarding Screen

Now that we’ve been introduced to Nuxt Icon and its properties, it’s time to put it into practice by building with it. Let’s take a look at what we’ll be building.

our demo app.

Our demo app is the onboarding screen for Medium users. While replicating this screen, we’d be implementing all icons using Nuxt-icon. Let’s jump right in:

Installation

To get started, we’ll need to set up our Nuxt project. Run this command in your terminal:

npx nuxi@latest init nuxt-medium

Navigate into your project’s directory and initiate your project:

cd nuxt-medium

npm run dev

Now we can install Nuxt-Icon. Run this command in your terminal:

npm install --save-dev nuxt-icon

#or

yarn add --dev nuxt-icon

Register the nuxt-icon module by adding it to the modules array in your nuxt.config.ts file.

export default defineNuxtConfig({
  modules: ["nuxt-icon"],
  devtools: { enabled: true },
});

Styling and Typography

Let’s start out by adding the right fonts to the project. Go into our sample repo and download these two fonts: GT-Super-Display-Light-Trial and Sohne. Once they’ve been downloaded, create a fonts folder inside our project’s public folder and have them live there. So both font files live inside the public/fonts folder.

We’re going to be styling our project using SASS. Feel free to use plain CSS if you want

Within the root folder, create an assets folder, then create a scss folder inside of it. Now, inside the assets/scss folder, we’re going to create four stylesheet files: base.scss ( foundational styles for our HTML elements), layout.scss(page styles), main.scss (where we import all the style files), and typography.scss(our font styles).

The goal here is to add our custom font files to the stylesheet. Within our typography.scss, we add these fonts using @font-face.

// typography.scss

@font-face {
  font-family: "GT-Super-Display-Light-Trial";
  src: url("/fonts/GT-Super-Display-Light-Trial.otf") format("opentype");
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "sohne";
  src: url("/fonts/sohne.otf") format('opentype'),;
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

For base.scss file, we take a bunch of the main HTML elements and give them a default style for our project. These include setting the margin, border, and padding to 0. Then, we go on to set the default font-family to "sohne" and vertical-align is set to baseline.

We then set our default display to block; while margin-block-start and margin-block-end are set to 0.

// base.scss

html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
  margin: 0;
  padding: 0;
  border: 0;
  box-sizing: border-box;
  font-family: "sohne", Courier, monospace;
  vertical-align: baseline;
  margin-block-start: 0;
  margin-block-end: 0;
  display: block;
}

For the html, set your font-size to 62.5% because we’re using rem to represent font-size instead of px. So, the logic is that the root font size of our browsers is 16px, and we want to make it possible to convert our px to rem in tens. So, we calculate the percentage of 10px out of 16px as 62.5%.

Technically, this means that 10px = 1rem everywhere in our app. For instance a font-size of 14px can be defined as 1.4rem.

html {
  /* 62.5% of 16px browser font size is 10px */
  font-size: 62.5%;
}

.some-element {
  /* 1.4 * 10px = 14px */
  font-size: 1.4rem;
}

Read more about this concept here.

Furthermore, there’s a media-query block for max-width: 600px, where we define our font-size as 55%.

html {
  font-size: 62.5%;
  @media screen and (max-width: 600px) {
    font-size: 55%;
  }
}

For button, we’re defining the styles as none. The goal is set all default styling, from text-decoration, -webkit-appearance, and outline to none. We’re doing this so we can customize our buttons as we want.

button {
  text-decoration: none;
  -webkit-appearance: none;
  outline: none;
}

Finally, we have ::-webkit-scrollbar where we hide our scroll-bar with display: none.

::-webkit-scrollbar {
  display: none;
}
// main.scss

@import "typography.scss";
@import "layout.scss";
@import "base.scss";

Inside main.scss, we simply import our scss files. This allows us to export a single scss file into our nuxt.config.ts file.

// nuxt.config.ts

export default defineNuxtConfig({
  modules: ["nuxt-icon"],
  css: ["~/assets/scss/main.scss"],
  devtools: { enabled: true },
});

Within our nuxt.config.ts, we add a css property and link our main.scss file with ["~/assets/scss/main.scss"].

Layout

Next, we define our app’s layout. Go into your app.vue and create a parent div

with the CSS class .wrapper. This is going to act as a container for our app display.

// app.vue

<template>
  <div class="wrapper">
  
  </div>
</template>

Now, let’s go into our layout.scss file in scss/layout.scss and style our .wrapper class. We’ll set the display to grid and set its content to the center with justify-content: center.

.wrapper {
  display: grid;
  justify-content: center;
}

Header

Now, to the exciting part. Let’s create another div inside of the .wrapper div and add a class called .wrapper__inner.

<template>
  <div class="wrapper">
    <div class="wrapper__inner">
   
    </div>
  </div>
</template>

Similarly, we’ll add the SCSS styles for this new div in layout.scss. Let’s define its display as grid and font-family as GT-Super-Display-Light-Trial.

.wrapper {
  ...
  &__inner {
    display: grid;
    font-family: "GT-Super-Display-Light-Trial", Courier, monospace;
  }
}

Now, we can add our <Icon/> component inside the div. Let’s call its class name wrapper__inner__icon, and its props include name, width, height, and color.

The name of the Icon is logos:medium, the width is 7em, height is 2.5em, and color is black.

<template>
  <div class="wrapper">
    <div class="wrapper__inner">
      <Icon
        class="wrapper__inner__icon"
        name="logos:medium"
        width="7em"
        height="2.5em"
        color="black"
      />
    </div>
  </div>
</template>

Directly beneath the <Icon/>, we can add the necessary text for the title of the onboarding page and its description.

<template>
  <div class="wrapper">
    <div class="wrapper__inner">
      <Icon
        class="wrapper__inner__icon"
        name="logos:medium"
        width="7em"
        height="2.5em"
        color="black"
      />
      <h3 class="wrapper__inner__title">What are you interested in?</h3>
      <p class="wrapper__inner__select">Choose three or more.</p>
    </div>
  </div>
</template>

Firstly, we’d give a general style to wrapper__inner__icon and wrapper__inner__select. We’ll set justify-self for each of them to center and font-size to 2rem.

.wrapper {
  display: grid;
  justify-content: center;
  &__inner {
    display: grid;
    font-family: "GT-Super-Display-Light-Trial", Courier, monospace;
    &__icon,
    &__select {
      justify-self: center;
      font-size: 2rem;
    }
    &__title {
      margin-top: 8rem;
      justify-self: center;
      margin-bottom: 4rem;
      font-size: 2.8rem;
      line-height: 3.2rem;
      font-weight: 400;
    }
  }
}

For wrapper__inner__title, we’d set it’s margin-top to 8rem, margin-bottom to 4rem, and justify it to the center. For the font-size, it’ll be slightly more prominent than the icon at 2.8rem, line-height is set at 3.2rem, and font-weight at 400.

Let’s run our app’s development server

#yarn
yarn dev

#npm
npm run dev

We should see our app displayed in our browser:

display of the app UI

Options List (Categories)

Next, we’ll be displaying our list of categories for users to select while onboarding.

Screenshot 2023-11-23 at 16.20.44.png

Let’s create a new div for this section. We’ll give the div a class of wrapper__options

<div class="wrapper__options">
  
</div>

Now, let’s add styles for our .wrapper__options. Set the display to flex, width to 70%, and wrap the div’s content using flex-wrap: wrap.

Next, let’s add a margin of 4.8rem auto 13vh auto. This helps us to set div in the middle and add some margin on top and below.

To enable scrolling, we’ll add overflow: auto, define the gap between the div items as 1rem, and justify-content: center.

.wrapper {
  display: grid;
  justify-content: center;
  &__inner {
    ...
  }
  &__options {
    display: flex;
		width: 70%;
		flex-wrap: wrap;
    margin: 4.8rem auto 13vh auto;
    justify-content: center;
    gap: 1rem;
    overflow: auto;
    @media (max-height: 800px) {
      height: 60vh;
    }
  }
  &__footer {
    ...
  }
}

For the content, let’s loop over each option. The list of options we want to display are topic categories for preferred articles on Medium. Within the root folder, create a new folder called data and add the file item-list.js. Copy and paste the file’s content from here.

// data/item-list.js

export const categories = [  {    id: 1,    icon: "ant-design:code-filled",    name: "Programming",  },	{	...	}]

Individual item in the categories array contains an id, the icon’s name, and the display name for each item.

Let’s import the categories array into app.vue and loop over its items. We’d be using v-for to loop over it with v-for="category in categories", and define the :key prop with category.id.

<script setup>
import { categories } from "/data/items-list.js";
</script>

<div class="wrapper__options">
  <div :key="category.id" v-for="category in categories">
    <OptionButton :category="category" />
  </div>
</div>

We’re creating a new component to loop over called: <OptionButton :category="category" />. This component with be inside our components folder as components/OptionButton.vue.

Within the component, we define the props. In this case, we use defineProps to define the category prop; its type is an Object and it’s required.

Next, we’d want to change the UI of the button when a user clicks and selects a category. To do this, let’s create a reactive variable called toggleUser to track this behaviour.

// OptionButton.vue

<script setup>
import { ref } from "vue";
const props = defineProps({
  category: {
    type: Object,
    required: true,
  },
});
const { category } = props;
const toggleUser = ref(false);
</script>

This entire component is a <button/>. Directly on the <button/> we have the @click event to change the style when it’s clicked. In our case, once it’s clicked we want to change the boolean value for toggleUser. We also add dynamic classes that takes effect based on boolean value.

Technically, anytime toggleUser is true, .wrapper__options__button__active style is added to the <button/>. All it does is change the border’s color to green when clicked.

The first <Icon/> in the button is the avi that displays the icon property that we’re passing into it through the category prop. We’ll set the :name prop to category.icon, size to 1.5em, and class is wrapper__options__button__avi. Then, we’ll display the category.name.

Next, we have two icons: the plus icon (clarity:plus-line)and check (iconamoon:check-light) icon. Let’s use v-if to display the plus icon when toggleUser is false and vice-versa with the check icon.

<template>
  <button
    @click="toggleUser = !toggleUser"
    :class="[
      toggleUser && 'wrapper__options__button__active',
      'wrapper__options__button',
    ]"
  >
    <Icon
      size="1.5em"
      class="wrapper__options__button__avi"
      :name="category.icon"
    />
    {{ category.name }}
    <Icon
      v-if="!toggleUser"
      size="1.5em"
      name="clarity:plus-line"
    />

    <Icon
      v-else
      size="1.5em"
      name="iconamoon:check-light"
      color="green"
    />
  </button>
</template>

For the styles, .wrapper__options__button has a transparent border and the border-radius is 9.9rem. We also add other styles like setting the display to flex, aligning the items to the center, adding the appropriate font-size, and padding.

.wrapper {
  &__options {
    ... 
    &__button {
      border: 1px solid transparent;
      border-radius: 9.9rem;
      display: flex;
      align-items: center;
      font-size: 1.4rem;
      padding: 0.7rem 1.5rem;
      &__icon {
        margin-left: 0.8rem;
      }

      &__avi {
        margin-right: 0.8rem;
      }
      &__active {
        border: 1px solid green;
      }
    }
  }
  ...
} 

.wrapper__options__button__icon and .wrapper__options__button__avi both have a margin-left and margin-right of 0.8rem respectively. As mentioned earlier, .wrapper__options__button__active sets the border to the color green.

Footer

Screenshot 2023-11-23 at 18.22.49.png

Go back into the app.vue, and inside of it, create a div at the bottom with the class .wrapper__footer with a <button/> with .wrapper__footer__button.

<template>
  <div class="wrapper">
    <div class="wrapper__inner">
      ...
    </div>
    <div class="wrapper__options">
      ...
    </div>
    <div class="wrapper__footer">
      <button class="wrapper__footer__button">Continue</button>
    </div>
  </div>
</template>

For the styles, we set the height of .wrapper__footer as 13vh, width is 100%, bottom is 0, and position is fixed. We also set the appropriate line-height to 2, text-align is center, background-color is white, font-size is 3rem, and font-weight is bold.

.wrapper {
  ... 
  &__footer {
    line-height: 2;
    text-align: center;
    background-color: white;
    font-size: 3rem;
    font-weight: bold;
    position: fixed;
    bottom: 0;
    width: 100%;
    height: 13vh;
    &__button {
      font-family: "sohne", Courier, monospace;
      border: none;
      background-color: black;
      color: white;
      border-radius: 9.9rem;
      width: 45%;
      padding: 1.2rem 0;
      font-size: 1.4rem;
      margin: 1.2rem 0;
    }
  }
}

.wrapper__footer__button has a font-family of "sohne", Courier, monospace. background-color is black, color is white, and border-radius is 9.9rem.

We set the <button/>'s width to 45%, padding is set to 1.2rem 0, we have font-size: 1.4rem;, border: none, and margin is 1.2rem 0.

Wrapping Up

Congratulations on making it this far! You have successfully learned about Nuxt-Icon and how to use it in your production app. Here is the source code and the live demo to this tutorial.

If you’d like to advance your learning, check out the official Nuxt-Icon documentation.

In this article:

Dive Deeper into Vue today

Access our entire course library with a special discount.

Get Deal

Download the cheatsheets

Save time and energy with our cheat sheets.