Skip to content

Instantly share code, notes, and snippets.

@CodeMyUI

CodeMyUI/index.html

Created Oct 2, 2018
Embed
What would you like to do?
Notifications Concept
<!-- only for preview -->
<nav>
<div class="nav__notification">
<span class="nav__notification__icon"></span>
<span class="nav__notification__num">3</span>
</div>
</nav>

Notifications Concept

A UX experiment for a notification bar/panel. Made with React and React-Motion.

A Pen by Guy Waldman on CodePen.

License.

// inspiration: https://dribbble.com/shots/3003823-Notification-Dropdown
const {
Component
} = React;
const {
Motion,
StaggeredMotion,
spring,
presets
} = ReactMotion;
class Media extends Component {
render() {
const cls = "nav__notifications__list__item" + (this.props.new ? " nav__notifications__list__item--new" : "");
return (
<li style={this.props.style} className={cls}>
<div className="nav__notifications__list__item__display">
<Motion defaultStyle={{x:0.6}} style={{x:spring(this.props.open ? 1 : 0.6, presets.wobbly)}}>
{interp => <img src={this.props.imageURL} className="nav__notifications__list__item__photo" style={{transform: `scale(${interp.x})`}}/> }
</Motion>
</div>
<div className="nav__notifications__list__item__desc">
<Motion defaultStyle={{x: 0}} style={{x: spring(this.props.open ? 0 : 1, presets.wobbly)}} >
{interp => <div style={{transform: `translateZ(0) translateY(${-15*interp.x}px)`, opacity: 1-interp.x}}>
<em>{this.props.name}</em> {this.props.action} <em> {this.props.content}</em>.
</div>}
</Motion>
</div>
</li>
);
}
}
class NotificationsBar extends Component {
constructor(props) {
super(props);
this.state = {
media: [ {imgURL: 'https://randomuser.me/api/portraits/men/74.jpg',
name: 'Gerlald Thompson',
action: 'approved your request to',
content: 'become friends',
new: true},
{imgURL: 'https://randomuser.me/api/portraits/women/32.jpg',
name: 'Dana Newman',
action: 'liked',
content: 'your photo',
new: true},
{imgURL: 'https://randomuser.me/api/portraits/men/93.jpg',
name: 'Dan Ingrid',
action: 'also commented on',
content: 'your status',
new: true},
{imgURL: 'https://randomuser.me/api/portraits/women/16.jpg',
name: 'Lena Direlson',
action: 'checked in at',
content: 'Greenstreet Pub',
new: false},
{imgURL: 'https://randomuser.me/api/portraits/men/78.jpg',
name: 'Dan Witherson',
action: 'also commented on',
content: 'your status',
new: false}]
};
}
render() {
const {media} = this.state;
const motionParams = media.map(_ => Object.assign({},{h:0}));
return (
<Motion defaultStyle={{opacity: 0}} style={{opacity: spring(this.props.open ? 1 : 0, presets.stiff)}}>
{interpOuter =>
<div style={interpOuter} className="nav__notification_bar">
<Motion defaultStyle={{x: 0}} style={{x: spring(this.props.open ? 0 : -5, presets.stiff)}}>
{interp => <h3 style={{transform: `translateY(${interp.x}px)`}}>Notifications</h3>}
</Motion>
<StaggeredMotion defaultStyles={motionParams} styles={prevInterpolatedStyles => prevInterpolatedStyles.map((_, i) => {
return i === 0
? {h: spring(this.props.open ? 100 : 0, presets.wobbly)}
: {h: spring(prevInterpolatedStyles[i - 1].h)}
})}>
{interps =>
<ul className="nav__notifications__list">
{interps.map((style, i) => <Media key={i} style={{height:style.h}} imageURL={media[i].imgURL} name={media[i].name} action={media[i].action} content={media[i].content} open={this.props.open} new={media[i].new} />)}
</ul>
}
</StaggeredMotion>
</div>
}
</Motion>
);
}
}
class Notifications extends Component {
constructor(props) {
super(props);
this.toggleNotificationBar = this.toggleNotificationBar.bind(this);
this.count = 0;
}
toggleNotificationBar() {
this.props.toggleNotificationsBar();
}
render() {
return (
<div className="nav__notification">
<span className="nav__notification__icon" onClick={this.toggleNotificationBar}/>
<Motion defaultStyle={{x: 0}} style={{x: spring(this.props.open ? 0 : 1, presets.stiff)}} >
{interp => <span className="nav__notification__num" style={{transform: `translateZ(0) scale(${interp.x}`, opacity: interp.x}}>3</span>}
</Motion>
<NotificationsBar open={this.props.open}/>
</div>
)
}
}
class NavBar extends Component {
constructor(props) {
super(props);
this.state = {
isNotificationsOpen: false
}
this.toggleNotificationsBar = this.toggleNotificationsBar.bind(this);
this.closeNotificationsBar = this.closeNotificationsBar.bind(this);
}
toggleNotificationsBar () {
this.setState({...this.state, isNotificationsOpen: !this.state.isNotificationsOpen});
}
closeNotificationsBar() {
if (!this.state.isNotificationsOpen) return;
this.setState ( {...this.state, isNotificationsOpen: false})
}
render() {
return (
<nav tabIndex="0" onBlur={ this.closeNotificationsBar }>
<Notifications toggleNotificationsBar={this.toggleNotificationsBar} open={this.state.isNotificationsOpen} />
</nav>
)
}
}
ReactDOM.render(<NavBar/>, document.body)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.1/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.3.1/react-dom.min.js"></script>
<script src="https://unpkg.com/react-motion/build/react-motion.js"></script>
@import 'https://fonts.googleapis.com/css?family=Roboto:300,500,700';
$clr-white-1: #ecf0f1;
$clr-white-2: darken($clr-white-1, 3.5%);
$clr-t300: #7f8c8d;
$clr-t500: #95a5a6;
$clr-p300: #3498db;
$clr-a300: #e74c3c;
$trans: cubic-bezier(0.3,0,0.7,1);
$media-mobile: "only screen and (max-width: 720px)";
body {
display: flex;
justify-content: center;
padding: 0;
margin: 0;
padding-top: 5rem;
box-sizing: border-box;
align-items: flex-start;
width: 100vw;
height: 100vh;
background: $clr-white-1;
font-family: 'Roboto', Arial, sans-serif;
font-weight: 500;
}
nav {
width: 70vw;
height: 3rem;
background: $clr-p300;
background: linear-gradient(to right, $clr-p300, lighten($clr-p300, 8%));
box-shadow: inset 0 0 1px 0 rgba(black, 0.1), 0 0 5px 0 rgba(white, 0.3);
border-radius: 0.5em;
box-sizing: border-box;
padding: 0.5rem 3rem;
position: relative;
display: flex;
justify-content: flex-end;
align-items: center;
outline: none;
}
.nav__notification {
position: relative;
width: 1.5rem;
height: 1.5rem;
}
.nav__notification__icon {
cursor: default;
position: absolute;
background: $clr-white-1;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
// will-change: transform;
// transition: transform $trans;
box-shadow: 1px 1px 3px 0 rgba(black, 0.1);
&:before {
content: "";
border: 3px solid rgba($clr-white-1,.3);
box-sizing: border-box;
position: absolute;
border-radius: 50%;
background: none;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
animation: bounceScale 3s $trans infinite 2s;
}
animation: bounce 3s $trans infinite 2s;
}
// .nav__notification__icon:hover + .nav__notification__num {
// transform: scale(1.1);
// }
@keyframes bounceScale {
0%, 20% {
opacity: 0;
border-width: 3px;
}
10% {
opacity: 1;
}
20%, 100% {
transform: scale(2);
border-width: 1px;
opacity: 0;
}
}
@keyframes bounce {
0%, 20% {
transform: scale(1);
}
5% {
transform: scale(1.15);
}
}
.nav__notification__num {
position: absolute;
user-select: none;
cursor: default;
font-size: 0.6rem;
background: $clr-a300;
width: 1.2rem;
height: 1.2rem;
color: $clr-white-1;
display: inline-flex;
justify-content: center;
align-items: center;
border-radius: 50%;
top: -0.4rem;
right: -0.4rem;
box-sizing: border-box;
// will-change: transform;
// transition: transform $trans;
}
// ------------------- nav bar -------------------
.nav__notification_bar {
&:before {
// triangle
content: "";
position: absolute;
top: 0;
right: 0;
width: 0;
height: 0;
transform: translate(-1rem, -100%);
border-left: 0.7rem solid transparent;
border-right: 0.7rem solid transparent;
border-bottom: 0.7rem solid $clr-white-1;
}
contain: layout;
position: absolute;
top: 2em;
right: 0;
width: 35vw;
background: $clr-white-1;
transform: translate(1rem, 0.5rem);
border-radius: 0.5rem;
@media #{$media-mobile} {
width: calc(70vw - 2em);
}
padding: 0.5rem 0.75rem;
box-sizing: border-box;
box-shadow: 0.5rem 0.5rem 2rem 0 rgba(black, 0.2);
h3 {
user-select: none;
cursor: default;
font-size: 0.7rem;
text-transform: uppercase;
color: $clr-t300;
letter-spacing: 0.1rem;
}
}
.nav__notifications__list {
list-style: none;
width: 100%;
box-sizing: border-box;
padding: 0;
margin: 0;
}
.nav__notifications__list__item {
contain: strict;
background: none;
height: 5rem;
width: 100%;
box-sizing: border-box;
border-radius: 0.5rem;
padding: 0;
margin: 0;
display: flex;
padding: 0 1.5rem;
justify-content: space-around;
align-items: center;
opacity: 0.5;
transition: opacity 250ms $trans;
&:hover {
opacity: 1;
}
&:not(:last-of-type) {
margin-bottom: 0.5rem;
}
&.nav__notifications__list__item--new {
background: $clr-white-2;
opacity: 1;
}
}
.nav__notifications__list__item__photo {
background-image: url(attr(data-url));
width: 100%;
height: 100%;
border-radius: 50%;
transform: translateZ(0);
position: relative;
}
.nav__notifications__list__item__display {
width: 3rem;
height: 3rem;
}
.nav__notifications__list__item__desc {
height: 100%;
flex: 1;
padding: 0 1rem;
padding-right: 0;
box-sizing: border-box;
display: inline-flex;
align-items: center;
font-size: 0.7rem;
color: rgba($clr-t500,.8);
em {
text-decoration: none;
font-style: normal;
font-weight: 600;
color: $clr-t300;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment