Skip to content

Instantly share code, notes, and snippets.

@Poordeveloper
Created October 22, 2015 03:45
Show Gist options
  • Save Poordeveloper/25f5510c1111135a7f44 to your computer and use it in GitHub Desktop.
Save Poordeveloper/25f5510c1111135a7f44 to your computer and use it in GitHub Desktop.
Meaningful Transitions
<!--
Based on the contacts video from Meaningful Transitions
https://www.google.com/design/spec/animation/meaningful-transitions.html#
-->
<!-- Centered wrapper -->
<div class="app-wrapper">
<!-- Header for the 'app' -->
<header class="app-bar">
<button id="menuToggle" class="app-bar-button menu-toggle menu-is-closed"><i class="fa fa-bars"></i></button>
<h1 id="appHeadline" class="app-headline">All Contacts</h1>
<button id="sortToggle" class="app-bar-button sort-toggle"><i class="fa fa-sort-alpha-asc"></i></button>
</header>
<!-- END 'app' Header -->
<!-- Table of contacts -->
<div class="app-body">
<!-- List of contacts, these are built dynamicly by javascript on page load -->
<table id="contactList" class="contact-list">
<tbody>
</tbody>
</table>
<!-- END List of contacts -->
<!-- Contact Info (Details), hidden by default, visible when a contact is selected -->
<!-- This info is not contact item specific so it is not loaded dynamically -->
<table id="contactInfo" class="contact-info">
<tbody>
<tr id="contactUsername" class="contact-info-item"><td class="contact-info-icon"><i class="fa fa-comment-o"></i></td><td class="contact-info-detail">joedoe</td></tr>
<tr id="contactEmail" class="contact-info-item"><td class="contact-info-icon"><i class="fa fa-envelope-o"></i></td><td class="contact-info-detail">[email protected]</td></tr>
<tr id="contactHomeNumber" class="contact-info-item"><td class="contact-info-icon"><i class="fa fa-phone"></i></td><td class="contact-info-detail">(555) 987-1234</td></tr>
<tr id="contactWorkNumber" class="contact-info-item"><td class="contact-info-icon"><i class="fa fa-building-o"></i></td><td class="contact-info-detail">(555) 987-1234</td></tr>
<tr id="contactAddress" class="contact-info-item"><td class="contact-info-icon"><i class="fa fa-home"></i></td><td class="contact-info-detail">123 Elm Street</td></tr>
</tbody>
</table>
<!-- End Contact Info (Details) -->
</div>
</div>
// Utility Function
// Takes any collection with a length property that can be
// emumerated numerically. Each item has it's own callback
function forEach(collection, action, scope) {
for (var i = 0; i < collection.length; i++) {
action.call(scope, collection[i], i);
}
}
// Collection of names and randomly generated colors
// randomColor() is from http://llllll.li/randomColor/
var contactInformation = [{'firstName': 'Micheal', 'lastName': 'Carswell', 'color': randomColor({luminosity : 'light'})},
{ 'firstName': 'Jed', 'lastName': 'Cherry', 'color': randomColor({luminosity : 'light'})},
{ 'firstName': 'Freddie', 'lastName': 'Crimmins', 'color': randomColor({luminosity : 'light'})},
{ 'firstName': 'Dimple', 'lastName': 'Deloatch', 'color': randomColor({luminosity : 'light'})},
{ 'firstName': 'Tomas', 'lastName': 'Duhn', 'color': randomColor({luminosity : 'light'})},
{ 'firstName': 'Coralee', 'lastName': 'Earheart', 'color': randomColor(), 'color': randomColor({luminosity : 'light'})},
{ 'firstName': 'Solomon', 'lastName': 'Magruder', 'color': randomColor({luminosity : 'light'})},
{ 'firstName': 'Antionette', 'lastName': 'May', 'color': randomColor({luminosity : 'light'})},
{ 'firstName': 'Illa', 'lastName': 'Schwindt', 'color': randomColor({luminosity : 'light'})},
{ 'firstName': 'Jesica', 'lastName': 'Utt', 'color': randomColor({luminosity : 'light'})}];
// Adds a contact to the contact list using the data above
// Structure
// <tbody>
// <tr class="contact">
// ...
// </tr>
//
// <tr class="contact">
// <td class="contact-image">
// <div data-image-color="#xxxxxx"></div>
// </td>
// <td class="contact-name">
// <span class="first-name">
// <span class="last-name">
// <td>
// </tr>
//
// <tr class="contact">
// ...
// </tr>
// </tbody>
var contactList = document.querySelector('#contactList tbody');
function addContact(contactInfo) {
var contact = document.createElement('tr'),
contactImgWrapper = document.createElement('td'),
contactImg = document.createElement('div'),
contactName = document.createElement('td'),
firstName = document.createElement('span'),
lastName = document.createElement('span');
// Add classes to each element in a contact <tr>
contact.classList.add('contact');
contactName.classList.add('contact-name');
firstName.classList.add('first-name');
lastName.classList.add('last-name');
contactImgWrapper.classList.add('contact-image');
// Add the color to the contact-color <td> -> <div>
contactImg.style.backgroundColor = contactInfo['color'];
contactImg.setAttribute('data-image-color', contactInfo['color']);
// Append each element to the contact <tr>
// starting with the inner most element
firstName.appendChild(document.createTextNode(contactInfo['firstName']));
lastName.appendChild(document.createTextNode(contactInfo['lastName']));
contactImgWrapper.appendChild(contactImg);
contact.appendChild(contactImgWrapper);
contactName.appendChild(firstName);
contactName.appendChild(document.createTextNode(' '));
contactName.appendChild(lastName);
contact.appendChild(contactName);
// Append the contact to the contact list
contactList.appendChild(contact);
}
// Use the contactInformation collection to build the contact list
forEach(contactInformation, function(contactInfo) {
addContact(contactInfo);
});
// Event listener for table cells
// Use CSS directly to bring the selected tab into focus
// Rely on CSS Transitions for the animation
function focusSelectedContact(event) {
var currentTarget = event.currentTarget,
appBody = document.querySelector('.app-body'),
rect = currentTarget.getBoundingClientRect(),
appBarRect = appBody.getBoundingClientRect(),
translate = appBarRect.top - rect.top + 171,
root = document.querySelector('html'),
menuToggleIcon = document.querySelector('#menuToggle i'),
contactInfo = document.querySelector('.contact-info'),
color = currentTarget.querySelector('.contact-image div').getAttribute('data-image-color');
// Add the initial styles to the selected contact
currentTarget.classList.remove('previously-selected');
currentTarget.classList.add('selected-contact');
// Hide the contacts that weren't clicked
root.classList.add('hide-contacts');
root.classList.remove('show-contacts');
// Reposition the selected contact table cell
currentTarget.style.webkitTransform = 'translateY(' + translate +'px)';
currentTarget.style.transform = 'translateY(' + translate +'px)';
currentTarget.offsetHeight; // Force a redraw so the animation works
// Apply a gradient to the table's background
appBody.style.backgroundImage = 'linear-gradient(' + color + ' 0%, #fff 100%)';
appBody.style.backgroundPosition = '0 0';
appBody.style.overflow = 'hidden';
// Change the menu button icons
menuToggleIcon.classList.remove('fa-bars');
menuToggleIcon.classList.add('fa-arrow-left');
contactInfo.classList.add('visible');
}
// Attach the focusSelectedContact event listener to each table cell
forEach(document.querySelectorAll('.contact'), function(contact) {
contact.addEventListener('click', focusSelectedContact);
});
// Event listener for the menu button
// Undo all of the css set by focusSelectedContact
function showAllContacts() {
var appBody = document.querySelector('.app-body'),
selectedContact = document.querySelector(".selected-contact"),
menuToggleIcon = document.querySelector('#menuToggle i'),
contactInfo = document.querySelector('.contact-info');
// Slide the selected contact back into its original position
selectedContact.style.webkitTransform = '';
selectedContact.style.transform = '';
selectedContact.offsetHeight; // Force a redraw so the browser doesn't skip the animation
// Remove the gradient on the table's background
appBody.style.backgroundPosition = '';
appBody.style.overflow = 'auto';
// Revert the menu button icons
menuToggleIcon.classList.add('fa-bars');
menuToggleIcon.classList.remove('fa-arrow-left');
contactInfo.classList.remove('visible');
// After the selected contact is in position (the transition is complete)
// display the other contacts again
setTimeout(function() {
var root = document.querySelector('html');
root.classList.add('show-contacts');
root.classList.remove('hide-contacts');
selectedContact.classList.remove('selected-contact');
}, 250);
}
// Attach the showAllContacts event listener to the menu button
document.querySelector('#menuToggle').addEventListener('click', showAllContacts);
<script src="//cdnjs.cloudflare.com/ajax/libs/randomcolor/0.1.1/randomColor.min.js"></script>
/* Import the Google Font 'Roboto' */
@import url(http://fonts.googleapis.com/css?family=Roboto:400,700);
/* Hide scrollbars in Webkit browsers*/
::-webkit-scrollbar {
display: none;
}
/* Dimensions and base styles for the containers and headers */
body {
background-color: #f9f9f9;
font-family: 'Roboto', sans-serif;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.app-wrapper {
background-color: #fff;
font-family: sans-serif;
margin: 50px auto;
overflow: hidden;
position: relative;
width: 400px;
}
.app-bar {
background-color: #f2f2f2;
box-shadow: 0 2px 2px #c6c6c6;
box-sizing: border-box;
padding: 15px 70px;
position: absolute;
width: 100%;
z-index: 3;
transition: background-color 500ms, box-shadow 500ms;
}
/* Declarations that hide the header when a contact is selected */
.app-headline {
color: #5f5f5f;
cursor: default;
display: inline-block;
font-family: 'Roboto', sans-serif;
font-size: 24px;
font-weight: normal;
margin: 0;
opacity: 1;
transition: opacity 500ms;
}
.app-bar-button {
background: none;
border: none;
color: #b1b1b1;
height: 30px;
outline: none;
padding: 2px 0 3px;
text-shadow: 0 0 transparent;
transition: color 500ms, text-shadow 500ms;
width: 30px;
}
.app-bar-button:hover {
color: #c1c1c1;
}
.app-bar-button:active {
color: #919191;
}
#menuToggle .fa {
font-size: 2.25em;
}
#sortToggle .fa {
font-size: 1.75em;
}
.menu-toggle {
left: 13px;
position: absolute;
top: calc(50% - 13px);
}
.sort-toggle {
position: absolute;
right: 13px;
top: calc(50% - 13px);
}
.app-body {
background-image: linear-gradient(transparent, transparent);
background-position: 0 -300px;
background-repeat: no-repeat;
background-size: 100% 238px;
height: 450px;
-ms-overflow-style: none;
overflow-y: auto;
overflow-x: hidden;
padding: 62px 0 0;
transition: background-position 500ms;
}
.app-body::-webkit-scrollbar {
display: none;
}
.contact-list {
display: block;
padding: 2px;
}
.contact-list tbody {
display: -webkit-flex;
display: -ms-flex;
display: flex;
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
}
/* END Dimensions and base styles for the containers and headers */
/* Declarations for when the header is set to be hidden */
.hide-contacts .app-bar {
background-color: transparent;
box-shadow: 0 0 0 transparent;
}
.hide-contacts #menuToggle {
cursor: pointer;
}
.hide-contacts .app-headline {
opacity: 0;
}
.hide-contacts .app-bar-button {
color: #fff;
text-shadow: 1px 1px #c1c1c1;
}
/* END Declarations for when the header is set to be hidden */
/* Contact table cell styles */
.contact {
border-bottom: 1px solid #f5f5f5;
cursor: pointer;
position: relative;
display: block;
-webkit-flex: 1 0 100%;
-ms-flex: 1 0 100%;
flex: 1 0 100%;
opacity: 1;
overflow: hidden;
-webkit-transition-property: opacity, top, -webkit-transform;
-webkit-transition-property: opacity, top, transform;
-webkit-transition-duration: 500ms;
transition-property: opacity, top, transform;
transition-duration: 500ms;
-webkit-transform: translateY(0);
transform: translateY(0);
}
.contact-image div {
border-radius: 50%;
display: inline-block;
height: 35px;
margin: 10px 13px;
width: 35px;
vertical-align: middle;
}
.contact-name {
color: #515151;
cursor: inherit;
font-family: 'Roboto', sans-serif;
font-size: 16px;
position: relative;
z-index: 2;
-webkit-transition: -webkit-transform 500ms;
transition: transform 500ms;
}
.contact-name .last-name {
color: #4d4d4d;
font-weight: bold;
}
.selected-contact {
cursor: default;
pointer-events: none;
}
.selected-contact .contact-name {
-webkit-transform: scale(1.2) translateX(9px);
transform: scale(1.2) translateX(9px);
}
.hide-contacts .contact:not(.selected-contact) {
pointer-events: none;
-webkit-transform: translateY(50px);
transform: translateY(50px);
opacity: 0;
}
/* END Contact table cell styles */
/* Contact info styles, hidden by default*/
.contact-info {
background-color: #fff;
font-size: 18px;
min-height: 20px;
opacity: 0;
position: absolute;
top: 233px;
-webkit-transform: translateY(275px);
transform: translateY(275px);
-webkit-transition: -webkit-transform 500ms, opacity 1000ms;
-webkit-transition: transform 500ms, opacity 1000ms;
transition: transform 500ms, opacity 1000ms;
width: 100%;
z-index: 1;
}
.contact-info.visible {
opacity: 1;
-webkit-transform: translateY(0);
transform: translateY(0);
}
.contact-info {
color: #484848;
}
.contact-info-item {
border-bottom: 1px solid #f5f5f5;
display: block;
}
.contact-info-icon {
height: 35px;
padding: 10px 13px;
width: 35px;
text-align: center;
}
.contact-info-icon i {
font-size: 1.5em;
vertical-align: middle;
}
.contact-info-detail {
padding-left: 5px;
}
/* END Contact info styles */
<link href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.1.0/css/font-awesome.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment