3

enter image description here

Problem: the middle bar is a pixel taller than the other bars. This is not because I got the css wrong, it's because browsers align the edges of rectangles to the pixel grid.

I've tried a few different ways of making a hamburger icon:

  1. absolutely position a few divs with background-colors.

  2. inline svg tag

  3. svg as a background image

  4. single div with top/bottom border and background-color for the bars, and padding and background-clip: content-box; for the spaces.

All of these methods have the same problem:

At some zoom-levels/sizes, the icon looks bad because the three rectangles aren't all the same size. They're different sizes sometimes because browsers like to align the edges of rectangles with the pixel grid.

I of course want the two spaces between the rectangles to be the same thickness too.

I get that this comes with the compromise that the icon must be a multiple of 5px tall. Or, if you can expand the spaces by one pixel at alternating breakpoints than when you expand the bars, then it can be 2X or 3X as many different sizes.

So what's the trick?

Can a webfont use hinting to make it so all three rectangles get the same number of pixels of thickness regardless of size/resolution/zoom?

Is there some sort of CSS math I can do?

I'm really hoping I don't have to have JavaScript that tries to detect changes in the zoom level.

EDIT: LIVE DEMO

var hamburgerIconEl = document.getElementById('hamburgerIcon');
var fontSizeEl = document.getElementById('fontSize');
fontSizeEl.addEventListener('input', function (e) {
    hamburgerIconEl.style.fontSize = '' + (8 + parseFloat(fontSizeEl.value) / 100) + 'px';
});
    .body {
        background: white;
    }
    #hamburgerIcon {
        box-sizing: border-box;
        display: inline-block;
        height: 1.7ex;
        width: 2.04ex;
        border-width: .34ex 0;
        border-style: solid;
        border-color: black;
        background-clip: content-box;
        padding: .34ex 0;
        background-color: black;
    }
<p>Change the font size, watch the hamburger icon, see how there are a lot of sizes in which the bars don't all have the same thickness and/or spacing.</p>
<div>
  <input type="range" id="fontSize" min="0" max="1000" value="0">
  <label for="volume">Font Size</label>
</div>
<div>
    <div id="hamburgerIcon" style="font-size: 8px"></div> Menu
</div>
JasonWoof
  • 4,176
  • 1
  • 19
  • 28
  • 1
    Can you post your code so we can help? We don't know what you're working with. – disinfor Dec 10 '21 at 19:51
  • I don't have any code that produces the desired result. If you know a way, please post it. – JasonWoof Dec 10 '21 at 19:58
  • I've added a live code snippet with a slider that scales the icon, so you can see how it looks good at some sizes and bad at others. Note that this depends on the screen resolution / zoom level, so I can't just pick a size that's good on my screen and expect it to look good everywhere. – JasonWoof Dec 10 '21 at 20:18
  • Obviously you don't have the code that produces the result you want...or you wouldn't be asking for help :) The snippet is what I was asking for since we can test it now. – disinfor Dec 10 '21 at 22:29

4 Answers4

3

Now, I haven't seen your inline SVG, but could the issue be that you placed the lines too close to the edge of the SVG? If so, part of the top and bottom lines will seam thinner because they are cute off.

var hamburgerIconEl = document.getElementById('hamburgerIcon');
var fontSizeEl = document.getElementById('fontSize');
fontSizeEl.addEventListener('input', function (e) {
  hamburgerIconEl.style.fontSize = '' + (8 + parseFloat(fontSizeEl.value) / 100) + 'px';
});
svg {
  height: 2em;
  width: 2em;
  font-size: 8px;
}
<div>
  <input type="range" id="fontSize" min="0" max="1000" value="0">
  <label for="volume">Font Size</label>
</div>
<div>
  <svg viewBox="0 0 10 10" id="hamburgerIcon">
    <path d="M 1 3 H 8 M 1 5 H 8 M 1 7 H 8" fill="none" stroke="black" stroke-width="1" />
  </svg>
</div>
chrwahl
  • 8,675
  • 2
  • 20
  • 30
  • Ooh, that's an improvement :) So far this looks the best in chrome anyway. But I'm still hoping to find a way to get the height to be just-so automatically so no anti-aliasing is needed. – JasonWoof Dec 11 '21 at 19:43
1

Systems do struggle with what to do with part pixels - one CSS pixel can map to several screen pixels on modern devices.

When there is an attempt at showing a fraction of a pixel there has to be a decision on whether to leave a screen pixel 'behind' or not.

A slightly simpler way of drawing the hamburger, using background image linear gradient, removes the problem of the middle bar sometimes not appearing centered. There are however the odd occasions where the middle bar is not exactly the same height as the other two.

var hamburgerIconEl = document.getElementById('hamburgerIcon');
var fontSizeEl = document.getElementById('fontSize');
const label = document.querySelector('label');
fontSizeEl.addEventListener('input', function(e) {
  hamburgerIconEl.style.fontSize = '' + (8 + parseFloat(fontSizeEl.value) / 100) + 'px';
  label.innerHTML = 'Font Size = ' + hamburgerIconEl.style.fontSize;
});
#hamburgerIcon {
  display: inline-block;
  padding: 0;
  margin: 0;
  height: 1.7ex;
  width: 2.05ex;
  background-image: linear-gradient(black 0 20%, transparent 20% 40%, black 40% 60%, transparent 60% 80%, black 80% 100%);
}
<div>
  <input type="range" id="fontSize" min="0" max="1000" value="0">
  <label for="volume">Font Size</label>
</div>
<div id="hamburgerIcon"></div>
A Haworth
  • 30,908
  • 4
  • 11
  • 14
0

This is a super tricky issue. Different browsers round half-pixels in different ways and have different zoom-levels, so it will be hard to set a size that scales well on all browsers and zooms.

You can check out this question if you haven't found it already.

What you could do is try to use a <canvas> element to draw your shapes. This scales more evenly to my experience.

Example:

const canvas = document.getElementById('canvas')
if (canvas.getContext) {
    var ctx = canvas.getContext('2d');
    ctx.fillStyle = 'green';
    ctx.fillRect(0, 0, 30, 1);
    ctx.clearRect(0, 1, 30, 2);
    ctx.fillRect(0, 3, 30, 1);    
    ctx.clearRect(0, 4, 30, 2);
    ctx.fillRect(0, 6, 30, 1);
  }
.container {
  padding: 10px 20px;
  display: block;
}

.burger {
  background-color: green;
  background-clip: content-box;
  padding-top: 2px;
  padding-bottom: 2px;
  border-top: 2px solid green;
  border-bottom: 2px solid green;
  height: 2px;
  width: 30px;
}
<div class="container">
  <h5>Div with bordrder</h5>
  <div class="burger one"></div>
</div>


<div class="container">
  <h5>Canvas</h5>
  <canvas id="canvas"></canvas>
</div>
MarcRo
  • 2,434
  • 1
  • 9
  • 24
0

Here's a javascript solution. It works perfectly for me on Chrome, but fails in creative ways on my iPad.

var hamburgerIconEl = document.getElementById('hamburgerIcon');
var fontSizeEl = document.getElementById('fontSizeSlider');
var dppxEl = document.getElementById('dppx');
function updateHamburgerIcon () {
    var height = parseFloat(getComputedStyle(hamburgerIconEl).height);
    var physPx = height * window.devicePixelRatio;
    var sparePhysPx = physPx % 5;
    var sparePx = sparePhysPx / window.devicePixelRatio;
    hamburgerIconEl.style.setProperty('--sparePx', '' + sparePx + 'px');
}
fontSizeEl.addEventListener('input', function (e) {
    hamburgerIconEl.parentElement.style.fontSize = '' + (8 + parseFloat(fontSizeEl.value) / 50) + 'px';
    updateHamburgerIcon();
});
updateHamburgerIcon();
function onResolutionChange (cb) {
    dppxEl.innerText = '' + window.devicePixelRatio;
    matchMedia('(resolution: ' + window.devicePixelRatio + 'dppx)')
        .addEventListener("change",
            function (e) {
                onResolutionChange(cb);
                cb(e);
            }, { once: true });
}
onResolutionChange(updateHamburgerIcon);
.body {
    background: white;
    color: black;
}
#fontSizeSlider {
    width: 100%;
}
#hamburgerIcon {
    display: inline-block;
    height: .75em;
    width: .88em;
    background-color: transparent;
    background-position: 50% 100%;
    background-repeat: no-repeat;
    background-image: linear-gradient(black 0 20%, transparent 20% 40%, black 40% 60%, transparent 60% 80%, black 80% 100%);
    --sparePx: 0px;
    background-size: auto calc(.75em - var(--sparePx));
}
<p>Try the size slider below, and also changing your zoom (current dppx: <span id="dppx"></span>)</p>
<div>
    <input type="range" id="fontSizeSlider" min="0" max="1000" value="0">
</div>
<div style="font-size: 8px">
    <div id="hamburgerIcon"></div> Menu
</div>
JasonWoof
  • 4,176
  • 1
  • 19
  • 28