├── .babelrc ├── src ├── components │ └── Skeletor │ │ ├── index.ts │ │ ├── Skeletor.scss │ │ └── Skeletor.tsx ├── constants.ts ├── shims-vue.d.ts ├── index.ts ├── helpers.ts ├── composables │ └── use-skeletor.ts └── install.ts ├── .gitignore ├── .npmignore ├── playground ├── index.ts ├── index.scss ├── index.html └── Playground.tsx ├── tsconfig.json ├── types └── index.d.ts ├── rollup.config.js ├── package.json └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@vue/babel-plugin-jsx"] 3 | } -------------------------------------------------------------------------------- /src/components/Skeletor/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Skeletor'; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .cache 4 | *.code-workspace 5 | public -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const SkeletorSymbol = Symbol(); 2 | 3 | export const DEFAULT_OPTIONS = { 4 | shimmer: true, 5 | } -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue' 3 | const component: DefineComponent 4 | export default component 5 | } 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Skeletor } from './components/Skeletor'; 2 | export { default as useSkeletor } from './composables/use-skeletor'; 3 | export { default } from './install'; -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .cache 3 | playground 4 | 5 | yarn.lock 6 | package-lock.json 7 | node_modules 8 | 9 | *.config.js 10 | 11 | .vs-code 12 | *.code-workspace 13 | 14 | public 15 | .babelrc -------------------------------------------------------------------------------- /playground/index.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import Playground from './Playground'; 3 | // import VueSkeletor from '../src'; 4 | 5 | const app = createApp(Playground); 6 | // app.use(VueSkeletor); 7 | 8 | app.mount(document.querySelector('#app')); -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | export function convertToUnit (str: string | number | null | undefined, unit = 'px'): string | undefined { 2 | if (str == null || str === '') { 3 | return undefined 4 | } else if (isNaN(str as number)) { 5 | return String(str) 6 | } else { 7 | return `${Number(str)}${unit}` 8 | } 9 | } -------------------------------------------------------------------------------- /playground/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Roboto'; 3 | } 4 | 5 | .d-flex { 6 | display: flex; 7 | } 8 | 9 | .align-center { 10 | align-items: center; 11 | } 12 | 13 | .justify-center { 14 | justify-content: center; 15 | } 16 | 17 | .justify-end { 18 | justify-content: flex-end; 19 | } 20 | 21 | .flex-shrink-0 { 22 | flex-shrink: 0; 23 | } 24 | 25 | .flex-grow-1 { 26 | flex-grow: 1; 27 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "es2015", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "baseUrl": ".", 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /src/composables/use-skeletor.ts: -------------------------------------------------------------------------------- 1 | import { inject, warn } from 'vue'; 2 | import { SkeletorSymbol } from '../constants'; 3 | import { SkeletorOptions } from 'types'; 4 | import { DEFAULT_OPTIONS } from '../constants'; 5 | 6 | export default function useSkeletor() { 7 | const skeletor = inject(SkeletorSymbol, DEFAULT_OPTIONS); 8 | 9 | if(!skeletor) { 10 | warn('Skeletor is not installed on this Vue application.'); 11 | } 12 | 13 | return skeletor; 14 | } -------------------------------------------------------------------------------- /src/install.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue'; 2 | import { SkeletorSymbol } from './constants'; 3 | import { SkeletorPlugin, SkeletorOptions } from 'types'; 4 | import { DEFAULT_OPTIONS } from './constants'; 5 | 6 | const SkeletorPlugin: SkeletorPlugin = { 7 | install(app, options: SkeletorOptions = {}) { 8 | app.provide(SkeletorSymbol, reactive({ 9 | ...DEFAULT_OPTIONS, 10 | ...options, 11 | })); 12 | } 13 | } 14 | 15 | export default SkeletorPlugin; -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ddd 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AllowedComponentProps, 3 | ComponentCustomProps, 4 | VNodeProps, 5 | Plugin, 6 | App 7 | } from 'vue'; 8 | 9 | export interface SkeletorOptions { 10 | shimmer?: boolean; 11 | } 12 | 13 | export interface SkeletorPlugin extends Plugin { 14 | install: (app: App, options: SkeletorOptions) => void; 15 | } 16 | 17 | export const useSkeletor: () => SkeletorOptions; 18 | 19 | export interface SkeletorProps { 20 | as?: string; 21 | circle?: boolean; 22 | pill?: boolean; 23 | shimmer?: boolean; 24 | size?: string | number; 25 | width?: string | number; 26 | height?: string | number; 27 | } 28 | 29 | export const Skeletor: new () => { 30 | $props: AllowedComponentProps & ComponentCustomProps & VNodeProps & SkeletorProps; 31 | }; -------------------------------------------------------------------------------- /src/components/Skeletor/Skeletor.scss: -------------------------------------------------------------------------------- 1 | .vue-skeletor { 2 | position: relative; 3 | overflow: hidden; 4 | background-color: rgba(#000, 0.12); 5 | 6 | &:not(&--shimmerless) { 7 | &:after { 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | width: 100%; 12 | height: 100%; 13 | transform: translateX(-100%); 14 | background-image: linear-gradient( 15 | 90deg, 16 | rgba(255, 255, 255, 0), 17 | rgba(255, 255, 255, .3), 18 | rgba(37, 22, 22, 0) 19 | ); 20 | animation: shimmer 1.5s infinite; 21 | content: ''; 22 | } 23 | } 24 | 25 | &--rect, &--circle { 26 | display: block; 27 | } 28 | 29 | &--circle { 30 | border-radius: 50%; 31 | } 32 | 33 | &--pill, &--text { 34 | border-radius: 9999px; 35 | } 36 | 37 | &--text { 38 | line-height: 1; 39 | display: inline-block; 40 | width: 100%; 41 | height: inherit; 42 | vertical-align: middle; 43 | top: -1px; 44 | } 45 | } 46 | 47 | @keyframes shimmer { 48 | 100% { 49 | transform: translateX(100%); 50 | } 51 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import typescript from 'rollup-plugin-typescript'; 3 | import babel from '@rollup/plugin-babel'; 4 | import scss from 'rollup-plugin-scss' 5 | import alias from '@rollup/plugin-alias'; 6 | 7 | const typescriptPlugin = typescript({ 8 | tsconfig: './tsconfig.json', 9 | }); 10 | 11 | const babelPlugin = babel({ 12 | babelrc: true, 13 | extensions: ['.jsx', '.tsx', '.ts', '.js'] 14 | }); 15 | 16 | const aliasPlugin = alias({ 17 | entries: [ 18 | { find: '@', replacement: path.resolve(__dirname, 'src') }, 19 | ] 20 | }); 21 | 22 | const plugins = [ 23 | typescriptPlugin, 24 | babelPlugin, 25 | aliasPlugin, 26 | scss({ 27 | output: 'dist/vue-skeletor.css', 28 | }), 29 | ]; 30 | 31 | export default [ 32 | { 33 | input: 'src/index.ts', 34 | output: { 35 | format: 'esm', 36 | file: 'dist/vue-skeletor.esm.js' 37 | }, 38 | plugins, 39 | external: ['vue'], 40 | }, 41 | { 42 | input: 'src/index.ts', 43 | output: { 44 | format: 'cjs', 45 | file: 'dist/vue-skeletor.cjs.js', 46 | }, 47 | plugins, 48 | external: ['vue'], 49 | } 50 | ] -------------------------------------------------------------------------------- /playground/Playground.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue'; 2 | import { Skeletor, useSkeletor } from '../src'; 3 | import './index.scss'; 4 | 5 | export default defineComponent({ 6 | name: 'Playground', 7 | 8 | setup() { 9 | const skeletor = useSkeletor(); 10 | }, 11 | 12 | render() { 13 | return [ 14 |
15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 |

23 | 24 |

25 |

26 | 27 |

28 |
29 |
30 | 31 |

32 | 33 | 34 | 35 | 36 |

37 | 38 |
39 | 40 | 41 |
42 |
43 | ]; 44 | } 45 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-skeletor", 3 | "version": "1.0.6", 4 | "description": "Vue 3 Skeleton Loading component.", 5 | "main": "dist/vue-skeletor.cjs.js", 6 | "module": "dist/vue-skeletor.esm.js", 7 | "types": "types/index.d.ts", 8 | "scripts": { 9 | "playground": "parcel playground/index.html --out-dir public", 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "build": "rollup -c rollup.config.js" 12 | }, 13 | "keywords": [ 14 | "vue", 15 | "vue js", 16 | "vue 3", 17 | "vue 3 skeleton", 18 | "vue 3 skeleton loading", 19 | "vue 3 skeleton loader", 20 | "vue skeleton", 21 | "vue skeleton loading", 22 | "vue skeleton loader" 23 | ], 24 | "author": "DarkC0der", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/DarkC0der11/vue-skeletor" 28 | }, 29 | "license": "MIT", 30 | "devDependencies": { 31 | "@babel/core": "^7.11.6", 32 | "@rollup/plugin-alias": "^3.1.1", 33 | "@rollup/plugin-babel": "^5.2.1", 34 | "@vue/babel-plugin-jsx": "^1.0.0-rc.3", 35 | "babel-loader": "^8.1.0", 36 | "parcel": "^1.12.4", 37 | "rollup": "^2.29.0", 38 | "rollup-plugin-scss": "^2.6.1", 39 | "rollup-plugin-serve": "^1.0.4", 40 | "rollup-plugin-typescript": "^1.0.1", 41 | "sass": "^1.27.0", 42 | "tslib": "^2.0.2", 43 | "typescript": "^4.0.3", 44 | "vue": "^3.0.0" 45 | }, 46 | "dependencies": {}, 47 | "sideEffects": false 48 | } 49 | -------------------------------------------------------------------------------- /src/components/Skeletor/Skeletor.tsx: -------------------------------------------------------------------------------- 1 | import './Skeletor.scss'; 2 | import { defineComponent, CSSProperties, computed, h } from 'vue'; 3 | import { convertToUnit } from '../../helpers'; 4 | import useSkeletor from '../../composables/use-skeletor'; 5 | 6 | const Skeletor = defineComponent({ 7 | name: 'Skeletor', 8 | 9 | props: { 10 | as: { 11 | type: String, 12 | default: 'span' 13 | }, 14 | 15 | circle: { 16 | type: Boolean, 17 | default: false, 18 | }, 19 | 20 | pill: { 21 | type: Boolean, 22 | default: false 23 | }, 24 | 25 | size: { 26 | type: [String, Number], 27 | default: null, 28 | }, 29 | 30 | width: { 31 | type: [String, Number], 32 | default: null, 33 | }, 34 | 35 | height: { 36 | type: [String, Number], 37 | default: null, 38 | }, 39 | 40 | shimmer: { 41 | type: Boolean, 42 | default: undefined, 43 | } 44 | }, 45 | 46 | setup(props) { 47 | const skeletor = useSkeletor()!; 48 | 49 | const isRect = computed(() => ( 50 | !props.circle && (props.size || props.height) 51 | )); 52 | 53 | const isText = computed(() => ( 54 | !props.circle && !props.size && !props.height 55 | )); 56 | 57 | const isShimmerless = computed(() => ( 58 | props.shimmer !== undefined ? !props.shimmer : !skeletor.shimmer 59 | )); 60 | 61 | const classes = computed(() => ({ 62 | 'vue-skeletor': true, 63 | 'vue-skeletor--rect': isRect.value, 64 | 'vue-skeletor--text': isText.value, 65 | 'vue-skeletor--shimmerless': isShimmerless.value, 66 | 'vue-skeletor--circle': props.circle, 67 | 'vue-skeletor--pill': props.pill, 68 | })); 69 | 70 | const style = computed(() => { 71 | const _style: CSSProperties = {}; 72 | 73 | if(props.size) { 74 | const size = convertToUnit(props.size); 75 | _style.width = size; 76 | _style.height = size; 77 | } 78 | 79 | if(!props.size && props.width) { 80 | _style.width = convertToUnit(props.width); 81 | } 82 | 83 | if(!props.size && props.height) { 84 | _style.height = convertToUnit(props.height); 85 | } 86 | 87 | return _style; 88 | }); 89 | 90 | const children = isText.value ? '\u200C' : null; 91 | 92 | return () => h(props.as, { 93 | class: classes.value, 94 | style: style.value, 95 | }, [children]); 96 | }, 97 | }); 98 | 99 | export default Skeletor; 100 | 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 💀 Vue Skeletor (Vue 3 Skeleton Loading component) 2 | 3 | Vue 3 adaptive skeleton loading component that will match your typography. 4 | 5 | [Codesandbox Card Example](https://codesandbox.io/s/epic-ishizaka-nl9z3?file=/src/App.vue) 6 | 7 | ## Installation 8 | `npm install vue-skeletor -S` 9 | 10 | ## Why adaptive skeletons? 11 | Skeletons are used to mimic how the real content would look, so in order to create nice skeleton you would have to manually create squares, circles and position/size them to match your real component and keep it updated whenever you change it. 12 | 13 | Aww sounds awful isn't it? 14 | 15 | Wouldn't be nice if you had a skeleton that automatically adjusts to your existing components layout? I think it would =) so the Vue Skeletor comes to rescue. 16 | 17 | Instead of creating separate skeleton components you can inject skeletons directly into your existing components. 18 | 19 | Example: 20 | 21 | ```html 22 | 55 | ``` 56 | 57 | And that's it, the text skeletons will automatically catch up with the styles you defined for the title and text elements. 58 | 59 | ## Basic Usage 60 | 61 | First import the Skeletor styles 62 | 63 | ```js 64 | import 'vue-skeletor/dist/vue-skeletor.css'; 65 | ``` 66 | 67 | Option 1 - Register Locally 68 | 69 | ```js 70 | // SomeComponent.vue 71 | import { Skeletor } from 'vue-skeletor'; 72 | 73 | export default { 74 | components: { Skeletor } 75 | } 76 | ``` 77 | 78 | Option 2 - Register Globally 79 | 80 | ```js 81 | // main.js 82 | import { Skeletor } from 'vue-skeletor'; 83 | 84 | app.component(Skeletor.name, Skeletor); 85 | ``` 86 | 87 | ```html 88 | 89 | 90 | ``` 91 | ## Global Configuration 92 | If you want you can globally turn off the shimmer animation using the Skeletor Plugin 93 | usage. 94 | 95 | ```js 96 | // Import the plugin 97 | import VueSkeletor from 'vue-skeletor'; 98 | 99 | // Register plugin in your vue app 100 | app.use(VueSkeletor, { 101 | shimmer: false, 102 | }) 103 | ``` 104 | 105 | ## useSkeletor 106 | When you install skeletor as Plugin it `provides` global config to your app and you get access to 'useSkeletor' composable which will inject the `skeletor` config object through which you can set any global config at runtime. 107 | 108 | ```js 109 | // Import the composable 110 | import { useSkeletor } from 'vue-skeletor'; 111 | 112 | export default defineComponent({ 113 | setup() { 114 | // In your setup function use the composable 115 | const skeletor = useSkeletor(); 116 | 117 | // Set the shimmer config 118 | skeletor.shimmer = false; 119 | } 120 | }) 121 | ``` 122 | 123 | ## Width 124 | `width`: number | string 125 | 126 | ```html 127 | 128 | 129 | 130 | 131 | 132 | 133 | ``` 134 | 135 | Width of your skeleton, can be a number or css string value. 136 | 137 | ## Height 138 | `height`: number | string 139 | 140 | ```html 141 | 142 | 143 | 144 | 145 | 146 | 147 | ``` 148 | 149 | Height of your skeleton, can be a number or css string value. 150 | 151 | ### ⚠️ Notice 152 | When you set height, your skeleton automatically becomes a rect with `display: 153 | block` meaning it will no longer adapt to your typography, which is useful for 154 | creating non text block level skeletons like image placeholders, buttons, and e.t.c. 155 | 156 | ## Size 157 | `size`: number | string 158 | 159 | ```html 160 | 161 | 162 | ``` 163 | 164 | Size sets both `width` & `height` to simplify creating square/circle shapes 165 | 166 | ## Circle 167 | `circle`: boolean (default: false) 168 | 169 | ```html 170 | 171 | 172 | ``` 173 | 174 | As the name suggest it just turns the element into a circle, use only when `width` & `height` or size is set. 175 | 176 | ## Pill 177 | `pill`: boolean (default: false) 178 | 179 | ```html 180 | 181 | ``` 182 | 183 | Makes rectangular skeletons fully rounded, useful when creating rounded button or chip 184 | and e.t.c shapes. 185 | 186 | ## Shimmer 187 | `shimmer`: boolean 188 | 189 | ```html 190 | 191 | ``` 192 | 193 | Optionally you can turn off/on specific skeleton's shimmer animation, it is based of 194 | your global config, if you disable shimmer globally, then this prop should be set 195 | accordingly. 196 | 197 | ## As 198 | `as`: string (default: 'span') 199 | 200 | ```html 201 | 202 | ``` 203 | 204 | By default skeletons are rendered as `span` tags, but you can change it 205 | using this prop. 206 | 207 | ## Customizing the style and animation 208 | Skeletor uses bem classes, that you can use to override your skeletons color and shimmer animation and you have the full control over how your skeletons look, there is no need for any javascript api for this in my opinion. 209 | 210 | ```css 211 | /* Static background */ 212 | .vue-skeletor { 213 | background-color: #ccc; 214 | } 215 | 216 | /* 217 | If you have theme switching in your app for example 218 | from light to dark, you can target skeletons under 219 | some global theme class or attribute e.g. 220 | */ 221 | [data-theme="dark"] .vue-skeletor { 222 | background: #363636; 223 | } 224 | 225 | /* 226 | Text skeleton 227 | By default skeletor uses fully rounded style for text 228 | type skeletons, you can change that as you like 229 | */ 230 | .vue-skeletor--text { 231 | /* Completely square style skeletons */ 232 | border-radius: 0; 233 | } 234 | 235 | /* Shimmer */ 236 | .vue-skeletor:not(.vue-skeletor--shimmerless):after { 237 | /* 238 | Change the shimmer color, its a simple 90 deg 239 | linear horizontal gradient, adjust it however 240 | you like. 241 | */ 242 | background: linear-gradient( 243 | 90deg, 244 | rgba(255,255,255,0) 0%, 245 | rgba(255,255,255,1) 50%, 246 | rgba(255,255,255,0) 100% 247 | ); 248 | 249 | /* Change any css keyframes animation property */ 250 | animation-duration: 2s; 251 | animation-timing-function: ease-in-out; 252 | /* ... */ 253 | 254 | /* 255 | Or implement your custom shimmer css animation 256 | if you want it's pure css no magic happening =) 257 | */ 258 | } 259 | 260 | /* Default keyframes used in skeletor */ 261 | @keyframes shimmer { 262 | 100% { 263 | transform: translateX(100%); 264 | } 265 | } 266 | ``` --------------------------------------------------------------------------------