<!-- markup
button, used as a toggle
nav, navigation opened/closed following a long press on the button itself
-->
<button>
<!-- svg describing a plus sign -->
<svg viewBox="-50 -50 100 100" width="100" height="100">
<circle r="50" fill="hsl(0, 0%, 100%)" />
<g fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round">
<path d="M -15 0 h 30 m -15 -15 v 30" />
</g>
</svg>
</button>
<nav>
<!-- rounded rectangle describing the different sections of the page through individual icons -->
<svg viewBox="0 0 260 100" width="260" height="100">
<g fill="hsl(0, 0%, 100%)">
<rect width="260" height="85" rx="42.5" />
<g stroke="hsl(0, 0%, 100%)" stroke-width="10" stroke-linejoin="round">
<g transform="translate(130 80)">
<path d="M -15 0 l 15 15 15 -15 z" />
</g>
</g>
<g>
<g transform="translate(0 42.5)">
<!-- wrap each icon in an anchor link element to make it select-able -->
<a href="#">
<!-- ! setting currentColor before the anchor link elements won't work as the anchor link would introduce its default color -->
<g transform="translate(60 0)">
<g stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<g fill="none">
<circle r="4.5" cx="-7" cy="-7" />
<circle r="4.5" cx="-7" cy="7" />
</g>
<g fill="currentColor">
<path id="scissor--half" d="M -3 -2 l 10 10 a 4 4 0 0 0 4 0 l -13 -13" />
<use href="#scissor--half" transform="scale(1 -1)" />
</g>
</g>
<rect x="-15" y="-15" width="30" height="30" opacity="0" fill="hsl(0, 0%, 0%)" />
</g>
</a>
<a href="#">
<g transform="translate(130 0)">
<g stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<g fill="none">
<path d="M -10 8 v -18 a 2 2 0 0 1 2 -2 h 13" />
</g>
<g fill="currentColor">
<path d="M -2 -6 h 9 l 4.5 4.5 v 12 a 2 2 0 0 1 -2 2 h -11.5 a 2 2 0 0 1 -2 -2 v -14.5 a 2 2 0 0 1 2 -2" />
</g>
</g>
<rect x="-15" y="-15" width="30" height="30" opacity="0" fill="hsl(0, 0%, 0%)" />
</g>
</a>
<a href="#">
<g transform="translate(210 0)">
<g stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<g fill="none">
<path d="M -5.5 8 h -3 a 2 2 0 0 1 -2 -2 v -16 a 2 2 0 0 1 2 -2 h 3 a 3 3 0 0 1 3 -3 h 2 a 3 3 0 0 1 3 3 h 3 a 2 2 0 0 1 2 2 v 2.5" />
</g>
<g fill="currentColor">
<path d="M -2 -6 h 9 l 4.5 4.5 v 12 a 2 2 0 0 1 -2 2 h -11.5 a 2 2 0 0 1 -2 -2 v -14.5 a 2 2 0 0 1 2 -2" />
</g>
</g>
<rect x="-15" y="-15" width="30" height="30" opacity="0" fill="hsl(0, 0%, 0%)" />
</g>
</a>
</g>
</g>
</g>
</svg>
</nav>
/* custom-properties describing the colors used throughout the project
&& the time after which the button is considered active */
:root {
--gradient-start: hsl(300, 100%, 70%);
--gradient-end: hsl(340, 85%, 60%);
--accent: hsl(320, 92.5%, 65%);
--delay: 2s;
--easeInCirc: cubic-bezier(0.6, 0.04, 0.98, 0.335);
--easeOutCirc: cubic-bezier(0.075, 0.82, 0.165, 1);
--easeInOutCubic: cubic-bezier(0.645, 0.045, 0.355, 1);
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
/* display the contents in a reverse column, so to have the navigation atop the button
! this messes up the side describing the margin, see the owl selector
*/
body {
min-height: 100vh;
display: flex;
flex-direction: column-reverse;
justify-content: center;
align-items: center;
color: hsl(0, 0%, 20%);
background: linear-gradient(to bottom right, var(--gradient-start), var(--gradient-end));
}
body > * + * {
margin-bottom: 0.75rem;
}
button {
width: 80px;
height: 80px;
display: block;
color: var(--accent);
border: none;
background: none;
border-radius: 50%;
filter: drop-shadow(0 0 8px hsla(0, 0%, 0%, 0.2));
outline: none;
position: relative;
z-index: 5;
}
button svg {
/* transition the rotation of the + sign with a considerable duration
use a timing function which snaps toward the end
*/
transition: all 0.2s var(--easeInOutCubic);
width: 100%;
height: 100%;
display: block;
}
/* in favor of the default outline use a pseudo element to add a semitransparent border around the button */
button:after {
z-index: -5;
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: hsla(0, 0%, 100%, 0.3);
border-radius: inherit;
transform: scale(1);
transition: all 0.2s var(--easeInOutCubic);
}
button:focus:after,
button:hover:after {
transform: scale(1.2);
}
/* with a second pseudo element, show momentarily a set of particles
! only following the active state
*/
button:before {
z-index: -5;
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-50 -50 100 100' width='100' height='100'%3E%3Cg fill='hsl(0, 0%25, 100%25)' stroke-width='1' stroke-linecap='round' stroke-linejoin='round' stroke='hsl(0, 0%25, 100%25)'%3E%3Cg%3E%3Ccircle cx='-30' r='4' /%3E%3Ccircle cx='20' cy='35' r='3' /%3E%3Ccircle cx='40' cy='15' r='1' /%3E%3Ccircle cx='-10' cy='-30' r='1' /%3E%3Crect transform='translate(38 -10) rotate(-15)' x='-3.5' y='-3.5' width='7' height='7' /%3E%3Crect transform='translate(-35 -30) rotate(10)' x='-3' y='-3' width='6' height='6' /%3E%3C/g%3E%3Cg %3E%3Cpath transform='translate(-10 35) rotate(12)' d='M -3 3 l 6 0 -3 -6 z' /%3E%3Cpath transform='translate(-35 25) rotate(50)' d='M -4 0 h 8 m -4 -4 v 8' /%3E%3Cpath transform='translate(25 -30) rotate(50)' d='M -4 0 h 8 m -4 -4 v 8' /%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
background-size: 100%;
transform: scale(1);
opacity: 1;
/* to have the transition only one way, be sure to describe it as the active state is enabled */
transition: none;
}
/* as the button is pressed, rotate the button
! apply the rotation also through a class, which is added after the transition has had a change to complete
*/
button:active svg,
button.active svg {
transform: rotate(45deg);
transition: all var(--delay) var(--easeInCirc);
}
/* always as the button is being pressed, increase the area of the pseudo element
! use a similar timing function
*/
button:active:after,
button.active:after {
transform: scale(2);
opacity: 0;
transition: all var(--delay) var(--easeInCirc);
}
/* finally and momentarily show the particles described by the before pseudo element
! use a different timing function for the opacity than for the transform
opacity should change slowly at first, while transform should initially move fast
*/
button:active:before,
button.active:before {
transition-property: transform, opacity;
transition-duration: 0.55s, 0.5s;
transition-delay: var(--delay);
transition-timing-function: var(--easeOutCirc), var(--easeInCirc);
transform: scale(2);
opacity: 0;
}
/* target the navigation following the button (shown earlier, but appearing later in the DOM)
remove it from reach with visibility hidden
*/
button + nav {
z-index: 5;
filter: drop-shadow(0 2px 8px hsla(0, 0%, 0%, 0.2));
/* transition the navigation when the class of .active is applied on the button
scale from the bottom
*/
transform-origin: 50% 100%;
transition: all 0.3s var(--easeOutCirc);
/* to apply the delay only as the class active is removed be sure to remove it
when the class is actually added
*/
transition-delay: 0.25s;
transform: scale(0);
opacity: 0;
visibility: hidden;
}
button.active + nav {
opacity: 1;
visibility: visible;
transform: scale(1);
transition-delay: 0s;
}
/* style the icons nested in the anchor link elements with a subdued color
reset opacity and color on hover/focus
*/
nav a {
color: hsl(0, 0%, 50%);
opacity: 0.4;
transition: all 0.2s var(--easeInOutCubic);
outline: none;
}
nav a:focus,
nav a:hover {
color: var(--accent);
opacity: 1;
}
const button = document.querySelector('button');
const delay = 2000;
let timeout;
// function removing or adding (after a delay) a class of active
function toggleActive() {
if (button.classList.contains('active')) {
button.classList.remove('active');
} else {
timeout = setTimeout(() => {
button.classList.add('active');
timeout = null;
}, delay);
}
}
// function removing the existing, if any, timeout
// this is made necessary in a situation where the press is removed before the timeout has run out
function removeActive() {
if (timeout) {
clearTimeout(timeout);
}
}
// handle presses through the mouse cursor
// ! to replicate the spacebar, a few more lines of JS are necessary
button.addEventListener('mousedown', () => toggleActive());
button.addEventListener('mouseup', () => removeActive());
/* KEY events */
// trigger the active functions only following a press on the spacebar
const KEY_CODE = 32;
// use a boolean to avoid running the function repeatedly (keydown runs multiple times while the spacebar is being pressed)
let isKeydown = false;
// hitting spacebar for the first time, use the function to add/remove the class
button.addEventListener('keydown', ({keyCode}) => {
if(keyCode === KEY_CODE && !isKeydown) {
isKeydown = true;
toggleActive();
}
});
// when the spacebar is released, remove the existing, if any timeout
button.addEventListener('keyup', ({keyCode}) => {
if(keyCode === KEY_CODE) {
isKeydown = false;
removeActive();
}
});
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.