How to implement context menus similarly to youtube cards?
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 usedline-height
. You can also usepadding
ormargin
on eachli
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
anddisplay
). Here, I chose to usetransform: scale
andopacity
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>
$(document).ready(function() {
$('.dropdown-opener').on('click', function() {
$('.dropdown-menu').removeClass('show');
$(this).parents('.dropdown').children('.dropdown-menu').toggleClass('show')
});
$(document).on('click', function(e) {
if (!(e.target.matches('.dropdown-opener') || e.target.matches('.fa.fa-ellipsis-v'))) {
$('.dropdown-menu').removeClass('show');
//debugger;
}
});
});
.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;
list-style-type: none;
}
.dropdown .dropdown-toggle:checked+ul {
display: block;
}
ul.dropdown-menu {
background-color: #efefef;
list-style-type: none;
line-height: 1.5rem;
border-radius: 3px;
padding: 0;
min-width: 60px;
float: left;
font-size: 14px;
font-weight: bold;
-webkit-box-shadow: 2px 3px 3px -1px rgba(196, 186, 196, 1);
-moz-box-shadow: 2px 3px 3px -1px rgba(196, 186, 196, 1);
box-shadow: 2px 3px 3px -1px rgba(196, 186, 196, 1);
}
ul.dropdown-menu.show {
display: block;
}
ul.dropdown-menu li a {
text-decoration: none;
color: #030303;
width: 100%;
float: left;
padding: 7px 15px;
box-sizing: border-box;
}
ul.dropdown-menu li a:hover {
background: #ddd;
}
ul.dropdown-menu li a i {
margin-right: 10px;
}
.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;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.12.0-2/css/all.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<!DOCTYPE html>
<html>
<head>
</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-opener2"><i class="fa fa-ellipsis-v" aria-hidden="true"></i></label>
<input class="dropdown-toggle" type="checkbox">
<ul class="dropdown-menu">
<li><a href="#"> <i class="fa fa-heart" aria-hidden="true"></i> Wishlist</a></li>
<li><a href="#"><i class="fa fa-share-alt" aria-hidden="true"></i>
Share</a></li>
<li><a href="#"><i class="fa fa-ban" aria-hidden="true"></i> Not interested</a></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-opener2"><i class="fa fa-ellipsis-v" aria-hidden="true"></i></label>
<input class="dropdown-toggle" type="checkbox">
<ul class="dropdown-menu">
<li><a href="#"> <i class="fa fa-heart" aria-hidden="true"></i> Wishlist</a></li>
<li><a href="#"><i class="fa fa-share-alt" aria-hidden="true"></i>
Share</a></li>
<li><a href="#"><i class="fa fa-ban" aria-hidden="true"></i> Not interested</a></li>
</ul>
</nav>
</div>
</body>
</html>
Hi Please check this fiddle, Hope this will help you
https://jsfiddle.net/hm2zuo3r/2/