<!-- 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();
  }
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.