Hamburger Morphing
A lightweight ES module for creating animated hamburger menu icons that can morph into various shapes using SVG.js — plus a native Rust/egui port of the same technique.
Features
- Stacked-Layer Morphing: Three stacked SVG lines that smoothly appear and disappear behind each other, letting one icon fluidly morph into another
- Smooth Animations: 500ms morphing animations between shapes
- Configurable: Customize size, foreground, background, and hover colors
- Lightweight: Minimal dependencies (only SVG.js)
- ES Module: Modern JavaScript module format
- Multiple Shapes: Support for 25 different icon shapes
Rust / egui Version
A native Rust port of the same stacked-layer morphing technique lives in rust/, as a Cargo workspace with two crates:
rust/hamburger-morphing— the reusable library crate. Add it to any egui project as a path or git dependency.rust/demo— a test window for viewing and trying out the morphing.
Running the test window
cd rust
cargo run -p hamburger-morphing-demo
The test window includes:
- Main icon — click it to toggle between ☰ and ✕
- Shape gallery — all 25 shapes drawn side by side; click any tile to morph the main icon to it
- Settings — sliders for size and duration, color pickers for foreground/background/hover (defaults: black on white; the background color fills the whole window)
- Auto-cycle — automatically morphs through every shape in sequence
- Snap buttons — jump to a shape without animating
Using the library in another egui project
[dependencies]
hamburger-morphing = { path = "path/to/rust/hamburger-morphing" } # or { git = "..." }
eframe = "0.35"
use hamburger_morphing::{HamburgerMorphing, Shape};
struct MyApp {
burger: HamburgerMorphing,
}
impl eframe::App for MyApp {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
let ctx = ui.ctx().clone();
if ui.button("Close").clicked() {
self.burger.animate_to_ctx(Shape::X, &ctx);
}
// Paint the icon; it animates smoothly towards the target shape.
let response = self.burger.paint(ui);
// Toggle between ☰ and ✕ when clicked:
if response.clicked() {
let target = match self.burger.shape() {
Shape::X => Shape::Burger,
_ => Shape::X,
};
self.burger.animate_to_ctx(target, &ctx);
}
}
}
The Rust API mirrors the JavaScript one:
| JavaScript | Rust |
|---|---|
new HamburgerMorphing(sel, opts) |
HamburgerMorphing::new(size) with .with_foreground(...) / .with_background(...) / .with_hover(...) |
hamburger.animateTo('x') |
burger.animate_to_ctx(Shape::X, ctx) (or animate_to(shape, now_secs)) |
| shape name strings | The Shape enum (Shape::X, Shape::ArrowLeft, ...) with as_str() / FromStr for the JS names |
| — | burger.snap_to(shape) for instant, non-animated jumps |
| — | burger.paint(ui) returns an egui Response (click/hover) |
Run the library's test suite with:
cd rust && cargo test
Installation (JavaScript)
As a Git Submodule
Add this project as a submodule to your repository:
git submodule add <repository-url> lib/hamburger-morphing
Manual Installation
- Download
hamburger.mjsfrom this repository - Include SVG.js in your project (CDN or npm)
- Import the module in your JavaScript
Quick Start
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/@svgdotjs/svg.js@3.0/dist/svg.min.js"></script>
</head>
<body>
<div id="menu-icon"></div>
<script type="module">
import { HamburgerMorphing } from './lib/hamburger-morphing/hamburger.mjs';
// Create hamburger instance
const hamburger = new HamburgerMorphing('#menu-icon', {
size: 80,
foreground: '#ff6b6b',
background: '#ffffff',
hover: '#ff0000'
});
// Morph to different shapes
hamburger.animateTo('x'); // Close/X icon
hamburger.animateTo('burger'); // Back to hamburger
</script>
</body>
</html>
How It Works
The morphing effect is built on one core idea: three SVG <line> elements are stacked on top of each other, and each one can smoothly appear, disappear, or repaint itself. Because the lines are layered, they can slide in and out behind one another — which is what allows a single icon to fluidly morph into 25 different shapes.
The three stacked layers
The icon always consists of exactly three lines, created in order during init():
this.line1 = this.draw.line(20, 20, 80, 20)... // bottom layer
this.line2 = this.draw.line(20, 50, 80, 50)... // middle layer
this.line3 = this.draw.line(20, 80, 80, 80)... // top layer
SVG paints elements in document order, so line3 renders on top of line2, which renders on top of line1.
Appearing and disappearing
Every shape is simply a different assignment of endpoints (x1/y1/x2/y2), stroke-width, and stroke color to the three lines, animated simultaneously over 500ms:
-
Disappearing — a line vanishes by collapsing to a zero-length segment at a single point, tucking it in behind whatever is stacked above it. In the
minusshape, for example,line2andline3both shrink into the center point(50, 50), which lies online1's stroke, so they vanish seamlessly:// 'minus': lines 2 and 3 collapse into the center and disappear behind line1 this.line2.animate(...).attr({ x1: 50, x2: 50, y1: 50, y2: 50 }); this.line3.animate(...).attr({ x1: 50, x2: 50, y1: 50, y2: 50 }); -
Appearing — the reverse: a collapsed line expands from its point into its new position, so new strokes seem to grow out of the existing icon.
-
Cutouts — a line can also be repainted with the
backgroundcolor. Since it sits on top of the stack, it visually erases whatever is beneath it, punching a hole in the icon. This is how thedoughnut(a background-colored line over a fat foreground dot) andmoon(a background-colored dot sliding off the edge of a foreground dot) shapes are drawn:// 'doughnut': line3 becomes a background-colored "hole" on top of the stack this.line3.animate(...).attr({ x1: 50, x2: 50, y1: 50, y2: 50, 'stroke-width': 45, stroke: this.options.background });
Why the morphs look smooth
- All three lines animate simultaneously with the same duration, so the icon never jumps between states.
- Round line caps (
stroke-linecap: round) make collapsing lines shrink into dots instead of hard-edged stubs. - Since every transition is just an interpolation of coordinates, width, and color, any shape can morph directly into any other shape — no keyframes, paths, or shape-specific logic required.
API Reference
Constructor
new HamburgerMorphing(containerSelector, options)
Parameters:
containerSelector(string): CSS selector for the container elementoptions(object, optional): Configuration optionssize(number, default: 100): Size of the SVG in pixelsforeground(string, default: "#000000"): Foreground/stroke color for the linesbackground(string, default: "#ffffff"): Background color for cutout effectshover(string, default: "#ff0000"): Hover color for interactive effects
Example:
const hamburger = new HamburgerMorphing('#menu', {
size: 60,
foreground: '#333333',
background: '#ffffff'
});
Methods
animateTo(shape)
Animates the hamburger icon to the specified shape.
Parameters:
shape(string): Target shape name
Available shapes:
'burger'- Three horizontal lines (default)'x'- Close/X icon'plus'- Plus/add icon'minus'- Minus/subtract icon'arrow_left'- Left arrow'arrow_right'- Right arrow'arrow_up'- Up arrow'arrow_down'- Down arrow'triangle_up'- Triangle pointing up'triangle_down'- Triangle pointing down'triangle_left'- Triangle pointing left'triangle_right'- Triangle pointing right'carrot_right'- Carrot/play shape pointing right'carrot_left'- Carrot/play shape pointing left'carrot_up'- Carrot/play shape pointing up'carrot_down'- Carrot/play shape pointing down'circle'- Perfect circle shape'doughnut'- Doughnut shape with center cutout'moon'- Crescent moon shape'u'- Inverted U shape (∩)'u_up'- Upright U shape (U)'u_left'- Left-facing U shape (⊂)'u_right'- Right-facing U shape (⊃)'hamburger_vertical'- Hamburger with middle line hidden (☰)'hamburger_horizontal'- Horizontal hamburger with middle line hidden (⫘)
Example:
hamburger.animateTo('x');
hamburger.animateTo('arrow_right');
getSvgCode()
Returns the current SVG markup as a string.
Returns: (string) SVG markup
Example:
const svgCode = hamburger.getSvgCode();
console.log(svgCode);
// Output: <svg viewBox="0 0 100 100" width="100px" height="100px">...
destroy()
Cleans up the hamburger instance and removes the SVG from the DOM.
Example:
hamburger.destroy();
Complete Example
<!DOCTYPE html>
<html>
<head>
<title>Hamburger Menu Demo</title>
<script src="https://cdn.jsdelivr.net/npm/@svgdotjs/svg.js@3.0/dist/svg.min.js"></script>
<style>
#menu-icon {
cursor: pointer;
margin: 20px;
}
button {
margin: 5px;
padding: 10px 20px;
}
</style>
</head>
<body>
<div id="menu-icon"></div>
<div>
<button onclick="changeShape('burger')">Hamburger</button>
<button onclick="changeShape('x')">X</button>
<button onclick="changeShape('plus')">Plus</button>
<button onclick="changeShape('minus')">Minus</button>
<button onclick="changeShape('arrow_left')">Arrow Left</button>
<button onclick="changeShape('arrow_right')">Arrow Right</button>
<button onclick="changeShape('arrow_up')">Arrow Up</button>
<button onclick="changeShape('arrow_down')">Arrow Down</button>
<button onclick="changeShape('triangle_up')">Triangle Up</button>
<button onclick="changeShape('triangle_down')">Triangle Down</button>
<button onclick="changeShape('triangle_left')">Triangle Left</button>
<button onclick="changeShape('triangle_right')">Triangle Right</button>
<button onclick="changeShape('carrot_right')">Carrot Right</button>
<button onclick="changeShape('carrot_left')">Carrot Left</button>
<button onclick="changeShape('carrot_up')">Carrot Up</button>
<button onclick="changeShape('carrot_down')">Carrot Down</button>
<button onclick="changeShape('circle')">Circle</button>
<button onclick="changeShape('doughnut')">Doughnut</button>
<button onclick="changeShape('moon')">Moon</button>
<button onclick="changeShape('u')">∩</button>
<button onclick="changeShape('u_up')">U</button>
<button onclick="changeShape('u_left')">⊂</button>
<button onclick="changeShape('u_right')">⊃</button>
<button onclick="changeShape('hamburger_vertical')">☰</button>
<button onclick="changeShape('hamburger_horizontal')">⫘</button>
</div>
<script type="module">
import { HamburgerMorphing } from './lib/hamburger-morphing/hamburger.mjs';
// Create hamburger with custom options
const hamburger = new HamburgerMorphing('#menu-icon', {
size: 100,
foreground: '#007bff',
background: '#ffffff',
hover: '#ff0000'
});
// Make it globally available for onclick handlers
window.changeShape = (shape) => {
hamburger.animateTo(shape);
};
</script>
</body>
</html>
Browser Support
- Modern browsers that support ES modules
- Chrome 61+
- Firefox 60+
- Safari 10.1+
- Edge 16+
Dependencies
- SVG.js (3.0+): Required for SVG manipulation and animations
License
This project is open source and available under the MIT License.
Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Test thoroughly
- Submit a pull request
Demo
JavaScript
Open hamburger.html in your browser to see all available shapes and animations in action.
The demo includes:
- 300x300px container with a 1px border and red dot grid overlay
- 25 interactive buttons for all available shapes
- SVG code output showing the current markup
- Hover effects that change line colors on mouse interaction
- Smooth animations between all shape transitions
Rust
cd rust && cargo run -p hamburger-morphing-demo