A contacts list based on a video from Google's Material Design spec.
A Pen by Kyle Edwards on CodePen.
<!-- | |
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> |
A contacts list based on a video from Google's Material Design spec.
A Pen by Kyle Edwards on CodePen.
// 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" /> |