Sarah Drasner
CSS-Tricks,
IBM,
Microsoft, Salesforce
Smashing Magazine,
NetMag,
Zillow, Workflo,
O’Reilly, Frontend Masters, & Mule Design
Microsoft
Our story starts with performance.
User attention span is short.
until dropoff
Amazon has discovered that for every one second delay, conversions dropped by 7%. If you sell $100k per day, that’s an annual loss of $2.5m.
Walmart has found that it gains 1% revenue increase for every 100ms of improvement.
Humans over-estimate passive waits by 36% - Eli Fitch and Richard Larson, MIT
Your benchmarks aren't telling you the full story.
Viget did an experiment and found that despite some individual variation, novel loaders as a whole had a higher wait time and lower abandon rate than generic ones
Creating Spatial Awareness
“We’ve evolved to perform actions that flow more or less seamlessly.
"We aren’t wired to deal with the fits and starts of human-computer interaction.”
Sensory memory: Your occipital lobe (AKA “the memory store”) works in 100ms bursts.
-Tammy Everts
Spatial or otherwise
Paul Bakaus
From this CSS-Tricks Article
this pen.
If you know the end, you can figure out what comes in between
see the comparison
Hello World!
<div id="app">{{ text }} Nice to meet Vue.</div>
Vanilla JS vs Vue for Conditional Rendering
const items = [
'thingie',
'another thingie',
'lots of stuff',
'yadda yadda'
];
function listOfStuff() {
let full_list = '';
for (let i = 0; i < items.length; i++) {
full_list = full_list + `<li> ${items[i]} </li>`
}
const contain = document.querySelector('#container');
contain.innerHTML = `<ul> ${full_list} </ul>`;
}
listOfStuff();
HTML:
<div id="container"></div>
yields:
This pen
new Vue({
el: '#app',
data: {
items: [
'thingie',
'another thingie',
'lots of stuff',
'yadda yadda'
]
}
});
<div id="app">
<ul>
<li v-for="item in items">
{{ item }}
</li>
</ul>
</div>
yields:
This pen
Creates a relationship between the data in the instance/component and a form input, so you can dynamically update values
Accepting user input and managing it in a responsible manner
new Vue({
el: '#app',
data() {
return {
message: 'This is a good place to type things.'
}
}
});
<div id="app">
<h3>Type here:</h3>
<textarea v-model="message" class="message" rows="5" maxlength="72"/>
<br>
<p class="booktext">{{ message }} </p>
</div>
<div>
<h2>Here I am!</h2>
<slot></slot>
</div>
<div id="app">
<h3>Let's trigger this here modal!</h3>
<button @click="toggleShow">
<span v-if="isShowing">Hide child</span>
<span v-else>Show child</span>
</button>
<app-child v-if="isShowing" class="modal">
<button @click="toggleShow">
Close
</button>
</app-child>
</div>
const Child = {
template: '#childarea'
};
new Vue({
el: '#app',
data() {
return {
isShowing: false
}
},
methods: {
toggleShow() {
this.isShowing = !this.isShowing;
}
},
components: {
appChild: Child
}
});
This pen.
<transition name="fade">
<app-child v-if="isShowing" class="modal">
<button @click="toggleShow">
Close
</button>
</app-child>
</transition>
<app-child v-if="isShowing" class="modal">
<button @click="toggleShow">
Close
</button>
</app-child>
Encapsulate what is changing declaratively
Default 'v-' prefix, otherwise name="foo"
Example:
.v-enter-active {
transition: color 1s ease;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.25s ease-out;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
Reusable for other components
In this case, ease-out, but ease-out, ease-in for more complex effects
This is unnecessary, as it's default:
.fade-enter-to, .fade-leave {
opacity: 1;
}
This pen.
But...
<div v-bind:class="[isShowing ? blurClass : '', bkClass]">
<h3>Let's trigger this here modal!</h3>
<button @click="toggleShow">
<span v-if="isShowing">Hide child</span>
<span v-else>Show child</span>
</button>
</div>
new Vue({
el: '#app',
data() {
return {
isShowing: false,
bkClass: 'bk',
blurClass: 'blur'
}
},
...
});
.bk {
transition: all 0.05s ease-out;
}
.blur {
filter: blur(2px);
opacity: 0.4;
}
This pen.
Transition Modes
🏆
🏆
This pen
Without transition modes
The current element waits until the new element is done transitioning in to fire
The current element transitions out and then the new element transitions in.
<transition name="flip" mode="out-in">
<slot v-if="!isShowing"></slot>
<img v-else src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/cartoonvideo14.jpeg" />
</transition>
HTML
@mixin accelerate($name) {
will-change: $name;
transform: translateZ(0);
backface-visibility: hidden;
perspective: 1000px;
}
.foo {
@include accelerate(transform);
}
enter-active-class="toasty"
leave-active-class="bounceOut"
Still <transition /> component, but
<div id="app">
<h3>Bounce the Ball!</h3>
<button @click="toggleShow">
<span v-if="isShowing">Get it gone!</span>
<span v-else>Here we go!</span>
</button>
<transition
enter-active-class="bouncein"
leave-active-class="rollout">
<div v-if="isShowing">
<app-child class="child"></app-child>
</div>
</transition>
</div>
Bounce a ball
@mixin ballb($yaxis: 0) {
transform: translate3d(0, $yaxis, 0);
}
@keyframes bouncein {
1% { @include ballb(-400px); }
20%, 40%, 60%, 80%, 95%, 99%, 100% { @include ballb() }
30% { @include ballb(-80px); }
50% { @include ballb(-40px); }
70% { @include ballb(-30px); }
90% { @include ballb(-15px); }
97% { @include ballb(-10px); }
}
.bouncein {
animation: bouncein 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
}
.ballmove-enter {
@include ballb(-400px);
}
Keep it DRY
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
:css="false">
</transition>
Custom Naming
<transition
@enter="enterEl"
@leave="leaveEl"
:css="false">
<!-- put element here-->
</transition>
methods: {
enterEl(el, done) {
//entrance animation
done();
},
leaveEl(el, done) {
//exit animation
done();
},
}
This pen.
<textarea class="message" rows="5" v-model.lazy="message" maxlength="72" />
<br>
<button type="submit" class="submit" @click="load = !load">
<span v-if="!load">
Write Me
</span>
<span v-if="load">
Erase
</span>
</button>
<transition @before-enter="beforeEnter" @enter="enter" :css="false">
<p class="booktext" v-if="load">
{{ message }}
</p>
</transition>
new Vue({
el: '#app',
data() {
return {
message: 'This is a good place to type things.',
load: false
}
},
methods: {
beforeEnter(el) {
TweenMax.set(el, {
transformPerspective: 600,
perspective: 300,
transformStyle: "preserve-3d",
autoAlpha: 1
});
},
enter(el, done) {
...
tl.add("drop");
for (var i = 0; i < wordCount; i++) {
tl.from(split.words[i], 1.5, {
z: Math.floor(Math.random() * (1 + 150 - -150) + -150),
ease: Bounce.easeOut
}, "drop+=0." + (i/ 0.5));
...
}
}
});
let tl = new TimelineMax({ onComplete: done });
onComplete: done
done();
or
& Vue's Reactivity System
Reactive programming is programming with asynchronous data streams.
A stream is a sequence of ongoing events ordered in time that offer some hooks with which to observe it.
When we use reactive premises for building applications, this means it's very easy to update state in reaction to events.
More Resources:
Despite the name, React is not Reactive- it uses a "pull" approach (rather than "push")
Vue takes the object, walks through its properties and converts them to getter/setters
new Vue({
data: {
text: 'msg'
}
})
The properties touched by the watcher during the render are registered as dependencies
When the setter is triggered, it lets the watcher know, and causes the component to re-render.
Each component has a watcher instance.
Good for asynchronous updates,
and updates/transitions with data changes
We're going to 'watch' any data property declared on the Vue instance
SVG is good for this because it's built with MATH
<select v-model="selected">
<option v-for="option in options" v-bind:value="option.value">
{{ option.text }}
</option>
</select>
new Vue({
el: '#app',
data() {
return {
selected: [25, 37, 15, 13, 25, 30, 11, 17, 35, 10, 25, 15, 5, 27, 15, 13, 25, 36, 15, 14, 35, 10, 14, 15, 35, 17, 12, 13, 25, 30, 14, 17, 35, 10, 25, 15],
options: [
{ text: 'First Dataset', value: [25, 37, 15, 13, 25, 30, 11, 17, 35, 10, 25, 15, 5, 27, 15, 13, 25, 36, 15, 14, 35, 10, 14, 15, 35, 17, 12, 13, 25, 30, 14, 17, 35, 10, 25, 15] },
]
}
},
});
watch: {
selected: function(newValue, oldValue) {
var tweenedData = {}
var update = function() {
let obj = Object.values(tweenedData);
obj.pop();
this.targetVal = obj;
}
var tweenSourceData = { onUpdate: update, onUpdateScope: this}
for (let i = 0; i < oldValue.length; i++) {
let key = i.toString()
tweenedData[key] = oldValue[i]
tweenSourceData[key] = newValue[i]
}
TweenMax.to(tweenedData, 1, tweenSourceData)
}
}
SVG!
Built with math
<!--xaxis -->
<g targetVal="targetVal" class="xaxis">
<line x1="0" y1="1" x2="350" y2="1"/>
<g v-for="(select, index) in targetVal">
<line y1="0" y2="7" v-bind="{ 'x1':index*10, 'x2':index*10 }"/>
<text v-if="index % 5 === 0" v-bind="{ 'x':index*10, 'y':20 }">{{ index }}</text>
</g>
</g>
Flexible
great case for SVG
Entire filesize: 6KB!
You never have to worry about positioning in CSS
this pen.
Emotions are tied to your limbic system and easier to remember
This pen.
<div id="app" @mousemove="coordinates">
coordinates(e) {
const audio = new Audio('https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/Whoa.mp3'),
walleBox = document.getElementById('walle').getBoundingClientRect(),
walleCoords = walleBox.width / 2 + walleBox.left;
...
TweenMax.set("#eyes", {
scaleX: 1 + (1 - e.clientX / walleCoords) / 5
});
TweenMax.set("#walle", {
x: ((e.clientX / walleCoords) * 50) - 40
});
this.startArms.progress(1 - (e.clientX / walleCoords)).pause();
}
},
In <template>
In Vue Instance
clipPath- great support
Interpolation with style bindings- this pen
In the instance:
new Vue({
el: '#app',
data() {
return {
x: 0,
y: 0
}
},
methods: {
coords(e) {
this.x = e.clientX / 10;
this.y = e.clientY / 10;
},
}
})
In the template:
<div id="contain" :style="{ perspectiveOrigin: `${x}% ${y}%` }">
<div class="square square2">
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 419.9 421.9"
preserveAspectRatio="none">
This pen.
createBigCircles() {
const svgNS = this.$refs.figure.namespaceURI;
this.$refs.patterngroup.innerHTML = '';
for (let i = 0; i < this.numLines/2; i++) {
let circ = document.createElementNS(svgNS, 'circle');
this.append(this.$refs.patterngroup, circ);
this.setAttributes(circ, {
'cx': this.size/2,
'cy': this.size/2,
'r': this.totesRando(this.size/2, 0),
'fill': 'none',
'stroke': this.gradients2[this.totesRando(1, 0)],
'stroke-width': 1
});
}
},
<div class="formarea">
<h3>Create Circles:</h3>
<button @click="createSmCircles">Make small circles</button>
<button @click="createBigCircles">Make big circles</button>
</div>
animation() {
let tl = new TimelineMax()
tl.add('begin')
tl.to('line', 2, {
rotation: 360,
repeat: -1,
transformOrigin: '50% 50%',
ease: Sine.easeOut
}, 'begin')
...
return tl;
},
pauseAnim() {
var tl = TimelineLite.exportRoot();
tl.pause(0);
},
<div class="formarea">
<h3>Animation:</h3>
<button @click="animation">Play Animation</button>
<button @click="pauseAnim">Stop Animation</button>
</div>
This pen.
data() {
return {
total: 200,
radius: 15,
}
},
methods: {
incrementHeight() {
this.total += 100
},
incrementRadius() {
this.radius += 1
},
bounceBall() {
this.vy += this.g; // gravity increases the vertical speed
this.x1 += this.vx; // horizontal speed inccrements horizontal position
this.y1 += this.vy; // vertical speed increases vertical position
if (this.y1 > this.total - this.radius) { // if ball hits the ground
this.y1 = this.total - this.radius; // reposition it at the ground
this.vy *= -0.8; // then reverse and reduce its speed
}
},
...
animateBall() {
//use rAF to animate but put a boundary on it so it doesn't run forever
let start,
vueThis = this;
this.running = true;
function step(timestamp) {
if (!start) start = timestamp;
var progress = timestamp - start;
if (progress < 13000) {
vueThis.bounceBall();
vueThis.req = window.requestAnimationFrame(step);
} else {
vueThis.x1 = this.radius;
vueThis.y1 = this.radius;
vueThis.running = false;
}
}
this.req = window.requestAnimationFrame(step);
},
<button @click="animateBall" v-if="!running">Start</button>
Encapsulate what is changing - repo
Encapsulate what is changing- Vuex
export const store = new Vuex.Store({
state: {
showWeather: false,
template: 0
},
mutations: {
toggle: state => state.showWeather = !state.showWeather,
updateTemplate: (state) => {
state.showWeather = !state.showWeather;
state.template = (state.template + 1) % 4;
}
}
});
<transition @leave="leaveDialog" :css="false">
<app-dialog v-if="showWeather"></app-dialog>
</transition>
<transition @leave="leaveDroparea" :css="false">
<g v-if="showWeather">
<app-droparea v-if="template == 1"></app-droparea>
<app-windarea v-else-if="template == 2"></app-windarea>
<app-rainbowarea v-else-if="template == 3"></app-rainbowarea>
<app-tornadoarea v-else></app-tornadoarea>
</g>
</transition>
export default {
computed: {
template() {
return this.$store.state.template;
}
},
methods: {
toggle() {
this.$store.commit('toggle');
}
},
mounted() {
//enter weather
const tl = new TimelineMax();
tl.add("enter");
tl.fromTo("#dialog", 2, {
opacity: 0
}, {
opacity: 1
}, "enter");
tl.fromTo("#dialog", 2, {
rotation: -4
}, {
rotation: 0,
transformOrigin: "50% 100%",
ease: Elastic.easeOut
}, "enter");
}
}
Lifecycle hooks
This pen.
const Child = {
beforeCreate() {
console.log("beforeCreate!");
},
...
};
Vue.directive('tack', {
bind(el, binding, vnode) {
el.style.position = 'fixed'
}
});
<p v-tack>I will now be tacked onto the page</p>
Vue.directive('tack', {
bind(el, binding, vnode) {
el.style.position = 'fixed'
el.style.top = binding.value + 'px'
}
});
<div id="app">
<p>Scroll down the page</p>
<p v-tack="70">Stick me 70px from the top of the page</p>
</div>
Vue.directive('tack', {
bind(el, binding, vnode) {
el.style.position = 'fixed';
const s = (binding.arg == 'left' ? 'left' : 'top');
el.style[s] = binding.value + 'px';
}
});
<p v-tack:left="70">I'll now be offset from the left instead of the top</p>
Pass an argument
Vue.directive('tack', {
bind(el, binding, vnode) {
el.style.position = 'fixed';
el.style.top = binding.value.top + 'px';
el.style.left = binding.value.left + 'px';
}
});
<p v-tack="{ top: '40', left: '100' }">Stick me 40px from the top of the
page and 100px from the left of the page</p>
More than one value
Vue.directive('scroll', {
inserted: function(el, binding) {
let f = function(evt) {
if (binding.value(evt, el)) {
window.removeEventListener('scroll', f);
}
};
window.addEventListener('scroll', f);
},
});
// main app
new Vue({
el: '#app',
methods: {
handleScroll: function(evt, el) {
if (window.scrollY > 50) {
TweenMax.to(el, 1.5, {
y: -10,
opacity: 1,
ease: Sine.easeOut
})
}
return window.scrollY > 100;
}
}
});
<div class="box" v-scroll="handleScroll">
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. A atque amet harum aut ab veritatis earum porro praesentium ut corporis. Quasi provident dolorem officia iure fugiat, eius mollitia sequi quisquam.</p>
</div>
Custom Directives + D3
export default {
methods: {
totalImpact: function(evt, el) {
if (window.scrollY > 1100) {
TweenMax.to(el, 0.75, {
opacity: 0
})
let circ = d3.selectAll("circle")
.attr("cx", function(d) {
let lat = d["Longitude (Deg)"];
if (lat.includes("E")) {
return midX - parseInt(lat) * incByW;
} else {
return midX + (parseInt(lat) * incByW);
}
})
...
.attr("r", 5)
.attr("fill", "url(#radgrad)")
}
return window.scrollY > 1300;
},
Update the circle's coordinates
<div class="box accelerate impact" v-dscroll="totalImpact">
<h3>Total Impact</h3>
<p>Most alksdjflkjasd laksdjfl;kasjdf laksd falksdjf lsdj f</p>
</div>
By rendering on the server, you can cache the final shape of your data
-Karl Seguin
npm install -g vue-cli
--------
vue init nuxt/starter my-project
cd my-project
yarn
npm run dev
Templates in the pages directory
<nuxt-link to="/product">Product</nuxt link>
Transition hook already available
name="page"
.page-enter-active, .page-leave-active {
transition: all .25s ease-out;
}
.page-enter, .page-leave-active {
opacity: 0;
transform: scale(0.95);
transform-origin: 50% 50%;
}
Animation as well
.page-enter-active {
animation: acrossIn .45s ease-out both;
}
.page-leave-active {
animation: acrossOut .65s ease-in both;
}
export default {
transition: {
mode: 'out-in',
css: false,
enter (el, done) {
let tl = new TimelineMax({ onComplete: done }),
spt = new SplitText('h1', {type: 'chars' }),
chars = spt.chars;
TweenMax.set(chars, {
transformPerspective: 600,
perspective: 300,
transformStyle: 'preserve-3d'
})
tl.add('start')
tl.from(el, 0.8, {
scale: 0.9,
transformOrigin: '50% 50%',
ease: Sine.easeOut
}, 'start')
...
tl.timeScale(1.5)
}
...
make it extraordinarily simple
to create complex and beautiful interactions
that feel seamless for our users.
We can connect states and reduce cognitive load for things that are changing in our application with ease.
@sarah_edo on twitter
These slides:
slides.com/sdrasner/animating-vue-keynote