A Minimal Web Developer Portfolio | Free Template

Update: I've built a web app that lets you build an awesome developer portfolio in minutes using your GitHub profile. Check it out and let me know what you think: mydevportfol.io

In this post I'll discuss (and give away the code for) my latest portfolio template. This design is based on a portfolio I discovered by Jeremy Thomas, the creator of the Bulma CSS framework (among other things). Jeremy has since updated his site, but at the time he had a really clean, minimal portfolio site that piqued my interest in terms of design as well as the underlying code that made it work.

Check out the demo site here


Jeremy's design was extremely minimal. It was structured in such a way that the projects he showcased inspired curiousity. They had very little copy to describe what they were, encouraging readers to click through to the different projects to check them out.

The layout was simple, with a project taking up the entire height of the screen, and when the user scrolled, Javascript would run to swap out the projects, change the background colours and animate different elements in and out of view. It was quite elegant.

For my take on the design, I wanted to provide just a little more information for readers, so that you could understand at a glance the projects, skills and technologies of the exhibitor. It's easy to get carried away with the whole "minimal" vibe, but you run the risk of not communicating well if you omit too much. It's a fine line.

The Technologies (or lack thereof)

In an effort to maintain the minimal feel, I opted to use no CSS framework, and no jQuery. The only external resources I'm using is some Google fonts, and the fantastic Konpa Devicon library for some of the technology logos.

For layout I'm using flexbox so that centering is easy, as there's a lot of centering going on here. Even still, the CSS is less than 200 lines!

Achieving the scroll effect

I'm about to get technical here, so feel free to skip this part if you like...

When I first found Jeremy's website, I was mystified as to how he achieved the effect where scrolling would cycle different page elements in and out. I thought he must be using a library, but after inspecting the source code, it became clear that he had coded the animations himself.

I couldn't make sense of how he had laid the elements out, and the logic he was using to switch the elements. Of course, curiousity got the better of me and I wanted to build it myself. It turned out to be relatively simple...

Laying out the HTML

In the HTML, each section element holds everything that would go in one "slide" lets say. My CSS sets these as position:fixed so that they take up the entire width and height of the viewport.

Since each section is going to have its own background and font colour, I decided it was easiest to set these as data attributes on the sections themselves, eg: data-bgcolor="#25a877" data-textcolor="white".

This gave me a quick and easy way to update individual sections as I built them, and since I'd be running Javascript to swap out the elements anyway, it was trivial to pull this info from the attributes.

Some Javascript Magic

All screens are different, so the first thing I wanted to get is the screen height and set it to a variable; screenHeight = screen.height.

Once I had that, I wanted to get the number of sections that exist on the page so that I could multiply those two figures, and set that as the overall page size via the body element.

I needed to do this because I had previously set the page sections to be position:fixed, meaning the page wouldn't naturally have the height we'd need in order to scroll.

This meant that if I had 6 sections, and a screen height of 1000px, the body element was being made 6000px high, giving us plenty of page height to scroll into.

// When the page is loaded
document.addEventListener("DOMContentLoaded", function(event) {

  // Get height of screen
  screenHeight = screen.height

  // Get all page-sections on page and populate arrays
  pageSectionArr = document.getElementsByClassName('page-section');

  // set total page height depending on number of projects
  pageHeight = screenHeight * pageSectionArr.length;

  document.getElementById('body').style.height = pageHeight+"px";

  // Check the scroll position, show appropriate project


Once I had set the height of the page as appropriate, I wanted to run some logic to check where on the page the scroll position was, as browsers nowadays can remember scroll position.

I could have just always scrolled to the top of the page and applied the first element, but that's not very nice...

function checkScroll(){

	last_known_scroll_position = window.scrollY;

	if (!ticking) {

	  window.requestAnimationFrame(function() {
	    ticking = false;
	  ticking = true;


I'll be honest, for this part I pulled the code from this Mozilla reference, and this html5rocks reference. I'm not entirely sure why it's checking the ticking variable, but my understanding is that it's so that we don't unnecessarily poll for the scroll position too much and cause a performance issue.

checkScroll() gets the current scroll position, and passes it to another function. It doesn't just fire on page load, I'm also calling it on every scroll event, so that when the user scrolls, we constantly check to see if we need to swap out the element for another one.

The logic to check for this resides within the checkElement() function:

function checkElement(scroll_pos) {

  // how many times does pageHeight go into scrollPos
  let x = Math.floor(scroll_pos / screenHeight);

  // set appropriate element if not already
  if(currentEl !== x){

Here I'm passing the scroll position scroll_pos into the checkElement() function, and we're dividing it by the screenHeight, and passing that result to Math.floor(). This tells us what section should be showing at that particular scroll position.

So, lets say our screen height is 1000px high. If the user has scrolled past the first section but not past the second section, they'll have a scroll position somewhere between 1000 and 2000, let's say 1200.

If we divide 1200 by 1000, we get 1.2. When we pass that to Math.floor() it rounds it down to 1, and since we're using zero-indexed arrays in JS, this means the second element should be showing.

The Final Piece

Finally, once I've figured out I need to swap out the element showing on the page, I run the applyElement() function:

function applyElement(x){

	// Set currentEl
	currentEl = x;

	// for each project and project-img
	// hide all
	for (var i = pageSectionArr.length - 1; i >= 0; i--) {
		// projectImgArr[i].style.display = "none"

	// Set background color on body
	// get data-bgcolor attribute from .project-text element, white as fallback
	document.getElementById('body').style.background = pageSectionArr[x].dataset.bgcolor || "white";

	// Set text color on body, get it from data attribute, black as fallback
	document.getElementById('body').style.color = pageSectionArr[x].dataset.textcolor || "black";

	// Show appropriate project


Firstly, I'm setting the currentEl variable so that checkScroll() can use it.

Then, I'm iterating through all of the sections on the page, and hiding them all.

Next, I grab the background colour from the data attribute of the selected element, and setting a fallback just in case it's not set. I'm doing the same for the text colour too.

Finally, I'm removing the hidden class from the element, and applying the shown class, thereby bringing the section to life on the page.

In Conclusion

I'm actually quite happy with this portfolio. It's clean and elegant, and with its minimal approach it's likely going to be very quick to load. One thing I would like to check is how it behaves when Javascript is disabled. For whatever reason that could happen, and I'd like it to fail gracefully so that the content is at least legible.

What do you think could be improved? Let me know over on Twitter @cderm

You've successfully subscribed to Chris Dermody
Great! Next, complete checkout to get full access to all premium content.
Error! Could not sign up. invalid link.
Welcome back! You've successfully signed in.
Error! Could not sign in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.