Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement context menus similarly to youtube cards?

I'm on my way to learn a bit of CSS and today I've found myself wanting to implement a similar context menu (the one it's shown when you the vertical elipsis on youtube cards is clicked) but my attempt didn't make it very far :D . Here's what I've got:

<!DOCTYPE html>
<html>

<head>
<style type="text/css">
.container {
    display: inline-block;
    position: relative;
}

.dropdown {
    position: absolute;
    right: 1rem;
    top: 1rem;
}

.dropdown-opener {
    cursor: pointer;
    user-select: none;
    position: absolute;
    right: 0;
}

.dropdown .dropdown-toggle,
.dropdown .dropdown-menu {
    display: none;
    style-type: none;
}

.dropdown .dropdown-toggle:checked+ul {
    display: block;
}

.style-scope .menu-renderer {
    --layout-inline_-_display: inline-flex;
    --icon-button-icon-height: 24px;
    --icon-button-icon-width: 24px;
    --spec-icon-active-other: #606060;
    --spec-icon-inactive: #909090;
    --spec-text-disabled: #909090;
    --spec-text-secondary: #606060;
    align-items: var(--layout-center-center_-_align-items);
    color: var(--menu-renderer-button-color, var(--spec-icon-inactive));
    cursor: pointer;
    display: var(--layout-inline_-_display);
    fill: var(--iron-icon-fill-color, currentcolor);
    width: var(--icon-button-icon-width, 100%);
    background: transparent;
}
</style>
</head>

<body>
<div class="container">
    <img alt="sample" src="https://via.placeholder.com/200x200">
    <nav class="dropdown layer--topright">
        <label class="dropdown-opener" for="menu-opener1">...</label>
        <input class="dropdown-toggle" id="menu-opener1" type="checkbox">
        <ul class="dropdown-menu">
            <li>Foo1</li>
            <li>Bar1</li>
            <li>Baz1</li>
        </ul>
    </nav>
</div>
<div class="container">
    <img alt="sample" src="https://via.placeholder.com/200x200">
    <nav class="dropdown layer--topright">
        <label class="dropdown-opener" for="menu-opener1">...</label>
        <input class="dropdown-toggle" id="menu-opener1" type="checkbox">
        <ul class="dropdown-menu">
            <li>Foo2</li>
            <li>Bar2</li>
            <li>Baz2</li>
        </ul>
    </nav>
</div>
<div class="container">
    <img alt="sample" src="https://via.placeholder.com/200x200">
    <nav class="dropdown layer--topright">
        <icon-button id="button" class="dropdown-opener dropdown-trigger style-scope menu-renderer">
            <button id="button" class="style-scope icon-button" aria-label="Action menu">
                <icon class="style-scope menu-renderer">
                    <svg viewBox="0 0 24 24" preserveAspectRatio="xMidYMid meet" focusable="false" class="style-scope icon" style="pointer-events: none; display: block; width: 100%; height: 100%;">
                        <g class="style-scope icon">
                            <path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" class="style-scope icon"></path>
                        </g>
                    </svg>
                </icon>
            </button>
        </icon-button>
        <input class="dropdown-toggle" id="menu-opener2" type="checkbox">
        <ul class="dropdown-menu">
            <li>Foo3</li>
            <li>Bar3</li>
            <li>Baz3</li>
        </ul>
    </nav>
</div>
</body>

</html>

As you can see the code is dirty and broken... but there are major problems with my attempts I didn't know how to fix:

  1. Misplaced context menus when clicked the elipsis button
  2. Items showing the bulletpoint mark
  3. Items style is ugly in comparison to youtube's one which is really clean

So if you could explain how to address those ones in order to make something usable out of my attempt that'd be awesome.

Or if my approach is all wrong, unusable and ready to throw to the bin... could you please explain what'd the best to achieve my goal?

Thanks in advance.

like image 921
BPL Avatar asked Jun 13 '20 22:06

BPL


1 Answers

Issue #1

Misplaced context menus when clicking the overflow menu button. The context menus are not actually misplaced. The for attribute in your <label> element refers to the wrong context menus. For example, the for value of your <label> element in the first .container has a value of menu-opener1, but the for value on the <label> element in the second .container has the exact same value. Clicking either label causes the dropdown menu in the first container to be opened because both labels cause the hidden checkbox on the first container to be checked.

What can we do? Simply change the id value so that each dropdown menu has a unique id value. Then, use that id value for the for value inside your <label> element.


Issue #2

To hide the bulletins of li elements inside a ul, you have to use list-style-type: none on your CSS and not style-type: none.


Issue #3

This is a very subjective matter. A design can look clean to one but look unclean to others. Nevertheless, I tried to achieve what I wanted to see. Here are some things that you can change to improve the design aspect.

  • Change your font-family so that it suits your theme. I chose a sans-serif font family here.
  • Add a background to your context menu. Here, I chose the color white.
  • Add space between each li element. Here, I used line-height. You can also use padding or margin on each li element.
  • Add a box-shadow to show elevation. Google's Material Design recommends using this technique to show that an element's z-position is higher.
  • Allow transition from when the context menu changes from visible to invisible and vice-versa. This means avoiding using un-transition-able CSS properties (e.g. visibility and display). Here, I chose to use transform: scale and opacity transition.

Other concerns

Semantically, your HTML tags are incorrect.

  • The <nav> (navigation) element is used to navigate between pages. Here, you should probably use <menu> element instead. However, as it is still experimental, I chose to use <section>.
  • You are using some nonexistant HTML tags (e.g. <icon-button> and <icon>). Try consulting here for valid HTML tags and here for valid SVG tags.
  • The last .container item has a button that will check the hidden checkbox. However, the <button> element does not work with <label> element. So, try to make the <label> element visually look like a button instead. You can use :active and :hover CSS pseudoselectors to change the button style when it is pressed and hovered respectively. Furthermore, this reduces nesting.
  • Try to always avoid using inline styles when styling using CSS is possible (e.g. your inline SVG styles)
  • Personal preference. Most frameworks use .container to contain the whole page, so I have opted to use the class name .box instead of .container.

Here's the runnable snippet.

* {
  font-family: Helvetica;
  box-sizing: border-box;
}

.box {
  display: inline-block;
  position: relative;
}

.dropdown {
  position: absolute;
  right: 0;
  top: 8px;
}

.dropdown-opener {
  cursor: pointer;
  user-select: none;
  position: absolute;
  width: 24px;
  height: 24px;
  background: url('https://i.imgur.com/Qt3Qwgp.png');
  background-repeat: no-repeat;
  background-position: right;
  right: 0;
}

.dropdown .dropdown-toggle {
  display: none;
}

.dropdown .dropdown-menu {
  list-style-type: none;
  transform: scale(0);
  opacity: 0;
  transition:
    transform 0.25s ease,
    opacity 0.25s ease;
  
  position: absolute;
  top: 1.5em;
  right: 10px;
  line-height: 1.75em;
  background: white;
  box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.5);
  border-radius: 5px;
  padding: 20px;
  margin: 0;
  transform-origin: top right;
}

.dropdown .dropdown-toggle:checked + ul {
  transform: scale(1);
  opacity: 1;
}

.dropdown-opener-button {
  position: absolute;
  width: 24px;
  height: 24px;
  right: 8px;
  top: 0;
}

.icon-button {
  padding: 0;
  border: 0;
  border-radius: 5px;
  cursor: pointer;
  transition: 
    box-shadow .25s ease,
    background .25s ease,
    transform .25s ease;
  background: #ffffffdd;
}

.icon-button:hover {
  box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.35);
}

.icon-button:active {
  background: #ffffff77;
  transform: scale(0.9);
}

.icon-button ~ .dropdown-menu {
  top: 1.75em;
}
<div class="box">
  <img alt="sample" src="https://via.placeholder.com/200x200">
  <section class="dropdown layer--topright">
    <label class="dropdown-opener" for="menu-opener1"></label>
    <input class="dropdown-toggle" id="menu-opener1" type="checkbox">
    <ul class="dropdown-menu">
      <li>Foo1</li>
      <li>Bar1</li>
      <li>Baz1</li>
    </ul>
  </section>
</div>
<div class="box">
  <img alt="sample" src="https://via.placeholder.com/200x200">
  <section class="dropdown layer--topright">
    <label class="dropdown-opener" for="menu-opener2"></label>
    <input class="dropdown-toggle" id="menu-opener2" type="checkbox">
    <ul class="dropdown-menu">
      <li>Foo2</li>
      <li>Bar2</li>
      <li>Baz2</li>
    </ul>
  </section>
</div>
<div class="box">
  <img alt="sample" src="https://via.placeholder.com/200x200">
  <section class="dropdown layer--topright">
    <label class="dropdown-opener-button icon-button" for="menu-opener3">
      <svg viewBox="0 0 24 24" preserveAspectRatio="xMidYMid meet" focusable="false" class="style-scope icon">
        <g class="style-scope icon">
          <path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" class="style-scope icon"></path>
        </g>
      </svg>
    </label>
    <input class="dropdown-toggle" id="menu-opener3" type="checkbox">
    <ul class="dropdown-menu">
      <li>Foo3</li>
      <li>Bar3</li>
      <li>Baz3</li>
    </ul>
  </section>
</div>

Update

Not having a crystal-clear understanding of what the OP meant by how hard would it be to make the menu items change the state when hovering with the mouse, I decided to create an effect on hovered and a different effect (ripple) on click for the <li> elements. I recommend reading this writing on creating a ripple effect.

Also, as per request, the functionality to hide the menus when a click outside of the menu's box has been added. Here's the runnable snippet.

// Closing menu on outside click
const outsideClickListener = event => {
  let checkedToggle = document.querySelector('.dropdown-toggle:checked')
  let openedMenu = document.querySelector('.dropdown-toggle:checked + .dropdown-menu')
  
  // If click is performed on checkbox (through label), do nothing
  if (event.target.classList.contains('dropdown-toggle')) {
    return
  }
  
  // If click is performed on label, uncheck all other dropdown-toggle
  if (event.target.classList.contains('dropdown-opener') ||
      event.target.classList.contains('dropdown-opener-button')) {
    let forId = event.target.getAttribute('for')
    document.querySelectorAll('.dropdown-toggle').forEach(toggle => {
      if (forId !== toggle.getAttribute('id'))
        toggle.checked = false
    })
    return
  }
  
  // If click is performed outside opened menu
  if (openedMenu && !openedMenu.contains(event.target)) {
    checkedToggle.checked = false
  }
}
document.addEventListener('click', outsideClickListener)

// Ripple effect on li elements
const createRipple = event => {
  let li = event.target
  let liBox = li.getBoundingClientRect()
  let x = event.pageX - liBox.left
  let y = event.pageY - liBox.top
  let animDuration = 350
  let animationStart, animationFrame
  
  let animationStep = timestamp => {
    if (!animationStart) animationStart = timestamp
    let frame = timestamp - animationStart
    if (frame < animDuration) {
      let easing = (frame / animDuration) * (2 - (frame / animDuration))
      let circle = `circle at ${x}px ${y}px`
      let color = `rgba(0, 0, 0, ${0.2 * (1 - easing)})`
      let stop = `${100 * easing}%`
      li.style.backgroundImage = `radial-gradient(${circle}, ${color} ${stop}, transparent ${stop})`
      animationFrame = window.requestAnimationFrame(animationStep)
    }
    else {
      li.style.backgroundImage = ''
      window.cancelAnimationFrame(animationStep)
    }
  }
  
  animationFrame = window.requestAnimationFrame(animationStep)
}
const listItems = document.querySelectorAll('li')
listItems.forEach(li => {
  li.addEventListener('click', createRipple)
})
* {
  font-family: Helvetica;
  box-sizing: border-box;
}

.box {
  display: inline-block;
  position: relative;
}

.dropdown {
  position: absolute;
  right: 0;
  top: 8px;
}

.dropdown-opener {
  cursor: pointer;
  user-select: none;
  position: absolute;
  width: 24px;
  height: 24px;
  background: url('https://i.imgur.com/Qt3Qwgp.png');
  background-repeat: no-repeat;
  background-position: right;
  right: 0;
}

.dropdown .dropdown-toggle {
  display: none;
}

.dropdown .dropdown-menu {
  list-style-type: none;
  transform: scale(0);
  opacity: 0;
  transition:
    transform 0.25s ease,
    opacity 0.25s ease;
  
  position: absolute;
  top: 1.5em;
  right: 10px;
  background: white;
  box-shadow: 0px 0px 5px 0px rgba(0,0,0,0.5);
  border-radius: 5px;
  margin: 0;
  transform-origin: top right;
  padding: 7.5px 0 7.5px 0;
}

.dropdown .dropdown-toggle:checked + ul {
  transform: scale(1);
  opacity: 1;
}

.dropdown-menu li {
  padding: 7.5px;
  padding-left: 25px;
  cursor: pointer;
  transition: background .15s ease;
}

.dropdown-menu li:hover {
  background: #00000012;
}

.dropdown-opener-button {
  position: absolute;
  width: 24px;
  height: 24px;
  right: 8px;
  top: 0;
}

.dropdown-opener-button svg {
  pointer-events: none;
}

.icon-button {
  padding: 0;
  border: 0;
  border-radius: 5px;
  cursor: pointer;
  transition: 
    box-shadow .25s ease,
    background .25s ease,
    transform .25s ease;
  background: #ffffffdd;
}

.icon-button:hover {
  box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.35);
}

.icon-button:active {
  background: #ffffffaa;
  box-shadow: 0px 0px 4px 0px rgba(0,0,0,0.35);
  transform: scale(0.9);
}

.icon-button ~ .dropdown-menu {
  top: 1.75em;
}
<div class="box">
  <img alt="sample" src="https://via.placeholder.com/200x200">
  <section class="dropdown layer--topright">
    <label class="dropdown-opener" for="menu-opener1"></label>
    <input class="dropdown-toggle" id="menu-opener1" type="checkbox">
    <ul class="dropdown-menu">
      <li>Foo1</li>
      <li>Bar1</li>
      <li>Baz1</li>
    </ul>
  </section>
</div>
<div class="box">
  <img alt="sample" src="https://via.placeholder.com/200x200">
  <section class="dropdown layer--topright">
    <label class="dropdown-opener" for="menu-opener2"></label>
    <input class="dropdown-toggle" id="menu-opener2" type="checkbox">
    <ul class="dropdown-menu">
      <li>Foo2</li>
      <li>Bar2</li>
      <li>Baz2</li>
    </ul>
  </section>
</div>
<div class="box">
  <img alt="sample" src="https://via.placeholder.com/200x200">
  <section class="dropdown layer--topright">
    <label class="dropdown-opener-button icon-button" for="menu-opener3">
      <svg viewBox="0 0 24 24" preserveAspectRatio="xMidYMid meet" focusable="false" class="style-scope icon">
        <g class="style-scope icon">
          <path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" class="style-scope icon"></path>
        </g>
      </svg>
    </label>
    <input class="dropdown-toggle" id="menu-opener3" type="checkbox">
    <ul class="dropdown-menu">
      <li>Foo3</li>
      <li>Bar3</li>
      <li>Baz3</li>
    </ul>
  </section>
</div>
like image 155
Richard Avatar answered Oct 13 '22 12:10

Richard