|
| 1 | +--- |
| 2 | +title: Editable SVG Icon Systems |
| 3 | +type: cookbook |
| 4 | +order: 2 |
| 5 | +--- |
| 6 | + |
| 7 | +## Base Example |
| 8 | + |
| 9 | +There are many ways to create an SVG Icon System, but one method that takes advantage of Vue's capabilities is to create editable inline icons as components. Some of the advantages of this way of working is: |
| 10 | + |
| 11 | +* They are easy to edit on the fly |
| 12 | +* They are animatable |
| 13 | +* You can use standard props and defaults to keep them to a typical size or alter them if you need to |
| 14 | +* They are inline, so no HTTP requests are necessary |
| 15 | +* They can be made accessible dynamically |
| 16 | + |
| 17 | +First, we'll create a folder for all of the icons, and name them in a standardized fashion for easy retrieval: |
| 18 | + |
| 19 | +> components/icons/IconBox.vue |
| 20 | +> components/icons/IconCalendar.vue |
| 21 | +> components/icons/IconEnvelope.vue |
| 22 | +
|
| 23 | +Here's an example repo to get you going, where you can see the entire setup: [https://github.com/sdras/vue-sample-svg-icons/](https://github.com/sdras/vue-sample-svg-icons/) |
| 24 | + |
| 25 | + |
| 26 | + |
| 27 | +We'll create a base icon (`IconBase.vue`) component that uses a scoped slot. |
| 28 | + |
| 29 | +```html |
| 30 | +<template> |
| 31 | + <svg xmlns="http://www.w3.org/2000/svg" |
| 32 | + :width="width" |
| 33 | + :height="height" |
| 34 | + viewBox="0 0 18 18" |
| 35 | + :aria-labelledby="iconName" |
| 36 | + role="presentation" |
| 37 | + > |
| 38 | + <title :id="iconName" lang="en">{{iconName}} icon</title> |
| 39 | + <g :fill="iconColor"> |
| 40 | + <slot /> |
| 41 | + </g> |
| 42 | + </svg> |
| 43 | +</template> |
| 44 | +``` |
| 45 | + |
| 46 | +You can use this base icon as is- the only thing you might need to update is the `viewBox` depending on the `viewBox` of your icons. In the base, we're making the `width`, `height`, `color`, and name of the icon props so that it can be dynamically updated with props. The name will be used for both the title `id` for accessibility, and the `title`. |
| 47 | + |
| 48 | +Our script will look like this, we'll have some defaults so that our icon will be rendered consistently unless we state otherwise: |
| 49 | + |
| 50 | +```js |
| 51 | +export default { |
| 52 | + props: { |
| 53 | + iconName: { |
| 54 | + type: String, |
| 55 | + default: 'box' |
| 56 | + }, |
| 57 | + width: { |
| 58 | + type: [Number, String], |
| 59 | + default: 18 |
| 60 | + }, |
| 61 | + height: { |
| 62 | + type: [Number, String], |
| 63 | + default: 18 |
| 64 | + }, |
| 65 | + iconColor: { |
| 66 | + type: String, |
| 67 | + default: 'currentColor' |
| 68 | + } |
| 69 | + } |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +The currentColor property that's the default on the fill will make the icon inherit the color of whatever text surrounds it. We could also pass in a different color as a prop if we wish. |
| 74 | + |
| 75 | +We can use it like so, with the only contents of IconWrite.vue containing the paths inside the icon: |
| 76 | + |
| 77 | +```html |
| 78 | +<icon-base icon-name="write"><icon-write /></icon-base> |
| 79 | +``` |
| 80 | + |
| 81 | +Now, if we'd like to make many sizes for the icon, we can do so very easily: |
| 82 | + |
| 83 | +```html |
| 84 | +<p> |
| 85 | + <!--you can pass in a smaller width and height as props--> |
| 86 | + <icon-base width="12" height="12" icon-name="write"><icon-write /></icon-base> |
| 87 | + <!--or you can use the default, which is 18--> |
| 88 | + <icon-base icon-name="write"><icon-write /></icon-base> |
| 89 | + <!--or make it a little bigger too :) --> |
| 90 | + <icon-base width="30" height="30" icon-name="write"><icon-write /></icon-base> |
| 91 | +</p> |
| 92 | +``` |
| 93 | + |
| 94 | +<img src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/Screen%20Shot%202018-01-01%20at%204.51.40%20PM.png" width="450" /> |
| 95 | + |
| 96 | +## Animatable Icons |
| 97 | + |
| 98 | +Keeping icons in components comes in very handy when you'd like to animate them, especially on an interaction. Inline SVGs have the highest support for interaction of any method. Here's a very basic example of an icon that's animated on click: |
| 99 | + |
| 100 | +```html |
| 101 | +<template> |
| 102 | + <svg @click="startScissors" |
| 103 | + xmlns="http://www.w3.org/2000/svg" |
| 104 | + viewBox="0 0 100 100" |
| 105 | + width="100" |
| 106 | + height="100" |
| 107 | + aria-labelledby="scissors" |
| 108 | + role="presentation" |
| 109 | + > |
| 110 | + <title id="scissors" lang="en">Scissors Animated Icon</title> |
| 111 | + <path id="bk" fill="#fff" d="M0 0h100v100H0z"/> |
| 112 | + <g ref="leftscissor"> |
| 113 | + <path d="M..."/> |
| 114 | + ... |
| 115 | + </g> |
| 116 | + <g ref="rightscissor"> |
| 117 | + <path d="M..."/> |
| 118 | + ... |
| 119 | + </g> |
| 120 | + </svg> |
| 121 | +</template> |
| 122 | +``` |
| 123 | + |
| 124 | +```js |
| 125 | +import { TweenMax, Sine } from 'gsap' |
| 126 | + |
| 127 | +export default { |
| 128 | + methods: { |
| 129 | + startScissors() { |
| 130 | + this.scissorAnim(this.$refs.rightscissor, 30) |
| 131 | + this.scissorAnim(this.$refs.leftscissor, -30) |
| 132 | + }, |
| 133 | + scissorAnim(el, rot) { |
| 134 | + TweenMax.to(el, 0.25, { |
| 135 | + rotation: rot, |
| 136 | + repeat: 3, |
| 137 | + yoyo: true, |
| 138 | + svgOrigin: '50 45', |
| 139 | + ease: Sine.easeInOut |
| 140 | + }) |
| 141 | + } |
| 142 | + } |
| 143 | +} |
| 144 | +``` |
| 145 | + |
| 146 | +We're applying `refs` to the groups of paths we need to move, and as both sides of the scissors have to move in tandem, we'll create a funciton we can reuse where we'll pass in the `refs`. The use of GreenSock helps resolve animation support and transform-origin issues across browser. |
| 147 | + |
| 148 | +<p data-height="300" data-theme-id="0" data-slug-hash="dJRpgY" data-default-tab="result" data-user="Vue" data-embed-version="2" data-pen-title="Editable SVG Icon System: Animated icon" class="codepen">See the Pen <a href="https://codepen.io/team/Vue/pen/dJRpgY/">Editable SVG Icon System: Animated icon</a> by Vue (<a href="https://codepen.io/Vue">@Vue</a>) on <a href="https://codepen.io">CodePen</a>.</p><script async src="https://production-assets.codepen.io/assets/embed/ei.js"></script> |
| 149 | + |
| 150 | +<p style="margin-top:-30px">Pretty easily accomplished! And easy to update on the fly.</p> |
| 151 | + |
| 152 | +You can see more animated examples in the repo [here](https://github.com/sdras/vue-sample-svg-icons/) |
| 153 | + |
| 154 | +## Additional Notes |
| 155 | + |
| 156 | +Designers may change their minds. Product requirements change. Keeping the logic for the entire icon system in one base component means you can quickly update all of your icons and have it propogate through the whole system. Even with the use of an icon loader, some situations require you to recreate or edit every SVG to make global changes. This method can save you that time and pain. |
| 157 | + |
| 158 | +## When To Avoid This Pattern |
| 159 | + |
| 160 | +This type of SVG icon system is really useful when you have a number of icons that are used in different ways throughout your site. If you're repeating the same icon many times on one page (e.g. a giant table a delete icon in each row), it might make more sense to have all of the sprites compiled into a sprite sheet and use `use` tags to load them. |
| 161 | + |
| 162 | +## Alternative Patterns |
| 163 | + |
| 164 | +Other tooling to help manage SVG icons includes: |
| 165 | + |
| 166 | +* [svg-sprite-loader](https://github.com/kisenka/svg-sprite-loader) |
| 167 | +* [svgo-loader](https://github.com/rpominov/svgo-loader) |
| 168 | + |
| 169 | +These tools bundle SVGs at compile time, but make them a little harder to edit during runtime, because `use` tags can have strange cross-browser issues when doing anything more complex. They also leave you with two nested `viewBox` properties and thus two coordinate systems. This makes the implementation a little more complex. |
0 commit comments