skip to Main Content

Implementing a design, i ran into a tricky problem.

OVERVIEW

I have a menu with items, each of which contain a panel that opens upon hover. The content of each of these is dynamic, so the width of each panel can be (and is) different. The panel has a gradient as a background color:

background: linear-gradient(135deg, #579AD1 0%, #1F679D 100%);

Directly above the panel i want to display an arrow pointing upwards towards the menu item. I did this by placing a DIV within the menu item element and positioning it, so that it sits at 50% minus the arrow’s width, thus being centered neatly underneath the menu item. In cases where the panel exceeds the outer container’s width, it gets shifted to the left using negative margin-left which works fine, making sure the panel is within the outer container of the page.

Example

THE PROBLEM

The arrow’s color should blend smoothly into the panel. As you can see in my example however, the color of the arrow (being a fixed background color) will blend into the background of the panel either more or less. The wider the panel is, and the much further it is positioned to the left, the more the gradient color differs from the arrow’s color, which means that the arrow doesn’t blend into the gradient background nicely.

WHAT I TRIED SO FAR

Trying to solve this, i considered/tried three things:

  1. Using clip-path on the arrow element, to cut out a triangular shape, with the arrow element having the same width and the same gradient as the panel. This however does not work, because i need the arrow to have a 1px white border on the top-left and top-right angles.

  2. Copying the panel into a CANVAS and then reading the pixel color at the desired position. This fails entirely, it only seems to work with images.

  3. Writing a function that i can pass two arguments: a width and an arrow position. The function would then calculate the most optimal background color for the arrow by approximating the gradients color at the given arrow position based on the panel’s background gradient.

     calculateArrowColor: function(divWidth, arrowPosX) {
    
         const color1 = [78, 138, 188];
         const color2 = [49, 112, 162];
         const gradientSteps = divWidth / 2;
         const stepSize = 1 / gradientSteps;
         const arrowPosStep = arrowPosX / divWidth;
         const arrowColor = [];
    
         for (let i = 0; i < 3; i++) {
             const color1Value = color1[i];
             const color2Value = color2[i];
             let arrowColorValue;
             if (arrowPosStep < 0.5) {
                 const currentStep = arrowPosStep / stepSize;
                 const colorDifference = color1Value - color2Value;
                 arrowColorValue = color1Value - (currentStep * colorDifference);
             } else {
                 const currentStep = (arrowPosStep - 0.5) / stepSize;
                 const colorDifference = color2Value - color1Value;
                 arrowColorValue = color1Value + (currentStep * colorDifference);
             }
             arrowColor.push(Math.round(arrowColorValue));
         }
    
         return `rgb(${arrowColor.join(",")})`;
    
     };
    

Unfortunately my function doesn’t work and i can’t figure out why. It returns values out of range that make no sense.

Maybe my approach isn’t that clever.

I am looking for a good solution for this tricky problem.

Here is a workable snippet: (click the panel to shift it to the left, right-click it to increase its width)

$(document).ready(function(){
  $('.panel').on('click', function(){
    $(this).css('margin-left', '-200px');
  }).on('contextmenu', function(e){
    $(this).css('width', '600px');
    e.preventDefault();
  });
});
ul, li {
  list-style: none;
  margin: 0;
  padding: 0;
}
ul {
  width: 600px;
  margin: 0 auto;
  background: #369;
}
ul > li {
  color: #fff;
  padding: 5px 10px;
  display: inline-block;
}
div.label {
  position: relative;
}
div.arrow {
  position: relative;
  top: 20px;
  left: 50%;
  z-index: 10;
}
div.arrow:before, div.arrow:after {
  border: solid transparent;
  content: " ";
  display: block;
  height: 0;
  position: absolute;
  pointer-events: none;
  width: 0;
  bottom: 100%;
  border-color: transparent;
  border-bottom-color: #fff;
  left: 0;
  margin-left: -11px;
  border-width: 11px;
}
div.arrow:after {
  border-color: rgba(255, 255, 255, 0);
  border-bottom-color: #69c;
  left: 0;
  margin-left: -10px;
  border-width: 10px;
}
div.panel {
  background: linear-gradient(135deg, #579AD1 0%, #1F679D 100%);
  min-width: 320px;
  height: 120px;
  position: absolute;
  top: 38px;
  left: 0;
  box-shadow: 0px 0px 9.5px 0.5px rgba(0, 0, 0, 0.24);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul>
  <li>
    <div class="label">
      <span>MENU ITEM</span>
      <div class="arrow"></div>
      <div class="panel"></div>
    </div>
  </li>
</ul>

Thank You!

2

Answers


  1. As you aim to use JS anyway, to calculate the color, I suggest you let CSS do the color selection but use a bit of JS to find out where to draw the arrow head.

    This snippet ignores the arrow element, which probably shouldn’t be in the DOM anyway as it doesn’t have particular meaning but is just a visual clue.

    Instead it puts an after pseudo element on the panel, with almost the same dimensions and background gradient and positions it just above the panel. clip-path is then used to create the pointy bit.

    To get the white outline to the arrowhead it as a before pseudo element background white and clips that to be just slightly bigger than the colored arrow head. [This snippet shows it in red as it is otherwise difficult to see it against the rather faint shadow].

    Note, the arrow head has a gradient to its coloring, not just one color, but it’s too small to notice really.

    You may like to parameterise the size for the arrow – you’ll see 14px and variations all over the place here, would be more flexible if set at a CSS variable.

    <style>
      ul,
      li {
        list-style: none;
        margin: 0;
        padding: 0;
      }
      
      ul {
        width: 600px;
        margin: 0 auto;
        background: #369;
      }
      
      ul>li {
        color: #fff;
        padding: 5px 10px;
        display: inline-block;
      }
      
      div.label {
        position: relative;
      }
      
      div.panel {
        min-width: 320px;
        height: 120px;
        position: absolute;
        top: 38px;
        left: 0;
        box-shadow: 0px 0px 9.5px 0.5px rgba(0, 0, 0, 0.24);
        background: linear-gradient(135deg, #579AD1 0%, #1F679D 100%);
      }
      
      div.panel::before {
        content: '';
        background: red;
        position: absolute;
        left: 0;
        top: -15px;
        width: 100%;
        height: calc(100% + 15px);
        z-index: 1;
        clip-path: polygon(0 15px, calc(var(--midpoint) - 14px - 1px) 15px, var(--midpoint) 0, calc(var(--midpoint) + 15px) 15px, 100% 15px, 100% 16px, 0 16px);
      }
      
      div.panel::after {
        rdisplay: none;
        content: '';
        background: linear-gradient(135deg, #579AD1 0%, #1F679D 100%);
        position: absolute;
        left: 0;
        top: -14px;
        width: 100%;
        height: calc(100% + 14px);
        z-index: 1;
        clip-path: polygon(0 14px, calc(var(--midpoint) - 14px) 14px, var(--midpoint) 0, calc(var(--midpoint) + 14px) 14px, 100% 14px, 100% 15px, 0 15px);
      }
    </style>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <ul>
      <li>
        <div class="label">
          <span>MENU ITEM</span>
          <div class="arrow"></div>
          <div class="panel"></div>
        </div>
      </li>
    </ul>
    <script>
      function positionArrows() {
        const labels = document.querySelectorAll('.label');
        for (let i = 0; i < labels.length; i++) {
          const labelLeft = labels[i].getBoundingClientRect().left;
          const labelWidth = labels[i].getBoundingClientRect().width;
          const panel = labels[i].querySelector('.panel');
          const panelLeft = panel.getBoundingClientRect().left;
          labels[i].style.setProperty('--midpoint', (labelLeft - panelLeft + labelWidth / 2) + 'px');
        }
      }
      $(document).ready(function() {
        $('.panel').on('click', function() {
          $(this).css('margin-left', '-200px');
          positionArrows();
        }).on('contextmenu', function(e) {
          $(this).css('width', '600px');
          e.preventDefault();
          positionArrows();
        });
      });
      positionArrows();
      window.onresize = positionArrows;
    </script>
    Login or Signup to reply.
  2. Does the following suit your needs? I used clip-path and tweaked with the borders, the main key is the background-attachment: fixed;

    $(document).ready(function() {
      $('.panel').on('click', function() {
        $(this).css('margin-left', '-200px');
      }).on('contextmenu', function(e) {
        $(this).css('width', '600px');
        e.preventDefault();
      });
    });
    ul,
    li {
      list-style: none;
      margin: 0;
      padding: 0;
    }
    
    ul {
      width: 600px;
      margin: 0 auto;
      background: #369;
    }
    
    ul>li {
      color: #fff;
      padding: 5px 10px;
      display: inline-block;
    }
    
    div.label {
      position: relative;
    }
    
    div.arrow {
      background: linear-gradient(135deg, #579AD1 0%, #1F679D 100%);
      min-width: 22px;
      height: 11px;
      position: absolute;
      top: 28px;
      left: 50%;
      box-shadow: 0px 0px 9.5px 0.5px rgba(0, 0, 0, 0.24);
      clip-path: polygon(50% 0%, 0% 100%, 100% 100%);
      z-index: 10;
      background-attachment: fixed;
      margin-left: -11px;
      /*negative margin-left which is half of the width of the element to be positioned*/
    }
    
    div.arrow:before,
    div.arrow:after {
      border: solid #fff;
      content: " ";
      display: block;
      height: 0;
      position: relative;
      pointer-events: none;
      width: 0;
      bottom: 100%;
      border-color: #fff;
      border-bottom-color: transparent;
      left: 0px;
      margin-left: 0px;
      border-width: 13px;
      top: -13px;
    }
    
    
    /*
    div.arrow:after {
      border-color: rgba(255, 255, 255, 0);
      border-bottom-color: #69c;
      left: 0;
      margin-left: -10px;
      border-width: 10px;
    }*/
    
    div.panel {
      background: linear-gradient(135deg, #579AD1 0%, #1F679D 100%);
      min-width: 320px;
      height: 120px;
      position: absolute;
      top: 38px;
      left: 0;
      box-shadow: 0px 0px 9.5px 0.5px rgba(0, 0, 0, 0.24);
      background-attachment: fixed;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <ul>
      <li>
        <div class="label">
          <span>MENU ITEM</span>
          <div class="arrow"></div>
          <div class="panel"></div>
        </div>
      </li>
    </ul>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search