Creating a Single Page Application for a Portfolio using CSS

Sheldon Lloyd
10 min readFeb 22, 2018

I take you trough my process for creating a pure CSS single page application portfolio site template that uses the :target selector to navigate pages and flexbox for the layout.

The Case Study

I created a portfolio site not only for myself but for others as well so it needed to have decent backward compatibility for older browsers(IE 8 and up). This way the CSS will degrade gracefully in an older browser and if needed — it can be progressively enhanced with JavaScript.

The best way to do this while maintaining good usability was to have the home page also act as the navigation menu. It has four separate cards one for a resume, about page, recent projects, and blog posts.

The Html is organized so that the page starts with the content (about, resume and projects) and at the bottom are the Navigation cards.

This serves 2 purposes the first is that it allows people that don’t have target enabled browsers to get straight to the content. It also allows the main navigation to be easily hidden when someone navigates to one of the views using the target selector .view:taget~.main-nav.

To maintain a comfortable reading experience the copy is no more than 700 pixels wide. To keep consistency with the cards when viewing the contents of the cards while in desktop view the header is left-aligned while the copy is centered but perceived to be to the right when there is enough space.

Pick a card

Each card has links each of which displays sections within the single page application using the CSS target selector to display each section as a page. This is with the exception of the blog card. The blog card has a submenu of blog posts links which opens outside of the SPA portfolio in a new tab/window.

The project card also has a submenu but links to sections within the page. Like the project, the about and resume cards also use the target selector for displaying sections of the SPA.

The about and resume cards simply links to the #about and #resume sections of the page. The about card has a simple description for the developer, designer, artists, agency, etc of the portfolio.

The Project and About cards use images and gradient backgrounds. Instead of using a multilayered background I used a before and after pseudo element to be the backgrounds. Except when the project preview is shown the background uses a left to right fading gradient covering the image on the right, this will make it easier to read the words positioned on the right side of the cards.

.about:after {    content: “”;    background: url(;    background-position: -132px 0px;    background-size: cover;    background-repeat: no-repeat;    width: 100%;    height: 100%;    position: absolute;    z-index: 0;    left: 0;    top: 0;    bottom: 0;    right: 0;}.about:before {    content: “”;    background: #f7f7f7;    background: linear-gradient(to right,
rgba(247, 247, 247, 0.8) 0%,
rgba(247, 247, 247, 1) 65% );
width: 100%; height: 100%; position: absolute; z-index: 1; left: 0; top: 0; bottom: 0; right: 0;}.projects:after { content: “”; background: url( -122px -55px / cover; width: 100%; height: 100%; position: absolute; z-index: -2; left: 0; top: 0; bottom: 0; right: 0;}.projects:before { content: “”; background: #777; background: linear-gradient(to right,
rgba(119, 119, 119, 0.8) 0%,
rgba(119, 119, 119, 1) 65%);
background-size: cover; background-repeat: no-repeat; width: 100%; height: 100%; position: absolute; z-index: -1; left: 0; top: 0; bottom: 0; right: 0;}.projects .main-nav-list li:before { background: #777; content: “”; width: 0; height: 100%; position: absolute; top: 0; left: 0; z-index: -1; opacity: 0;}.projects .main-nav-list li:after { content: “”; width: 0; height: 100%; position: absolute; top: 0; bottom: 0; right: 0; left: 0; z-index: -1; opacity: 0; transition: opacity 0.5s;}


To help minimize code I used the Scalable and Modular Architecture for CSS (SMACSS), mainly implementing the methodology in which any time a declaration is used in multiple classes it is turned into it’s on class. I also create modules for parts of the design that is also used multiple times.

Since flexbox along with its column is used for the layout and various modules of the website, it is given its own class to reduce CSS.

.flexbox {    display: flex;}.col-1 {    flex: 1;    text-align: right;    justify-content: center;}.col-2 {    flex: 100%;}

To make it easier to make color changes there are CSS classes for primary, secondary, and tertiary colors. This is for both the text and the background.

.primary-color {    color: #222;}.primary-background {    background: #222;}.secondary-color {    color: #777;}.secondary-background {    background: #777;}.tertiary-background {    background: #f7f7f7;}.tertiary-color {    color: #f7f7f7;}

The header and footer use vertical text in supported browsers, at first this was going to be fixed but I decide it was a bad idea as the text won’t be visible nor scrollable if the screen is not tall enough. The header and footer is positioned to the left and right of the content respectively. The main navigation uses flexbox to align the navigation cards into a 4×4 grid.

.vertical-text {    word-wrap: break-word;    writing-mode: tb-rl;    writing-mode: vertical-lr;    display: block;    transform: rotate(-180deg);    white-space: nowrap;}

I realized I was using min-height: 100%, overflow:hidden, and margin:0 a lot so they got their own classes.

.height-100 {    min-height: 100%;}.overflow-hidden {    overflow: hidden;}.margin-0{    margin:0}


When each view is targeted the Main menu will shrink into a fixed circle at the top left corner of the page — that can be clicked to take the visitor back to the main menu. The four squares represents the 4x4 grid of the main menu.

To maintain a smooth transition between views I use keyframe animation since the display: none property can’t be transitioned.

@keyframes shown {    0% {        opacity: 0;    }    50% {        opacity: 0;    }    100% {        opacity: 1;    }}

This shown keyframe animation is used when transitioning between the menu and the views.

.main-nav-item {    margin: 5px;    flex: 100%;    min-height: 200px;    flex-direction: row;    flex-basis: calc(50% — 10px);    justify-content: center;    position: relative;    box-shadow: 0px 0px 2px 1px rgba(204, 204, 204, 0.8);    animation: shown 0.5s ease-out;}.view:target {    display: block;    margin: 0;    animation: shown 0.3s ease-out;}.view:target ~ .main-nav .main-nav-link a {    display: block;    color: #f1f1f1;    text-align: center;    text-decoration: none;    width: 50px;    line-height: 50px;    animation: shown 0.7s ease-out;}

I used transition with max-width min-height when the navigation menu shrinks and grows because using transition with just width or height does not work.

To make the transition a smooth as possible the border-radius is set to start at .15 seconds after the transition begins and finishes in .15 seconds instead of .3 seconds so that it does not lag behind the rest of the transition.

.view:target ~ .main-nav .main-nav-link {    background-color: #272727;    position: fixed;    color: #f1f1f1;    max-width: 50px;    min-height: 50px;    text-align: center;    align-content: center;    left: 75px;    top: 20px;    border-radius: 100%;    z-index: 2;    transition: max-width 0.3s, 
border-radius 0.15s 0.15s;

When the menu is expanded I do everything in reverse, so the max-width and min-height transition is delayed instead of the Border radius. I also use a transition on the background color to try to match the opacity fade of the shown keyframe animation. The z-index of the .main-nav-link element has a transition so that the menu button is not behind the menu as the menu transitions into view.

.main-nav-link {    position: absolute;    right: 105px;    top: 5px;    max-width: 100%;    min-height: 100%;    border-radius: 0;    left: 105px;    transition: max-width 0.15s 0.15s, 
0.15s 0.15s,
border-radius 0.3s,
background-color 0.5s 0.3s,
z-index 0s 0.75s;
z-index: -1;}

The Big Project

The project view and card had the most work put into it, which makes sense because projects are the focal point of any portfolio.

For the Project card Instead of using a multi-layered background I used before and after pseudo-elements to be the backgrounds, this was especially important as the project card has a background image that changes as each link is hovered over to preview each project.

To prevent the hover from affecting the header and active part of the list I used the not selector.

.project-list li:hover:not(:first-of-type):not(.is-active):after{    content: “+”;    position: absolute;    top: 0px;    left: -8px;    padding: 2px;    background: #fff;}

Since each project is connected to the project card it has its own navigation which is on the left-hand side of each view and it takes up 30% of the view container.

Since unlike the other views the project view has a subsection, I thought it would be cool to have the header of the project connected to the header of the project list with a plus sign. This plus sign is also linked to the plus sign of the active view link which also has a plus sign with a line(border) that extends to and through it.

Since each list item when it is active is given a plus sign indicating what page the user is on, aside from aesthetics this may also provide some potential UX benefits in terms of navigation orientation and can avoid potential confusion.

When a user hovers over a project link it is also given a plus sign. And since the project link is bolded when hovered I used a pseudo element as a placeholder to prevent the parent element from changing size.

.project-list li a:before {    display: block;    content: attr(title);    font-weight: bold;    height: 1px;    color: transparent;    overflow: hidden;    visibility: hidden;}

Responsive Design

The best practice in responsive design is to create breakpoints based on when the design breaks instead of designing for screens.

When the screen is smaller than 1020px space becomes limited so the project list navigation is moved to the top of the view container and the plus and lines will shift to the left as well as the text alignment.

Since the navigation is a list instead of a grid now the floating navigation button is changed from a grid icon to a hamburger/list icon to reflect that change. The viewport is also too small under 1020px for the navigation menu so it is changed from a gridview to a listview.

When the screen is under 700px I decrease the padding of the header view element the footer is not important as it just has copyright info so it is hidden. This provides a little more room and maintains good text copy length.

At 550px to maintain good text copy length, I reduce the size of the text to 15px. The text of the about card description as well as the portfolio and blog link list also looks weird at this width, so I switch the text to have left alignment.

Not having much room, the header is no longer vertical and I position the header above everything else and the word portfolio is hidden since much like the license it being visible is not highly important. I also align the text to the right this is so that the menu button will have room and it won’t look awkward with it having 50px of padding even when the menu button is not visible

Browser Compatibility

I want the website to degrade gracefully and still work in older browsers.

If the browser doesn’t support :target it will navigate to the section, which will not be hidden from view.

When flexbox and vertical text is not supported in a browser the header sits at the top and the footer at the bottom.

.vertical-text {    writing-mode: initial\9;    writing-mode: lr-bt\9;    transform: none\9;    text-align: left\9;    margin: auto\9;    max-width: 50%\9;}

For the .main-nav element the min-height:100% declaration does not work properly in internet explorer so I used min-height:100vh instead.

I used min-width: 1020px instead of max width to maintain backward compatibility with older browser since browsers that don’t support @media don’t support target either and this is what will primarily interfere with older browsers.

Since IE 9 does not support Flexbox but supports vertical text I don’t want to use vertical text as it significantly decreases the user experience.

What happens when there is no flexbox with vertical text—not very pretty

So I Hack around it using @media screen and (min-width: 0) and (min-resolution: 0.001dpcm) and applying \9 to the styles as this will prevent it from working in any browser except IE 9 this because adding decimal places to the thousandth for some odd reason only works in IE 9.

That’s it for the Single Page Application Portfolio you can see a work example of it on codepen



Sheldon Lloyd

Web developer, science enthusiast, and aspiring webomic creator. Follow my journey as I create my first comic and share tips and tricks I learn along the way.