Portfolio Rework Part 1: Animation, Micro interaction & SVG

Geoffrey Crofte

UX/UI Designer

I work in the city of Luxembourg as UX Designer in making usable, accessible and performant interfaces to build a better web and application ecosystem. I'm also a Ninja (I know kung-fu) and a Lion Dancer.

Follow on Twitter

I’m pretty sure you didn’t miss  Stéphanie’s Portfolio and blog redesign. If you missed it, no worries, she explained a big part of the process of redesigning a UX portfolio on her blog:

Read the Portfolio Redesign Article

I personally worked on the coding part, but also on the animation and micro-interaction part. And that’s what I want to write about today.

Animation can be considered as part of a visual identity, I was really glad when Stéphanie told to “do whatever you want, you know the style, I trust you”. First because I like when people trust me (they are crazy to do so, but… anyway) and second because I love working on animation and micro-interaction.

Let’s go through the different little animations and their specificities.  I want to share with you some tips, things I learnt before and practiced while creating those interactions, to inspire you.

Before we start, keep in mind that the following portions of code are just snippets taken out of the whole context. Also, you can totally use other units than px in your code, I even recommend relative font units like em or rem.

If you have any question, feel free to ask in the comments and share your animation tips and ideas as well.

Small screens’ main navigation animation

When you visit the website on small screen devices, you might notice the cute little burger menu and the whole animation behind this component.

Animating a burger shaped icon

When it is time to create a mobile menu on a website, I’m always wondering –and not only for this website– if a burger icon button is a right solution. Should we hide and show the menu? I usually prefer things that are not hidden to use, but this is a portfolio menu for a onepage layout.

And for the arguments against burger-icon, I let you read about it here and there if you are curious.

Shape of the burger icon

That burger will be a button with a span element and a text saying something like “Menu”. I guess it’s pretty easy to identify 😀

<button id="open-nav" class="open-main-nav virgin-colada" aria-hidden="true" aria-controls="nav">
	<span class="burger"></span>
	<span class="burger-text">Menu</span>

As you can see, the icon can be destructured into bevels and a little skew. A visual effect on purpose to keep the identity near the original idea: sketchnote/draft style. But the entire button remains visually organized and balanced.

To do so, I used pseudo-elements and CSS transformations on this span elements.

.burger {
	position: relative;
	display: block;
	width: 28px;
	height: 4px;
	margin: 0 auto;
	background: #5A3B5D;
	transform: skew(5deg);
	transition: all .275s;

The skew transformation on .burger allows me to skew (obvious, I know) the 3 lines at the same time.

.burger::after {
	content: '';
	display: block;
	height: 100%;
	background: #5A3B5D;
	transition: all .275s;

.burger::before {
	transform: translateY(-16px) skew(-10deg);

.burger::after {
	transform: translateY(-12px) translateX(-2px) skew(-20deg);

Then each line has its own little skew effect and a small offset on the :after to balance the global shape.

The text is an uppercase medium font. I let you deal with the code, but I suggest you to always couple an icon with a text for accessibility purpose.

Animating the burger icon

The animation idea: remove the central line and cross the top and bottom line to build a cross. To do so, I used CSS Transformations and opacity properties.

Yep. That’s all, you’ve got your burger animation with less than 10 lines of CSS.

.burger.is-open {
	transform: skew(5deg) translateY(-8px) rotate(-45deg);

This creates the first part of the cross: the entire icon skews and rotates at 45 deg.

.burger.is-open::before {
	transform: translateY(0px) skew(-10deg) rotate(75deg);

Meanwhile, I create the second part of the cross by rotating the :before pseudo-element to cross the first part.

.burger.is-open::after {
	transform: translateY(-12px) translateX(10px) skew(-20deg);
	opacity: 0;

Then I needed the :after pseudo-element to be hidden. It’s like a tomato slice running away from the bread on the top-right side, a little bit like it was ejected by the rotation.

Destructured Burger Icon Animation

Let’s see now how the animation of the main menu works.

Animating the main menu

Whether you are on big screen or on small screen, the same HTML markup is used for the main menu. Yep, I need to say it because I still see a lot of duplicated code for building small-screen navigation and big-screen navigation, and sometimes even 3 different structures in the worth scenario.

Here is the HTML code I used to build this menu.

<nav id="nav" class="main-nav" role="navigation">
		<li><a href="#about">About me</a></li>
		<li><a href="#speaker">Speaker & Writer</a></li>
		<li><a href="#work">Work</a></li>
		<li><a href="#contact">Contact</a></li>
		<li><a href="/blog/">Blog</a></li>

Main menu positioning

The “desktop” version is built with flexbox positioning for the header and inline-block links for the navigation.  We switch to mobile version when there’s not more space on the screen and links start colliding with each other. I use something like a content flow method I called Natural Flow First I detailed on my blog, so the menu has its own specific breakpoint.

The only request from Stephanie was “I want a beautiful full screen menu”. The first idea that pops in my mind was a centered list of links in full screen with a beautiful illustration in watermark like I did with Myriam’s portfolio’s menu. Lazy solution, I did it 2 weeks before.

I started with this idea and asked myself: why not keep the bevel effect you already have everywhere?

2 drawings of the same mobile menu

I designed directly in the browser using the Responsive Design Mode of Firefox Web Developer Tool. I started with the final state of all the components of the menu, then though about the animation between the two states.

Drawing of state of the menu animated

To start with the position of the menu, here is the code applied when the main navigation component needs to become a “mobile” nav. (when the links collide)

.main-nav {
	position: fixed;
	top: 0;
	right: 0;
	left: 0;
	bottom: 0;
	text-align: center;
	background: #FFF;
	opacity: 0;
	z-index: -1;
	visibility: hidden;
	transition: all .375s;

.main-nav.is-open {
	opacity: 1;
	z-index: 100;
	visibility: visible;

The container covers the entire viewport thanks to a fixed position. I applied a transition on all the properties, but in fact the only properties that matters is opacity for the visual effect, and z-index to avoid a sudden disappearance.
Visibility property is changed for accessibility reason: it hides the content also for screen readers.

Then I worked on the yellow diagonal effect on the background.

.main-nav::before {
	content: '';
	position: absolute;
	top: 0;
	bottom: 0;
	left: 0;
	right: -15px;
	text-align: center;
	background: #FEDC2A;
	transform-origin: 0 0;
	transform: skew(-14deg) translateX(-120%);
	transition: all .275s .1s;

.main-nav.is-open::before {
	transform: skew(-14deg) translateX(0);

The trick here is to declare the transform origin at 0 0 so the skew doesn’t “push” the yellow band too far to the right.

Transform origin settings: ‘auto’ (left) and ‘0 0’ (right)

Now that I get the background I was looking for, I need to position and skew my main menu.

.main-nav ul {
	display: inline-flex;
	flex-direction: column;
	height: 100%;
	align-items: flex-end;
	justify-content: center;
	transform: translateX(-18%) skew(-16deg);

Have a look at the skew value. It’s a bit more than the yellow band because of the visual alignment of the text.

At this point, you are totally right to think the readability of the menu is not the best 😀

Left with skew on the list. Right with skew on the list and its items.

We have to skew each nav item in the other direction to get the right alignment. Let’s do that.

.main-nav li {
	display: block;
	margin: .5rem 0;
	text-align: right;
	transform: skew(16deg);

You saw during the animation that each link appears with a little delay one after another. To do so we need to hide temporarily the links and move them a little bit to the top, then make them appear one by one.

.main-nav a {
	font-size: 1.5rem;
	opacity: 0;
	transform: translateY(-10px);

.main-nav.is-open a {
	opacity: 1;
	transform: translateY(0);

To quickly build the code I used Sass and a small loop from 1 to 5 (my 5 links)

@for $i from 1 through 5 {
	&:nth-child(#{$i}) a {
		transition: all .275s .125s + $i * .05;

Which computes something like that in clear CSS:

.main-nav li:nth-child(1) a {
	transition: all .275s .175s;

Here, the last value of the transition’s property represents the delay: the time to wait before animation occurs will be 175ms for the first item, 225ms for the second, 275ms for the third, etc.

As you can see, the delay between each item is really small. It’s on purpose: we want a smart and light animation to avoid motion sickness. I let you check all of that in detail on the CodePen below.

Animated Mobile Menu

Main navigation: a smooth and sticky story

Now let’s have a look at the big screen navigation. I won’t go deep in the process of creating it but more in where I “failed”.

What I worked on, and reworked along the way

The sticky effect

I was planning to play with scroll-behavior property and sticky position.

I would love to use them because I’m kind of lazy when I need to build smoothscroll and sticky scripts in JS. I am happy if the browser can handle those things. For me, this is styling so it should belong to CSS.

I started by testing the sticky position (I already wrote about it in 2015). After code-designing the first version of the sticky menu, Stéphanie and I wanted a menu that appears only when the user start scrolling back up on the page. Here is the code I use to do so:

.main-header {
	padding: 16px 15px;
	border-top: 2px solid $primary;
	background: #FFF;
	transition: box-shadow .275s;
	z-index: 50;

.he-can-fly .main-header {
	position: sticky;
	top: 0;
	box-shadow: 0 12px 12px -12px rgba(0, 0, 0, .1);
	animation: slideDown .475s 1 forwards;

@keyframes slideDown {
	0% {
		transform: translateY(-100%);
	100% {
		transform: translateY(0);

I combined position sticky with an animation only when I decide to apply the .he-can-fly class. When that class is applied, the header becomes sticky, a smooth shadow is applied and the slideDown animation is triggered. I also keep the last state (100%) inside the @keyframers declaration (forwards value).

Now I need to define when I will apply this class. As I told you I decided to apply it only when user scrolls up. Here is my JS script:

var body = document.querySelector( 'body' ),
	menuIsStuck = function( wScrollTop, lastScroll ) {
		var classFound = body.classList.contains( 'he-can-fly' ),
			navHeight  = header.offsetHeight,
			treshold   = 400, // scroll down limit
			scrollVal  = classFound ? treshold : treshold + navHeight;

		if ( wScrollTop > scrollVal && ! classFound && wScrollTop < lastScroll ) {
			body.classList.add( 'he-can-fly' );

		// if we are to high in the page AND he-can-fly class exists
		if ( ( classFound && wScrollTop > lastScroll ) || ( classFound && wScrollTop === 0 ) ) {
			body.classList.remove( 'he-can-fly' );

This little menuIsStuck() function checks if the last scroll position is higher than the previous. If it is I add the .he-can-fly class on the body.
It’s a lot of variables just to check the scrolling direction, I agree, but my variables make the whole thing more readable.

Then, I trigger this function on scroll event.

var tick  = false,
	lastY = 0;

window.addEventListener( 'scroll', function( e ) {
	if ( ! tick ) {
		window.requestAnimationFrame(function() {
			tick = false;

			wScrollTop = window.scrollY || window.pageYOffset || body.scrollTop;

			// use scroll datas as parameters…
			menuIsStuck( wScrollTop, lastY );

			// save current scroll as last scroll position
			lastY = wScrollTop;

			// [spycroll part]

	tick = true;
} );

The function is called only when the JS engine is not already busy with working on the execution. It’s not really useful in my case, but it can become a real performance saver when you introduce several functions working at the same time on scroll or resize events. Go deeper with requestAnimationFrame() thanks to “Leaner, Meaner, Faster Animations with requestAnimationFrame”.

Instead of using this little trick, you could give a try to Lodash.js and its _.throttle() function. And if you want to go further have a look at this blog post about debouncing javascript event.

I still needed JavaScript to do the job, but if you plan on having a persistant sticky header, or on coding a little prototype, this code I was planning to use at the beginning will save you time.

.main-header {
	position: sticky;
	top: 0;

The spy-scrolling

The idea was ideal here: sticky menu and in-page anchors, let’s spy the position in the page to mark the current section!

But when we decided to get the sticky header only on scroll up, I was wondering if the spy-scroll feature was still a good idea. We finally keep the feature for people who scrolls up.

I’m pretty sure the code might be modern as f*ck if I would use IntersectionObserver (I like this little post with a mobile demo). I’m not really confident yet with that, so I decided to go with measuring the scroll position and the top offset of each section of the page.

var sects = {},

navLinks.forEach( function( el ) {
	var id = el.href.split('#');
	sects[ id[1] ] = document.getElementById( id[1] ).offsetTop;

So here I save an object in a variable containing the top offset of all my sections.

Then I added the next portion of JS inside my previous “onscroll” listener. See the previous part of code marked line 17. ([spyscroll part])

for (i in sects) {
	if ( sects[i] <= wScrollTop + 140 ) {
		document.querySelector( '.is-current' ).classList.remove( 'is-current' );
		document.querySelector( 'a[href="#' + i + '"]' ).classList.add( 'is-current' );

The value of 140 is a magical number. Kind of a threshold to make the whole thing smoother… at least to me 😀

What I worked on, but finally didn’t keep

In an ideal world, I wish I could use a sticky position and a scroll-behavior set to smooth to deal with all the stuff I spoke before.

The fact is that scroll-behavior is not really well supported by the browsers. When I start thinking about that, even Chrome didn’t support scrollIntoView(). Yeah the support of behavior-scroll in CSS and the support of scrollIntoView() in JS is the same. You can actually do something like that in JS:

let target = document.getElementById('my-target');
target.scrollIntoView({ behavior: 'smooth', block: 'center' });

Or do the same with CSS by targeting body element if you want to apply the smooth-scrolling to the entire page.

body {
	scroll-behavior: smooth;

I finally went with a solution that allows me to use scroll-behavior in JS with a polyfill by Dustan Kasten. The polyfill gives me access to the scroll() function I used like that:

var links = document.querySelectorAll( 'a[href^="#"]' );

links.forEach( function( el ) {
	el.addEventListener( 'click', function( e ) {
		var header  = document.querySelector( '.main-header' ),
			target  = document.querySelector( e.target.getAttribute('href') ),
			elTop   = target.getBoundingClientRect().top,
			bodyTop = document.body.getBoundingClientRect().top,
			offset  = elTop - bodyTop,
			variant = 35;

			top: offset - header.offsetHeight - variant, left: 0, behavior: 'smooth'
	} );
} );

For each link targeting an internal anchor, I get the href attribute which is also the ID of the aimed section in the page. I catch the offset top of the target and ask to scroll to the top of that target. I kept a lot  of variables in my code to make the whole thing more understandable. I hope it is 😀

Animations on project component

Going back to an animation story. Did I already mention that I like animate things?
When you go to the home page of Stéphanie’s portfolio, you’ve got a list of projects with an illustration next to the description.

Animation details

I won’t go deep in the code, not this time. You just need to know that the animation is sliced in several items. The image, a purple frame, a yellow background made by 3 pieces.

I used a lot of pseudo-elements to build this. Once all this visual part is set, I make the text and the little arrow appears with a bit of delay.

I don’t want to bother you with code detail, I’ll let you check the source for that. Ask me question in the comment area if you need help 🙂

I would like to focus on the little arrow that appears in that animation.

Reusable SVG

As soon as I need to duplicate SVG, I like the idea of going with the reusable SVG method. This method is quite simple: define the SVG to reuse, pick the ones you need here and there in your document.

Let’s see the code together. I define the beautiful diamond arrow made by Stéphanie as reusable SVG like that:

<svg display="none" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
		<g id="squared-arrow">
			<path d="M28.6531762,47.8130222 L49.3988086,27.0673898 C50.1798572,26.2863412 50.1798572,25.0200113 49.3988086,24.2389627 L28.1598459,3 L29.7456324,1.41421356 C30.526681,0.633164979 31.7930109,0.633164979 32.5740595,1.41421356 L55.3988086,24.2389627 C56.1798572,25.0200113 56.1798572,26.2863412 55.3988086,27.0673898 L33.0673898,49.3988086 C32.2863412,50.1798572 31.0200113,50.1798572 30.2389627,49.3988086 L28.6531762,47.8130222 Z" class="shadow" fill="#FEDC2A"></path>

			<path d="M9.26276261,8.61159982 C8.71047786,8.61159982 8.26276261,9.05931507 8.26276261,9.61159982 L8.26276261,41.1929952 C8.26276261,41.7452799 8.71047786,42.1929952 9.26276261,42.1929952 L41.5418324,42.1929952 C42.0941171,42.1929952 42.5418324,41.7452799 42.5418324,41.1929952 L42.5418324,9.61159982 C42.5418324,9.05931507 42.0941171,8.61159982 41.5418324,8.61159982 L9.26276261,8.61159982 Z" id="background" fill="#FFFFFF" transform="translate(25.402297, 25.402297) rotate(45.000000) translate(-25.402297, -25.402297) "></path>

			<path d="M25.8669527,2.12132034 C25.4764284,1.73079605 24.8432634,1.73079605 24.4527392,2.12132034 L2.12132034,24.4527392 C1.73079605,24.8432634 1.73079605,25.4764284 2.12132034,25.8669527 L24.9460695,48.6917018 C25.3365938,49.0822261 25.9697587,49.0822261 26.360283,48.6917018 L48.6917018,26.360283 C49.0822261,25.9697587 49.0822261,25.3365938 48.6917018,24.9460695 L25.8669527,2.12132034 Z M27.2811663,0.707106781 L50.1059154,23.5318559 C51.2774883,24.7034288 51.2774883,26.6029237 50.1059154,27.7744966 L27.7744966,50.1059154 C26.6029237,51.2774883 24.7034288,51.2774883 23.5318559,50.1059154 L0.707106781,27.2811663 C-0.464466094,26.1095934 -0.464466094,24.2100985 0.707106781,23.0385256 L23.0385256,0.707106781 C24.2100985,-0.464466094 26.1095934,-0.464466094 27.2811663,0.707106781 Z" fill="#644566" fill-rule="nonzero" class="form"></path>

			<path d="M22.6077787,17.8223221 L29.3932441,25.3643449 C29.5510892,25.5408286 29.5510892,25.8077534 29.3932441,25.9842372 L22.6108781,33.5262599 C22.4412353,33.7155844 22.1506561,33.7326509 21.9600189,33.5644866 L21.5054505,33.1667224 C21.4128859,33.0848785 21.3567968,32.9694861 21.3496209,32.8461335 C21.3424451,32.7227809 21.3847755,32.6016626 21.4672255,32.5096366 C23.5148492,30.2315324 25.5345789,27.9792572 27.6007985,25.674291 L21.4692917,18.8379123 C21.3867157,18.746019 21.3442063,18.6249805 21.351188,18.5016314 C21.3581698,18.3782822 21.4140657,18.2628141 21.5064837,18.1808265 L21.9569195,17.7830623 C22.1482231,17.6163032 22.4379053,17.6337769 22.6077787,17.8223221 Z" fill="#644566" class="arrow"></path>

Then, to use this defined SVG later in the document, you need to target the ID of the group (<g>), like that.

<svg width="57" height="51" viewBox="0 0 57 51">
	<use xlink:href="#squared-arrow"></use>

One of the numerous advantages of that method is that the SVG code to use a previously declared path is really short. The other one main advantage is that your original SVG code is not duplicated: easier to maintain, less noise in your code when you reuse a SVG.

When you take a look at the Chrome Dev tool, you will see that a shadow-root is created to help you see what SVG is added to the DOM when using a <use> element.

Good to know: the namespace xlink to use href is deprecated but yet supported.
So instead of the previous code, we should write:

<svg width="57" height="51" viewBox="0 0 57 51">
	<use href="#squared-arrow"></use>

The fact is the namespace xlink will still be supported for a long time, like the syntax of pseudo-elements with “:” which became “::” (like ::before), but the old way is still supported.

currentColor trick

Take a look at the projects list: one in two is yellow, the alternate color is purple. To do so with my reused xlink SVG method becomes kind of tricky. You can’t really directly manipulate the fill value of your SVG based on the parent container 😀

The currentColor way works

I wanted to do this with CSS Custom Property to take advantage of the CSS heritage of the property though document. I  supposed that, if I can do it through document, I should be able to do it with reused SVG. Then I remembered the “CSS Variable”: currentColor. I was not really sure it’ll work, but it finally did the job perfectly.

Declaring the fill like that:

<path d="M28.6531762,47.8130222 L49.3988086,27.0673898 C50.1798572,26.2863412 50.1798572,25.0200113 49.3988086,24.2389627 L28.1598459,3 L29.7456324,1.41421356 C30.526681,0.633164979 31.7930109,0.633164979 32.5740595,1.41421356 L55.3988086,24.2389627 C56.1798572,25.0200113 56.1798572,26.2863412 55.3988086,27.0673898 L33.0673898,49.3988086 C32.2863412,50.1798572 31.0200113,50.1798572 30.2389627,49.3988086 L28.6531762,47.8130222 Z" 

And the CSS looks like that:

.work-illu .icon {
	color: #FEDC2A;

.work-item:nth-child(2n) .icon {
	color: #C3A3C9;

The CSS Custom Properties doesn’t work

My first idea was wrong on using CSS Custom Properties doesn’t work, you can not do the same thing with “CSS Variables” doing like this.

<path d="M28.6531762,47.8130222 L49.3988086,27.0673898 C50.1798572,26.2863412 50.1798572,25.0200113 49.3988086,24.2389627 L28.1598459,3 L29.7456324,1.41421356 C30.526681,0.633164979 31.7930109,0.633164979 32.5740595,1.41421356 L55.3988086,24.2389627 C56.1798572,25.0200113 56.1798572,26.2863412 55.3988086,27.0673898 L33.0673898,49.3988086 C32.2863412,50.1798572 31.0200113,50.1798572 30.2389627,49.3988086 L28.6531762,47.8130222 Z" 

And for CSS part:

.work-illu .icon {
	--pathcolor: #FEDC2A;

.work-item:nth-child(2n) .icon {
	--pathcolor: #C3A3C9;

It doesn’t work even if I put styles in a style block declaration inside the SVG declaration. So go for the currentColor trick.

What about the part 2?

Yeah you’re guessing it well, I’ve got a part 1 in the title, the part 2 will be about accessibility, color profile (some fun facts on Chrome, Firefox and Sketch and how we dealt with) and how we collaborated with Stéphanie on this rework.

I cant’ give you a precise date for this second part, so let’s keep in touch via Twitter, I’ll give you the update.

And remember, comments are very welcome!

Are you looking for a UX or UI designer, for a site or mobile application? Do you want me to give a talk at your conference, or simply want to know more about me? You can take a look at my portfolio and contact me.

3 thoughts on “Portfolio Rework Part 1: Animation, Micro interaction & SVG

    • Wow, thank you Dannie.
      That’s awesome. So my error was trying to use style attribute instead of fill attribute to use Custom Properties.
      Good to know. I’m pretty sure I tried it last year. Do you know since when Chrome supports that? (just to be sure if I’m crazy or not xD)

      Again thanks. I’ll update the blog post to complete with your examples 🙂

  1. Salut,

    Je suis autodidacte dans le domaine et lorsque j’ai réalisé mon premier site web (design et développement), j’ai travaillé sur ce système de menu en slide (disponible même en version Desktop). J’avais bien galéré pour venir au bout de ce que je voulais.. je pense que c’est loin d’être parfait, qu’il y a certainement beaucoup de choses à améliorer mais voilà.. c’était le début.

    Je continue mon apprentissage et j’y reviendrai certainement un de ces jours pour proposer une version 2.0 avec une amélioration du code et de l’UX.

Leave a Reply

Your e-mail address will not be published. Required fields are marked *