Created
June 18, 2019 03:53
-
-
Save jimratliff/96621f205e18e060a10a5329bf247808 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<!--See Elijah Manor, "CSS Animated Hamburger Icon," March 4, 2014. | |
https://elijahmanor.com/css-animated-hamburger-icon/ --> | |
<!--See also my equivalent CodePen: https://codepen.io/jimratliff/pen/agdxwX --> | |
<!-- | |
We start with a "hamburger" icon (three vertically stacked horizontal bars), created | |
purely by CSS (i.e., no image file and no icon font), which is commonly understood | |
to signal that a click on the hamburger will trigger a menu to reveal itself. | |
When the handburger is clicked on: | |
* The menu should drop (not included here) | |
* The hamburger icon turns into an X to signal the user that clicking on it again will | |
close the menu. | |
To achieve the hamburger-to-X transformation, clicking on the hamburger triggers | |
JavaScript to add "hamburger-active" to the list of CSS classes for each slice. Each | |
slice then transforms simultaneously: | |
* The middle slice disappears, by its background transitioning to transparent. | |
* Each of the top and bottom slice transforms by both a translation and a rotation. | |
* They each are vertically translated to the original location of the middle slice | |
* They each rotate 45° but in opposite directions. (The origin of each rotation is | |
the default: the center of the slice.) | |
* Note: Elijah's code didn't have the vertical translation explicitly; rather it | |
specified `top = 0;` for each. I find my approach clearer. | |
In addition, the height of the button transforms to the required height after the | |
hamburger as turned into an "X". This ensures that the vertical extent of the | |
clickable area is sufficient to include all of the "X". | |
To diagnose/confirm the clickable area in both hamburger and "X" mode, you can change | |
the background color `--myprefix-hamburger-color-background` (away from | |
`transparent`) to make this region distinguishable. | |
The appearance of the hamburger itself is controlled by: | |
--myprefix-hamburger-color-hamburger | |
--myprefix-hamburger-width: | |
--myprefix-hamburger-slice-thickness | |
--myprefix-hamburger-gap-between-slices | |
The location of the hamburger is controlled by: | |
--myprefix-hamburger-position-type: absolute ; | |
--myprefix-hamburger-left | |
--myprefix-hamburger-top | |
The speed of the transition is determined by: | |
--myprefix-hamburger-transition-time | |
The keyword `myprefix` is meant for you to replace with a unique identifier for your | |
project to ensure that the names of styles and CSS variables for the hamburger | |
won't collide with other names. | |
--> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Hamburgers</title> | |
<style type="text/css" media="screen"> | |
/* Begin CSS relevant to the example page only! Do not include in app */ | |
body{ | |
background-color: powderblue ; | |
height: 100% ; | |
} | |
h1 { | |
color: black; | |
font-size: 2em ; | |
} | |
p { | |
color: black; | |
} | |
p a:link { | |
color: blue; | |
} | |
/* End CSS relevant to the example page only! Do not include in app */ | |
/* BEGIN: Define CSS "custom properties" (aka CSS variables) */ | |
:root{ | |
/* Background color for entire hamburger button. | |
Typically this would be transparent, but you can change it for diagnostic | |
purposes because this area corresponds to the clickable area. | |
*/ | |
--myprefix-hamburger-color-background: transparent ; | |
/* Outline styling of button */ | |
/* Can be useful for diagnostic purposes. The outlined area appears to be the | |
same as the background-color area, which is the same as the clickable | |
region. | |
In any case, these definitions are important in order to override the | |
user-agent defaults. | |
*/ | |
--myprefix-hamburger-outline-style: none ; | |
--myprefix-hamburger-outline-width: 1px ; | |
--myprefix-hamburger-outline-color: black ; | |
/* At a minimum, specifying border styles serves the purpose of overriding | |
the default border specifications from the user-agent | |
*/ | |
--myprefix-hamburger-border-style: none ; | |
--myprefix-hamburger-border-width: 1px ; | |
--myprefix-hamburger-border-color: purple ; | |
/* Positions/pads the hamburger */ | |
--myprefix-hamburger-position-type: absolute ; | |
/* If --myprefix-hamburger-position-type is "absolute", these specify location | |
of hamburger relative to its containing block (i.e., the ancestor relative to | |
which the element is positioned.) | |
*/ | |
--myprefix-hamburger-left: 10em ; | |
--myprefix-hamburger-top: 20em ; | |
/* Sets padding around the button. Importantly, padding is included in the | |
clickable area of the button. | |
*/ | |
--myprefix-hamburger-padding-top: 15px ; | |
--myprefix-hamburger-padding-bottom: 15px ; | |
--myprefix-hamburger-padding-left: 15px ; | |
--myprefix-hamburger-padding-right: 15px ; | |
--myprefix-hamburger-padding: var(--myprefix-hamburger-padding-top) | |
var(--myprefix-hamburger-padding-right) | |
var(--myprefix-hamburger-padding-bottom) | |
var(--myprefix-hamburger-padding-left) ; | |
--myprefix-hamburger-margin-top: 10px ; | |
--myprefix-hamburger-margin-bottom: 10px ; | |
--myprefix-hamburger-margin-left: 10px ; | |
--myprefix-hamburger-margin-right: 10px ; | |
--myprefix-hamburger-margin: var(--myprefix-hamburger-margin-top) | |
var(--myprefix-hamburger-margin-right) | |
var(--myprefix-hamburger-margin-bottom) | |
var(--myprefix-hamburger-margin-left) ; | |
/* The common width/height of the slices are determined by | |
--myprefix-hamburger-width and --myprefix-hamburger-slice-thickness | |
*/ | |
--myprefix-hamburger-width: 44px ; | |
--myprefix-hamburger-slice-thickness: 5px ; | |
--myprefix-hamburger-gap-between-slices: 10px ; | |
/* The below larger sizes exist for diagnosing the clickable area | |
--myprefix-hamburger-width: 88px ; | |
--myprefix-hamburger-slice-thickness: 10px ; | |
--myprefix-hamburger-gap-between-slices: 20px ; | |
*/ | |
/* Calculates total width of hamburger, including any padding. Used to provide the | |
horizontal dimension for the enclosing <button>. | |
*/ | |
--myprefix-hamburger-total-width-not-X: | |
calc( var( --myprefix-hamburger-width ) + | |
var( --myprefix-hamburger-padding-left ) + | |
var( --myprefix-hamburger-padding-right ) | |
) ; | |
/* Calculates total height of hamburger. Used to provide the vertical dimension | |
for the enclosing <button> in its hamburger configuration | |
*/ | |
--myprefix-hamburger-total-height-not-X: | |
calc( var( --myprefix-hamburger-slice-thickness ) + | |
2 * var( --myprefix-hamburger-gap-between-slices ) + | |
var( --myprefix-hamburger-padding-bottom ) + | |
var( --myprefix-hamburger-padding-top ) | |
) ; | |
--myprefix-hamburger-total-height-as-X: | |
calc( 0.70707 * var( --myprefix-hamburger-width ) + | |
var( --myprefix-hamburger-padding-bottom ) + | |
var( --myprefix-hamburger-padding-top ) | |
) ; | |
/* Determines color of the hamburger slices */ | |
--myprefix-hamburger-color-hamburger: red ; | |
/* Controls how rounded the corners of the hamburger bars are */ | |
--myprefix-hamburger-border-radius: 7px ; | |
/* Controls speed of animation */ | |
--myprefix-hamburger-transition-time: 600ms ; | |
} | |
/* END: Define CSS "custom properties" */ | |
button#nav-toggle { | |
background-color: var( --myprefix-hamburger-color-background ) ; | |
position: var( --myprefix-hamburger-position-type ) ; | |
cursor: pointer ; | |
left: var( --myprefix-hamburger-left ) ; | |
top: var( --myprefix-hamburger-top ) ; | |
padding: var( --myprefix-hamburger-padding ) ; | |
margin: var( --myprefix-hamburger-margin ) ; | |
width: var( --myprefix-hamburger-total-width-not-X ) ; | |
height: var( --myprefix-hamburger-total-height-not-X ) ; | |
outline-style: var( --myprefix-hamburger-outline-style ) ; | |
outline-width: var( --myprefix-hamburger-outline-width ) ; | |
outline-color: var( --myprefix-hamburger-outline-color ) ; | |
border-style: var( --myprefix-hamburger-border-style ) ; | |
border-width: var( --myprefix-hamburger-border-width ) ; | |
border-color: var( --myprefix-hamburger-border-color ) ; | |
} | |
.myprefix-hamburger span, | |
.myprefix-hamburger span:before, | |
.myprefix-hamburger span:after { | |
display: block ; | |
cursor: pointer ; | |
height: var(--myprefix-hamburger-slice-thickness) ; | |
width: var(--myprefix-hamburger-width) ; | |
border-radius: var(--myprefix-hamburger-border-radius) ; | |
background: var(--myprefix-hamburger-color-hamburger) ; | |
content: '' ; | |
transition: all var(--myprefix-hamburger-transition-time) ease-in-out; | |
} | |
.myprefix-hamburger span { | |
/* Getting the right combination of values for the `position` property for both | |
this middle slice and the top and bottom slices was tricky and somewhat | |
trial-and-error. | |
The challenge was getting the three vertically stacked bars to sit | |
vertically centered within the button's clickable area. | |
I benefited from both: | |
jamesplease's answer <https://stackoverflow.com/a/14196691/8401379> to | |
":before, :after and padding," Stack Overflow, January 7, 2013, and | |
Jacob Gray's answer <https://stackoverflow.com/a/31969031/8401379> to | |
"Pseudo-elements and padding," Stack Overflow, August 12, 2015, | |
both of whom suggested, effectively, making the middle slice position:relative | |
and making the ::before and ::after pseudo elements position:absolute. | |
These choices solved the problem. | |
*/ | |
position: relative ; | |
} | |
/* The :before pseudo element is the top slice */ | |
.myprefix-hamburger span:before { | |
/* Raises the top slice above the middle slice */ | |
position: absolute ; | |
top: calc( -1 * var(--myprefix-hamburger-gap-between-slices) ) ; | |
} | |
/* The :after pseudo element is the bottom slice */ | |
.myprefix-hamburger span:after { | |
/* Raises the bottom slice by the same amount it was originally lowered */ | |
position: absolute ; | |
bottom: calc(-1 * var(--myprefix-hamburger-gap-between-slices) ) ; | |
} | |
/* Implements animation */ | |
/* | |
I am electing NOT to switch the height of the "X" button to its higher value when | |
it's an "X" (compared to when it's a hamburger). This caused the following problem: | |
"Note a remaining animation glitch. After the hamburger changes to an "X," it remains | |
in that state until clicked. When clicked, the "X" takes a small sudden jump up a few | |
pixels and then smoothly untransforms into a hamburger. I haven't figure out what | |
causes the jump." | |
Thus I comment out the next redefinition of `height`: | |
*/ | |
button#nav-toggle.hamburger-active { | |
/* height: var( --myprefix-hamburger-total-height-as-X ) ; */ | |
transition: all var(--myprefix-hamburger-transition-time) ease-in-out; | |
} | |
.myprefix-hamburger.hamburger-active span { | |
/* Fades out the middle slice */ | |
background-color: transparent; | |
} | |
.myprefix-hamburger.hamburger-active span:before { | |
/* (a) Translates downwards the top slice to coincide with the original middle | |
slice, and simultaneously (b) rotates the top slice clockwise | |
*/ | |
transform: translateY( var( --myprefix-hamburger-gap-between-slices ) ) | |
rotate(45deg); | |
} | |
.myprefix-hamburger.hamburger-active span:after { | |
/* (a) Translates upwards the bottom slice to coincide with the original middle | |
slice, and simultaneously (b) rotates the bottom slice counterclockwise | |
*/ | |
transform: translateY( calc( -1 * var(--myprefix-hamburger-gap-between-slices) ) ) | |
rotate(-45deg); | |
} | |
</style> | |
</head> | |
<body> | |
<h1>CSS Animated Hamburger Icon</h1> | |
<p>Draws heavily from Elijah Manor's | |
"<a href="https://elijahmanor.com/css-animated-hamburger-icon/" | |
title="CSS Animated Hamburger Icon">CSS | |
Animated Hamburger Icon,</a> March 4, 2014.</p> | |
<button id="nav-toggle" class="myprefix-hamburger"><span></span></button> | |
<script> | |
/* Note: This script must follow (not precede) the DOM element it references */ | |
document.querySelector( "#nav-toggle" ) | |
.addEventListener( | |
"click", | |
function() { | |
this.classList.toggle( "hamburger-active" ); | |
} | |
); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment