<a href="https://youtu.be/D0l9EFWWk9M" target="_blank" data-keyframers-credit style="color: #000"></a>
<script src="https://codepen.io/shshaw/pen/QmZYMG.js"></script>
<div id="app" data-state="send">
<div class="ui-layer" data-show="send">
<button class="ui-button" style="--color: #24A5EF">
<div class="ui-bg" data-flip-key="bg"></div>
<span>Send</span>
</button>
</div>
<div class="ui-layer" data-show="sending">
<div class="ui-progress" data-flip-key="bg">
</div>
</div>
<div class="ui-layer" data-show="sent">
<button class="ui-button" style="--color: #20D280">
<span>Sent</span>
</button>
</div>
<div class="ui-layer -error" data-show="error">
<div class="ui-message">
<div class="ui-title">Error</div>
<div class="ui-subtext">
WIFI no connected dumb dumb
</div>
</div>
<div class="ui-goo">
<div class="ui-bg" data-flip-key="bg"></div>
<button class="ui-button -icon -retry" data-click="RETRY">
<span>⟲</span>
</button>
<button class="ui-button -icon -cancel" data-click="CANCEL">
<span>✕</span>
</button>
</div>
</div>
</div>
<svg class="goo-filter" viewBox="0 0 1 1">
<filter id="goo">
<feGaussianBlur in="SourceGraphic" stdDeviation="10" result="blur" />
<feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 50 -20" result="goo" />
<feBlend in="SourceGraphic" in2="goo" />
</filter>
</svg>
$color-blue: #24A5EF;
#app {
font-size: 2rem;
font-family: "Helvetica Neue", sans-serif;
}
/* ---------------------------------- */
#app {
display: flex;
}
.ui-layer {
width: 100%;
margin-right: -100%;
display: flex;
justify-content: center;
align-items: center;
// position: absolute;
// top: 0;
// left: 0;
// width: 100%;
// height: 100%;
display: none;
&[data-active="true"] {
display: flex;
}
}
/* ---------------------------------- */
.ui-button {
appearance: none;
background: transparent;
padding: 0 3rem;
border-radius: 3rem;
border: 2px solid #DDDDDD;
letter-spacing: 1px;
color: var(--color);
display: block;
margin: auto;
cursor: pointer;
height: 5rem;
&.-icon {
width: 5rem;
padding: 0;
color: transparent;
text-shadow: 0 0 0 white;
}
}
/* ---------------------------------- */
.ui-progress {
--progress: 0%;
height: .5rem;
width: 15rem;
background-color: #DDDDDD;
border-radius: .5rem;
overflow: hidden;
[data-active] & {
animation: fake-load 3s linear infinite alternate;
@keyframes fake-load {
25% { --progress: 30%; }
40% { --progress: 50%; }
60% { --progress: 80%; }
80% { --progress: 100%; }
100% { --progress: 65%; }
}
}
&:after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: $color-blue;
border-radius: inherit;
transform: translateX(calc(-100% + var(--progress)));
transition-duration: .6s;
z-index: 5;
}
}
/* ---------------------------------- */
.ui-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--bg);
}
/* ---------------------------------- */
.goo-filter {
position: absolute;
visibility: hidden;
}
.-error {
text-align: center;
padding: 1rem 7rem;
align-items: center;
.ui-message { z-index: 3; }
.ui-goo {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: space-between;
filter: url("#goo");
}
.ui-bg {
left: 0;
right: 0;
width: 70%;
margin: auto;
height: 100%;
padding: 2.5rem;
z-index: 2;
border-radius: 6rem;
pointer-events: none;
}
.ui-button {
margin: 0;
border: none;
overflow: hidden;
span {
display: block;
}
&.-retry {
transform: translateX(100%);
opacity: 0;
span { transform: translateX(-100%); }
}
&.-cancel {
transform: translateX(-100%);
opacity: 0;
span { transform: translateX(100%); }
}
}
.ui-bg,
.ui-button,
.ui-button span {
background-color: #E95D5D;
animation-duration: 1s;
animation-delay: 1s;
animation-timing-function: cubic-bezier(.5, 0, .5, 1);
animation-fill-mode: both;
}
&[data-active] {
.ui-bg {
animation-name: goo-scale-down;
@keyframes goo-scale-down {
96% { transform: scale(1,0); opacity: 1; }
98% { opacity: 0; }
100% { transform: scale(1.4,1); opacity: 0; }
}
}
.ui-button,
.ui-button span {
animation-name: error-button-in;
@keyframes error-button-in {
20% { opacity: 1; }
100% { transform: translateX(0%); opacity: 1 }
}
}
}
}
.ui-title {
letter-spacing: 1px;
color: var(--color);
}
.ui-subtext {
font-size: .5rem;
letter-spacing: .5px;
}
/* ---------------------------------- */
html, body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
}
body {
display: flex;
justify-content: center;
align-items: center;
}
*, *:before, *:after {
position: relative;
box-sizing: border-box;
transition: all .4s cubic-bezier(.5, 0, .5, 1);
}
View Compiled
console.clear();
// FLIPPING!!!!
const flipping = new Flipping({
duration: 400
});
const elApp = document.querySelector("#app");
const elButtons = document.querySelectorAll(".ui-button");
function activate(state) {
flipping.read();
elApp.dataset.state = state;
document.querySelectorAll("[data-active]").forEach(el => {
el.dataset.active = false;
});
document.querySelectorAll(`[data-show~="${state}"]`).forEach(el => {
el.dataset.active = true;
});
flipping.flip();
}
const states = {
send: {
on: {
CLICK: "sending"
}
},
sending: {
onEntry: () => {
setTimeout(() => {
if (Math.random() < 0.3) {
send("SENT");
} else {
send("ERROR");
}
}, 2000);
},
on: {
SENT: "sent",
ERROR: "error"
}
},
error: {
on: {
RETRY: "sending",
CANCEL: "send",
}
},
sent: {
on: {
CLICK: "send"
}
}
};
const machine = {
initial: elApp.dataset.state,
states
};
let currentState = machine.initial;
function send(event) {
const nextState =
// get the next state based on the event
// get the current state
machine.states[currentState].on[event] || currentState;
if (currentState !== nextState) {
const entryAction = machine.states[nextState].onEntry;
if (entryAction) {
entryAction();
}
}
activate(nextState);
currentState = nextState;
}
activate(currentState);
[...elButtons].forEach(el => {
el.addEventListener("click", () => {
send(el.dataset.click || "CLICK");
});
});
View Compiled
This Pen doesn't use any external CSS resources.