Summary
In this article, we will:
- use custom SVG icons
- build a flexible and customizable reusable component to use SVG icons
- use Vite, Vue 3, Quasar and Pinia
- use both Composition API with script setup and Options API with Vue 3
- auto-register global components using
import.meta.glob
andimport.meta.globEager
- link CSS values to dynamic component state using
v-bind
CSS function
Waitπ²! All of these in an article ?
YES! Let's do it! π€ΉββοΈ
What are we going to build:
- Click here to see the app what we are going to build
-
Play with the code online on Stackblitz (you might have to run
vite
in the terminal to run the app) - Repo with each lesson in a branch
Requirements
Let's add a designer into the game who defines how we should build that component.
Our designer π¨βπ¨/π©βπ¨ is tired of using icon sets and has decided that the app needs its own unique SVG icons. Here are the specifications he/she gave us:
Most icons fall in 16, 24, 32, 48 sizes. However, a few icons, some special ones, have different sizes. The most used size is 48px.
-
SVG icons are scalable and their strokes too, but our smart designer wants to manually control the stroke width in different sizes:
- 16px and 24px: 1px stroke width
- 32px: 2px stroke width
- 48px: 2.5px stroke width
All icons have outlined style.
All icons should have a default color which is called 'primary' and a default hover color
white
, however, it must be easy to overwrite those defaults.The app has colors defined in CSS variables, but it should be possible to set the color with a color value (hex, rgb etc)
Here are the CSS Variables colors:
--primary: #007bff;
--secondary: #6c757d;
--positive: #28a745;
--negative: #dc3545;
--info: #17a2b8;
--warning: #ffc107;
Alright! As you can see, we need a flexible and customizable reusable component. π―
Let's start coding! β¨οΈπ₯
We could start by creating a new Vite + Vue 3 project which you can do by running npm init vue@latest
in the terminal, but to speed things up, I have already done that, cleaned up the project and added some SVG icons.
So, clone or download the repo or play directly with the code online on Stackblitz.
As you can see, we have a clean Vite + Vue 3 app and some SVG icons in src/components/icons
folder.
The next step, is to install Quasar and Pinia. Before doing so, I loved how in Vue 2 we could keep main.js
file clean and simple, so we are going to do exactly that!
First, let's create a plugins folder (src/plugins
) and inside a main-app.js
file:
import { createApp } from 'vue'
import App from '../App.vue'
export const app = createApp(App)
Then, our main.js
should look like this:
import { app } from './plugins/main-app'
app.mount('#app')
Clean and simple right?
Install Quasar and Pinia
First run the command:
npm install quasar @quasar/extras pinia
In order to make Quasar work in Vite, we need to install the appropriate plugin:
npm install -D @quasar/vite-plugin
Now that we installed them, let's register them in the plugins
folder:
pinia.js
import { app } from './main-app'
import { createPinia } from 'pinia'
app.use(createPinia())
quasar.js
import { Quasar } from 'quasar'
import { app } from './main-app'
// Import icon libraries
import '@quasar/extras/material-icons/material-icons.css'
// Import Quasar css
import 'quasar/src/css/index.sass'
app.use(Quasar, {
plugins: {} // import Quasar plugins and add here
})
Finally, let's import the Quasar and Pinia plugins in the main.js
file:
import { app } from './plugins/main-app'
import './plugins/quasar' // +
import './plugins/pinia' // +
app.mount('#app')
If something doesn't work on your end, see the working code here on Github or online on Stackblitz
Create reusable component for SVG icons
Now, we need to create a reusable component for SVG icons. Let's do it. π·
We will call it SvgIcon
and make it a global component so we can use it everywhere without importing it.
Let's create a SvgIcon.vue
and a contants.js
in src/components/global/svg-icon
folder. Inside components/global
we will keep all of our global components
Remember our requirements?
- our common icon sizes are 16, 24, 32 and 48. So we will call them
sm, md, lg, xl
respectively. - Default one is 48, so that means
xl
. - 16 and 24 have 1px stroke width, 32 has 2px, 48 has 2.5px.
- Default color is
primary
, and default hover color iswhite
.
Let's define those in contants.js
file:
export const sizes = {
sm: {
size: 16,
strokeWidth: 1
},
md: {
size: 24,
strokeWidth: 1
},
lg: {
size: 32,
strokeWidth: 2
},
xl: {
size: 48,
strokeWidth: 2.5
}
}
export const defaults = {
color: 'var:primary',
varPrefix: 'q-',
hoverColor: 'white',
size: 'xl'
}
Quasar variables are prefixed with q-
by default. e.g. --q-primary
.
In order to account for that we define a varPrefix
property in defaults
object.
var:primary
: color
and hoverColor
can either be a color value e.g. yellow
or a variable e.g. var:primary
. var:primary
targets the --q-primary
variable.
Next, let's write some code in SvgIcon.vue
file. We will use Options API π:
<script>
import { defineAsyncComponent } from 'vue'
import { sizes, defaults } from './constants'
export default {
props: {
name: {
type: String,
required: true
},
color: {
type: String,
default: defaults.color
},
size: {
type: String,
default: defaults.size,
validator: (val) => Object.keys(sizes).includes(val)
},
hoverColor: [Boolean, String]
},
computed: {
dynamicComponent() {
const name = this.name.charAt(0).toUpperCase() + this.name.slice(1) + 'Icon'
return defineAsyncComponent(() => import(`../../icons/${name}.vue`))
},
colorBind() {
const color = this.color ? this.color : defaults.color
return this.getVarOrColorValue(color)
},
hoverColorBind() {
if (this.hoverColor === false) return
if (this.hoverColor === true || !this.hoverColor) return defaults.hoverColor
return this.getVarOrColorValue(this.hoverColor)
},
svgSize() {
return sizes[this.size].size
},
strokeWidth() {
return sizes[this.size].strokeWidth
}
},
methods: {
getVarOrColorValue(str) {
return str.startsWith('var:') ? str.replace(/^var:/, `var(--${defaults.varPrefix}`) + ')' : str
}
}
}
</script>
I think the component's code is straightforward, but here is some explanations:
-
dynamicComponent
: Based on thename
prop, we import the corresponding icon component. -
colorBind
: ifcolor
prop isfalsy
usedefaults.color
, otherwise usecolor
. CallgetVarOrColorValue
to return the color or the variable. -
hoverColorBind
: ifhoverColor
prop isfalse
no hover is enabled. If it'sfalsy
(e.g.undefined
) we will usedefaults.hoverColor
. CallgetVarOrColorValue
to return the color or the variable. -
getVarOrColorValue
: ifstr
is a color value it returns it as is. Otherwise, if it starts withvar:
then it returns the CSS variable. e.g. str ===var:primary
will returnvar(--q-primary)
taking into accountdefaults.varPrefix
.
Next, let's add the <template>
and <style>
tags:
<template>
<component
:is="dynamicComponent"
class="svg-icon"
:width="svgSize"
:height="svgSize"
:stroke-width="strokeWidth"
:class="{ 'add-hover': !!hoverColorBind }"
/>
</template>
<style lang="scss" scoped>
.svg-icon {
color: v-bind(colorBind);
transition: color 0.2s ease-in-out;
&.add-hover:hover {
color: v-bind(hoverColorBind);
}
}
</style>
Not much to explain about the template but in the style we use v-bind
to link colorBind
and hoverColorBind
computed properties to the CSS color property. Whenever these computed properties change, the color property will be updated. In fact, the actual value will be compiled into a hashed CSS variable. Learn more in the docs.
Greatness! Here are some simple examples of using the component we just created:
<svg-icon name="home" />
<svg-icon name="home" size="sm" color="var:primary" hoverColor />
<svg-icon name="home" size="sm" color="var:primary" hoverColor="blue" />
<svg-icon name="home" size="sm" color="blue" hoverColor="var:primary" />
See the working code here on Github or online on Stackblitz.
Auto-register components
We haven't made our SvgIcon.vue
component global yet, so let's register all components in the components/global
folder.
In Vite, we can do this by using Glob import.
First create plugins/global-components.js
file:
import.meta.glob
By using import.meta.glob
, matched files are lazy loaded via dynamic import and will be split into separate chunks during build:
import { defineAsyncComponent } from 'vue'
import { app } from './main-app'
const globalComponentsPaths = import.meta.glob('/src/components/global/**/*.vue')
Object.entries(globalComponentsPaths).forEach(([path, module]) => {
// "./components/SvgIcon.vue" -> "SvgIcon"
const componentName = path
.split('/')
.pop()
.replace(/\.vue$/, '')
app.component(componentName, defineAsyncComponent(module))
})
import.meta.globEager
If you want to load all matched files eagerly, you can use import.meta.globEager
:
import { app } from './main-app'
const globalComponentsPaths = import.meta.globEager('/src/components/global/**/*.vue')
Object.entries(globalComponentsPaths).forEach(([path, module]) => {
// "./components/SvgIcon.vue" -> "SvgIcon"
const componentName = path
.split('/')
.pop()
.replace(/\.vue$/, '')
app.component(componentName, module.default)
})
In our case, we don't want separate chunks since we will only have a single page, so we will use import.meta.globEager
. This will load all components eagerly and will be included in the main bundle.
The last step is to import the global-components.js
file in main.js
:
import { app } from './plugins/main-app'
import './plugins/quasar'
import './plugins/pinia'
import './plugins/global-components' // +
app.mount('#app')
Now we can use the <svg-icon>
component everywhere in our app without the need to import it.
Now it's time to start building our interactive playground. π₯π₯
See the working code here on Github or online on Stackblitz.
Create and use Pinia store πͺ
The first step on building the interactive playground, is to create a global store so all our components can interact with it.
So let' go and create a global-store.js
file in src/stores
folder:
import { reactive, ref } from 'vue'
import { defineStore } from 'pinia'
export const useGlobalStore = defineStore('global-store', () => {
const availableIcons = ['user', 'search', 'home']
const selectedIcon = ref(availableIcons[0])
const color = ref()
const hasHoverColor = ref(false)
const hoverColor = ref()
const availableSizes = ['sm', 'md', 'lg', 'xl']
const selectedSize = ref(availableSizes[3])
const cssVarColors = reactive({
primary: '#007bff',
secondary: '#6c757d',
positive: '#28a745',
negative: '#dc3545',
info: '#17a2b8',
warning: '#ffc107'
})
return {
availableIcons,
selectedIcon,
color,
hasHoverColor,
hoverColor,
availableSizes,
selectedSize,
cssVarColors
}
})
Great! We have created a Pinia store π! That was simple right?
Now, let's use this store in App.vue
to bind cssVarColors
to Quasar CSS variables. We will use Composition API with script setup
for App.vue
and finally use SvgIcon.vue
component:
<script setup>
import { useGlobalStore } from '@/stores/global-store'
const globalStore = useGlobalStore()
</script>
<template>
<header>
<div class="gradient-font q-my-sm">Unified way of using SVG Icons</div>
</header>
<main class="">
<svg-icon name="user" />
</main>
</template>
<style lang="scss">
@import 'css/base';
.main {
--q-primary: v-bind('globalStore.cssVarColors.primary');
--q-secondary: v-bind('globalStore.cssVarColors.secondary');
--q-positive: v-bind('globalStore.cssVarColors.positive');
--q-negative: v-bind('globalStore.cssVarColors.negative');
--q-info: v-bind('globalStore.cssVarColors.info');
--q-warning: v-bind('globalStore.cssVarColors.warning');
width: 100%;
}
</style>
See the working code here on Github or online on Stackblitz.
Next steps
The article became a bit long, so let's build the interactive playground in the next article where we will:
- Use built-in component: Suspense
- create an interactive playground to play with the SvgIcon component
- highlight and generate the code using
Highlight.js
- add responsive design with CSS Grid & Quasar
- add CSS Gradient rounded borders
- More usage of Quasar, Pinia and Composition API with script setup
If you liked this article, you can show your support by buying me a coffee. It would motivate me a lot.
Thanks for reading, I hope you enjoyed it!
About me
My name is Roland Doda (Feel free to connect with me on Linkedin or follow me on Github, Dev.to, Hashnode or Twitter) and I work as a Senior Frontend Developer at CPI technologies.
We are also hiring btw! Check out CPI Career Center and also VueJS Frontend Developer job description.
Top comments (1)
Hi, thanks for the article, quite useful.
Does this solution mean that each icon requires network request? You basically use dynamic import here.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.