<div id="app">
<div class="blanket" v-if="!loaded"></div>
<button class="mail-open" v-on:click="openInbox" v-if="!messages.length"><div class="icon icon--mail">
<svg width="100%" viewBox="0 0 24 24">
<path d="M20,8L12,13L4,8V6L12,11L20,6M20,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V6C22,4.89 21.1,4 20,4Z" />
</svg>
</div>
</button>
<div class="mail-inbox" v-if="messages.length">
<header class="mail-screen-header">
<button class="mail-close" v-on:click="closeInbox"><svg class='icon icon--back' preserveAspectRatio="xMinYMin" height="24" width="24" viewBox="0 0 24 24">
<path d="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z"></path>
</svg>
</button>
<h1 class="mail-header__title">Inbox</h1>
<button class="mail-message__action" v-on:click="openSettings"><svg preserveAspectRatio="xMinYMin" class="icon icon--gear" height="24" width="24" viewBox="0 0 24 24">
<path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" />
</svg>
</button>
</header>
<div class="mail-settings" v-if="openingSettings || settingsOpen">
<header class="mail-screen-header">
<button class="mail-close" v-on:click="closeSettings"><svg preserveAspectRation="xMinYMin" class="icon icon--close" height="24" width="24" viewBox="0 0 24 24">
<path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
</svg>
</button>
<h1 class="mail-header__title">Settings</h1>
</header>
<div class="mail-screen-content">
<div class="mail-settings__setting">
<input type="color" id="theme" :value="theme" v-on:change="updateTheme"/>
<label for="theme">Theme</label>
</div>
</div>
</div>
<div class="mail-container" ref="mailContainer" v-if="activeMessage" v-bind:class="{'mail-message--opening': activeMessage && !messageOpen, 'mail-message--open': messageOpen}">
<header class="mail-screen-header mail-screen-header--fake" ref="fakeHeader">
<button class="mail-close" v-on:click="closeInbox"><svg class='icon icon--back' preserveAspectRatio="xMinYMin" height="24" width="24" viewBox="0 0 24 24">
<path d="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z"></path>
</svg>
</button>
<h1 class="mail-header__title">Inbox</h1>
<button class="mail-message__action" v-on:click="openSettings"><svg preserveAspectRatio="xMinYMin" class="icon icon--gear" height="24" width="24" viewBox="0 0 24 24">
<path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" />
</svg>
</button>
</header>
<ul class="fake-messages fake-messages--top" ref="fakeTop" v-if="activeMessage && !messageOpen" v-bind:style="{bottom: fakeOnePos}">
<li class="mail-message" v-for="(message, idx) in messages.slice(activeMessageIndex - 8 > 0 ? activeMessageIndex - 8 : 0, activeMessageIndex)">
<button class="mail-message"><img class="mail-message__avatar" :src="message.from.avatar"/>
<div class="mail-message__info">
<div class="mail-message__sender">{{message.from.name}}</div>
<div class="mail-message__subject">{{message.subject}}</div>
<div class="mail-message__timestamp">{{message.received}}</div>
</div>
</button>
</li>
</ul>
<ul class="fake-messages fake-messages--bottom" ref="fakeBottom" v-if="activeMessage && !messageOpen" v-bind:style="{top: fakeTwoPos}">
<li class="mail-message" v-for="(message, idx) in messages.slice(activeMessageIndex + 1, activeMessageIndex + 9 < messages.length ? activeMessageIndex + 9 : messages.length)">
<button class="mail-message"><img class="mail-message__avatar" :src="message.from.avatar"/>
<div class="mail-message__info">
<div class="mail-message__sender">{{message.from.name}}</div>
<div class="mail-message__subject">{{message.subject}}</div>
<div class="mail-message__timestamp">{{message.received}}</div>
</div>
</button>
</li>
</ul>
<header class="mail-screen-header mail-container__header">
<button class="mail-message__action" v-on:click="closeMessage"><svg class='icon icon--back' preserveAspectRatio="xMinYMin" height="24" width="24" viewBox="0 0 24 24">
<path d="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z"></path>
</svg>
</button>
<button class="mail-message__action"><svg preserveAspectRatio="xMinYMin" class='icon icon--reply' height="24" width="24" viewBox="0 0 24 24">
<path d="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9Z"></path>
</svg>
</button>
<button class="mail-message__action"><svg preserveAspectRatio="xMinYMin" class='icon icon--star' height="24" width="24" viewBox="0 0 24 24">
<path d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z"></path>
</svg>
</button>
<button class="mail-message__action"><svg preserveAspectRatio="xMinYMin" class="icon icon--trash" height="24" width="24" viewBox="0 0 24 24">
<path d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z" />
</svg>
</button>
<button class="mail-message__action"><svg preserveAspectRatio="xMinYMin" class='icon icon--warning' height="24" width="24" viewBox="0 0 24 24">
<path d="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z"></path>
</svg>
</button>
</header>
<div class="mail-message__header mail-container__message-header" ref="mailHeader" v-bind:style="{top: messageTop + 'px'}"><img class="mail-message__avatar" ref="mailAvatar" :src="activeMessage.from.avatar"/>
<div class="mail-message__info">
<div class="mail-message__sender" ref="mailSender">{{activeMessage.from.name}}</div>
<div class="mail-message__subject" ref="mailSubject">{{activeMessage.subject}}</div>
<div class="mail-message__timestamp" ref="mailTimestamp">{{activeMessage.received}}</div>
</div>
</div>
<article class="mail-message__message" ref="mailContent">{{activeMessage.message}}</article>
</div>
<ul class="mail-messages mail-messages--main" v-bind:class="{'mail-messages--load': !mailOpened}">
<li v-for="(message, idx) in messages">
<button class="mail-message" :id="'message--' + idx" v-on:click="openMessage(message, idx)"><img class="mail-message__avatar" :src="message.from.avatar"/>
<div class="mail-message__info">
<div class="mail-message__sender">{{message.from.name}}</div>
<div class="mail-message__subject">{{message.subject}}</div>
<div class="mail-message__timestamp">{{message.received}}</div>
</div>
</button>
</li>
</ul>
<button class="mail-compose-button" v-on:click="openComposer" v-if="!composerOpen"><svg class='icon icon--compose' preserveAspectRatio="xMinYMin" height="24" width="24" viewBox="0 0 24 24">
<path d="M20.71,4.04C21.1,3.65 21.1,3 20.71,2.63L18.37,0.29C18,-0.1 17.35,-0.1 16.96,0.29L15,2.25L18.75,6M17.75,7L14,3.25L4,13.25V17H7.75L17.75,7Z"></path>
</svg>
</button>
<div class="mail-composer" v-if="openingComposer || composerOpen">
<header class="mail-screen-header">
<button class="mail-message__action" v-on:click="closeComposer"><svg preserveAspectRation="xMinYMin" class="icon icon--close" height="24" width="24" viewBox="0 0 24 24">
<path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
</svg>
</button>
<h1 class="mail-header__title mail-composer__title">Compose</h1>
<button class="mail-message__action"><svg preserveAspectRatio="xMinYMin" class='icon icon--paperclip' height="24" width="24" viewBox="0 0 24 24">
<path d="M7.5,18A5.5,5.5 0 0,1 2,12.5A5.5,5.5 0 0,1 7.5,7H18A4,4 0 0,1 22,11A4,4 0 0,1 18,15H9.5A2.5,2.5 0 0,1 7,12.5A2.5,2.5 0 0,1 9.5,10H17V11.5H9.5A1,1 0 0,0 8.5,12.5A1,1 0 0,0 9.5,13.5H18A2.5,2.5 0 0,0 20.5,11A2.5,2.5 0 0,0 18,8.5H7.5A4,4 0 0,0 3.5,12.5A4,4 0 0,0 7.5,16.5H17V18H7.5Z" />
</svg>
</button>
<button class="mail-message__action"><svg preserveAspectRatio="xMinYMin" class='icon icon--send' height="24" width="24" viewBox="0 0 24 24">
<path d="M2,21L23,12L2,3V10L17,12L2,14V21Z" />
</svg>
</button>
</header>
<form class="mail-screen-content">
<input type="text" placeholder="To"/>
<input type="text" placeholder="Subject"/>
<textarea type="text" placeholder="Compose"></textarea>
</form>
</div>
</div>
</div>
@import url('https://fonts.googleapis.com/css?family=Lato:300,400')
$bg = #6c7a89
$appWhite = #ecf0f1
$appAccent = #a7dde9
$textColor = #707070
$borderColor = lighten(#b5c5c9, 50%)
$textColorSecondary = #7e7e7e
$animDuration = .15s
:root
--accent $appAccent
*
box-sizing border-box
outline 0
html
body
align-items center
background linear-gradient(45deg, #f39c12, #913d88)
color $textColor
display flex
flex-direction column
font-size 16px
font-family 'Lato', sans-serif
justify-content center
margin 0
overflow auto
padding 0
width 100vw
min-height 100vh
label
color $textColor
input
label
cursor pointer
input[type="color"]
height 40px
width 40px
border 0
margin-right 10px
form
width 100%
padding 0 10px 10px
display flex
flex-direction column
margin 0
input
textarea
border-radius 4px
border 0
margin-bottom 4px
padding 4px 12px
font-family 'Lato', sans-serif
width 100%
line-height 30px
&::placeholder
color #c3c3c3
textarea
flex 1
resize none
button
background-color var(--accent)
cursor pointer
border 0
display flex
justify-content center
align-items center
.blanket
background-color $appAccent
z-index 10
position absolute
top 0
right 0
bottom 0
left 0
&:after
box-sizing border-box
content ''
height 50px
width 50px
border 5px solid white
border-radius 100%
border-left-color transparent
border-right-color transparent
position absolute
top 50%
left 50%
margin-left -25px
margin-top -25px
animation rotate .5s infinite linear
.mail-screen-content
// padding 10px
padding-left 10px
width 100%
height 100%
overflow auto
position relative
.icon
fill $appWhite
position absolute
height 24px
width 24px
&:hover
fill darken($appWhite, 2%)
#app
height 100vh
width 100vw
position relative
background-color var(--accent)
overflow hidden
@media(min-width 768px)
height 480px
width 320px
.mail-inbox
height 100%
background-color $appWhite
.mail-settings
height 100%
width 100%
background-color $appWhite
position absolute
top 0
left 0
z-index 3
&__setting
display flex
flex-direction row
align-items center
.mail-close
position absolute
left 0
height 50px
width 50px
.mail-header__title
margin 0
padding 0
line-height 50px
font-size 1.2rem
color $appWhite
flex 1
text-align left
padding-left 54px
.mail-messages
.fake-messages
background $appWhite
padding 0
margin 0
list-style none
overflow auto
position absolute
top 50px
left 0
right 0
bottom 0
padding-bottom 50px
.fake-messages
bottom auto
z-index 2
top auto
padding 0
/**
* Mail messages should animate on first load
*/
.mail-messages--load
.mail-message
opacity 0
animation fadeIn $animDuration ease 0s
animation-fill-mode forwards
for $message in (1..15)
li:nth-child({$message})
button
animation-delay ($message * .1s)
.mail-screen-header
background-color var(--accent)
position relative
height 50px
display flex
align-items center
justify-content flex-end
overflow hidden
width 100%
button:first-child
position absolute
left 0
&--fake
z-index 3
/**
* Message styling
*/
.mail-message--focus .mail-message__info
height 60px
padding-right 0
.mail-message__subject
white-space pre-wrap
.mail-container
background $appWhite
position absolute
top 0
right 0
bottom 0
left 0
&__header
position absolute
top 0
&__message-header
padding 10px 0 10px 10px
top 0
.mail-message__avatar
height 50px
width 50px
.mail-message__subject
overflow visible
white-space initial
overflow initial
font-weight bolder
font-size 1.25rem
.mail-message__sender
font-size 0.75rem
.mail-message__timestamp
transform translateY(-20px)
button.mail-message
padding 0
padding-left 10px
&:hover
background-color darken($appWhite, 2%)
.mail-message
background-color $appWhite
height 60px
width 100%
&__action
height 50px
width 50px
&--opening
&--open
position absolute
z-index 3
border 0
&__header
display flex
flex-direction row
align-items center
min-height 60px
position absolute
width 100%
padding-left 10px
background $appWhite
top 50px
&__content
overflow auto
&__message
color $textColorSecondary
font-weight 300
padding 0 10px 50px 10px
opacity 0
overflow auto
position absolute
top 100%
&__avatar
height 34px
width 34px
background-color var(--accent)
border-radius 100%
overflow hidden
&__info
display flex
flex-direction column
align-items flex-start
flex 1
padding-left 10px
min-height 60px
justify-content center
padding-right 60px
position relative
overflow hidden
&__timestamp
height 50px
width 50px
position absolute
right 10px
top 5px
font-size 0.75rem
display flex
align-items center
justify-content center
color $textColor
&__sender
font-size 1rem
font-weight 400
color $textColor
&__subject
font-size .75rem
font-weight 300
color $textColorSecondary
white-space nowrap
text-overflow ellipsis
overflow hidden
width 100%
text-align left
.mail-compose-button
height 44px
width 44px
background-color var(--accent)
border-radius 100%
border 0
cursor pointer
bottom 10px
right 10px
position absolute
z-index 2
box-shadow 0 2px 2px #c3c3c3
.mail-composer
height 44px
width 44px
border-radius 100%
border none
position absolute
bottom 10px
right 10px
transform-origin bottom right
background-color var(--accent)
display flex
flex-direction column
z-index 4
.mail-open
height 50px
width 50px
position absolute
top 50%
left 50%
margin-left -25px
margin-top -25px
border-radius 100%
border 5px solid $appWhite
animation pulse 4s infinite ease
background-color var(--accent)
.icon:hover
fill $appWhite
@keyframes pulse
0%, 100%
transform scale(1)
box-shadow 0px 2px 2px black
50%
transform scale(1.05)
box-shadow 0px 4px 4px black
@keyframes fadeIn
from
opacity 0
to
opacity 1
@keyframes rotate
to
transform rotate(360deg)
View Compiled
const ANIMATION_DURATION = 0.25
const CLASSES = {
COMPOSER: 'mail-composer',
HEADER: 'mail-screen-header',
SETTINGS: 'mail-settings',
}
let messages = []
for (let i = 0; i < 15; i++) {
messages.push({
from: {
avatar: faker.image.avatar(),
name: faker.name.findName(),
},
received: moment(faker.date.recent()),
subject: faker.company.bs(),
message: faker.lorem.paragraphs(Math.floor(Math.random() * 10 + 2)),
})
}
messages = messages.sort((a, b) => a.received.diff(b.received)).reverse()
for (let message of messages) {
if (message.received.isAfter(moment().subtract(5, 'hours'))) {
message.received = message.received.format('HH:mm')
} else {
message.received = message.received.format('D MMM')
}
}
const app = new Vue({
el: '#app',
data: {
theme: '#a7dde9',
openingComposer: false,
closingComposer: false,
composerOpen: false,
messageOpen: false,
activeMessage: undefined,
activeMessageIndex: undefined,
fakeOnePos: undefined,
fakeTwoPos: undefined,
messageTop: undefined,
mailOpened: false,
openingSettings: false,
closingSettings: false,
settingsOpen: false,
loaded: false,
messages: [],
},
mounted: function() {
this.loaded = true
},
methods: {
openComposer: function() {
this.openingComposer = !this.openingComposer
this.$nextTick(() => {
const composer = document.querySelector(`.${CLASSES.COMPOSER}`)
const composerTl = new TimelineMax({
onComplete: () => {
this.openingComposer = false
this.composerOpen = true
this.mailOpened = true
},
})
composerTl.add(
TweenMax.to(composer, ANIMATION_DURATION, {
right: 0,
bottom: 0,
height: '100%',
width: '100%',
borderRadius: 0,
})
)
})
},
closeComposer: function() {
this.closingComposer = !this.closingComposer
const composer = document.querySelector(`.${CLASSES.COMPOSER}`)
const composerTl = new TimelineMax({
onComplete: () => {
this.composerOpen = false
this.closingComposer = false
},
})
composerTl.add(
TweenMax.to(composer, ANIMATION_DURATION, {
bottom: 10,
right: 10,
height: 44,
width: 44,
borderRadius: '100%',
})
)
},
openInbox: function() {
this.messages = messages
},
closeInbox: function() {
this.messages = []
this.mailOpened = false
},
closeMessage: function() {
TweenMax.to(this.$refs.mailContainer, ANIMATION_DURATION, {
onComplete: () => {
this.activeMessage = null
this.activeMessageIndex = null
this.messageOpen = false
},
x: '-100%',
})
},
openMessage: function(message, idx) {
const el = document.getElementById(`message--${idx}`)
const list = document.querySelector('.mail-messages--main')
const header = document.querySelector(`.${CLASSES.HEADER}`)
this.mailOpened = true
this.activeMessage = message
this.activeMessageIndex = idx
this.fakeTwoPos =
el.offsetTop + el.offsetHeight + header.offsetHeight - list.scrollTop
this.messageTop = el.offsetTop + header.offsetHeight - list.scrollTop
this.fakeOnePos = list.offsetHeight - el.offsetTop + list.scrollTop
this.$nextTick(() => {
const el = document.querySelector('.mail-message--opening')
const {
fakeTop,
fakeBottom,
fakeHeader,
mailHeader,
mailAvatar,
mailSubject,
mailSender,
mailTimestamp,
mailContainer,
mailContent,
} = this.$refs
const fakeHeaderPos = fakeHeader.getBoundingClientRect()
const mailHeaderPos = mailHeader.getBoundingClientRect()
const mailContainerPos = mailContainer.getBoundingClientRect()
const openTl = new TimelineMax({
onComplete: () => {
this.messageOpen = true
},
})
openTl
// Move fakes out of the way
.to(fakeHeader, ANIMATION_DURATION, { y: `-${this.messageTop}px` }, 0)
.to(fakeTop, ANIMATION_DURATION, { bottom: '100%' }, 0)
.to(fakeBottom, ANIMATION_DURATION, { top: '100%' }, 0)
// Move header to top and change sizing
.to(
mailHeader,
ANIMATION_DURATION,
{ y: `-${this.messageTop - fakeHeaderPos.height}`, paddingTop: 10 },
0
)
.from(
mailAvatar,
ANIMATION_DURATION,
{ height: '34px', width: '34px' },
0
)
.from(
mailTimestamp,
ANIMATION_DURATION,
{ fontSize: '0.75rem', y: 0 },
0
)
.from(
mailSubject,
ANIMATION_DURATION,
{
overflow: 'hidden',
fontSize: '0.75rem',
fontWeight: '400',
color: '#7e7e7e',
},
0
)
.from(mailSender, ANIMATION_DURATION, { fontSize: '1rem' }, 0)
// Animate in the article
.to(
mailContent,
ANIMATION_DURATION,
{
height: `${mailContainerPos.height -
(mailHeaderPos.height + fakeHeaderPos.height)}px`,
opacity: 1,
y: `-${mailContainerPos.height -
(mailHeaderPos.height + fakeHeaderPos.height)}px`,
},
0
)
})
},
openSettings: function() {
this.openingSettings = !this.openingSettings
this.$nextTick(() => {
const el = document.querySelector(`.${CLASSES.SETTINGS}`)
TweenMax.from(el, ANIMATION_DURATION, {
onComplete: () => {
this.openingSettings = false
this.settingsOpen = true
},
x: '100%',
})
})
},
closeSettings: function() {
const el = document.querySelector(`.${CLASSES.SETTINGS}`)
TweenMax.to(el, ANIMATION_DURATION, {
onComplete: () => {
this.openeingSettings = false
this.settingsOpen = false
},
x: '100%',
})
},
updateTheme: function(e) {
this.theme = e.target.value
document.documentElement.style.setProperty('--accent', e.target.value)
},
},
})
View Compiled
This Pen doesn't use any external CSS resources.