CSS-Only Responsive Layout with Smooth Transitions

A tutorial on how to create a 100% width and height smooth scrolling layout with CSS only. Using a radio button navigation and sibling combinators we will trigger transitions to the respective content panels, creating a “smooth scrolling” effect.

CSS-Only Responsive Layout with Smooth Transitions

In this tutorial we will create a responsive 100% width/height layout with some smooth page transitions. The idea is to have some content panels and a navigation which will allow us to navigate between the panels. We’ll use radio buttons for the navigation and animate the content to the right position with a transition, creating a “smooth scrolling” effect. This layout idea could be useful for web pages or web apps where the content should be strictly the size of the screen (width and height). Note that this is, of course, highly experimental and just a proof-of-concept.

Please note: the result of this tutorial will only work as intended in browsers that support the respective CSS properties.

Note that we will exclude vendor prefixes in this tutorial. You will, of course, find them in the files.

The Markup

The structure will be consist of a main container with the class st-container which will contain the radio buttons and link, and the wrapper with the class st-scroll for the panels. Each panel will have some content elements:

<div class="st-container">
			
	<input type="radio" name="radio-set" checked="checked" id="st-control-1"/>
	<a href="#st-panel-1">Serendipity</a>
	
	<input type="radio" name="radio-set" id="st-control-2"/>
	<a href="#st-panel-2">Happiness</a>
	
	<input type="radio" name="radio-set" id="st-control-3"/>
	<a href="#st-panel-3">Tranquillity</a>
	
	<input type="radio" name="radio-set" id="st-control-4"/>
	<a href="#st-panel-4">Positivity</a>
	
	<input type="radio" name="radio-set" id="st-control-5"/>
	<a href="#st-panel-5">Passion</a>
	
	<div class="st-scroll">

		<section class="st-panel" id="st-panel-1">
			<div class="st-deco" data-icon="H"></div>
			<h2>Serendipity</h2>
			<p>Banksy adipisicing eiusmod banh mi sed...</p>
		</section>
		
		<section class="st-panel st-color" id="st-panel-2">
			<!-- ... -->
		</section>
		
		<!-- ... st-panel-3, st-panel-4, st-panel-5 -->

	</div><!-- // st-scroll -->
	
</div><!-- // st-container -->

What we want to do is basically move the panel wrapper by changing it’s top value and bringing the respective panel into the viewport. This we can do by selecting the sibling of a checked radio button, the st-scroll division, with the sibling combinator and target the correct panel inside. Because of this technique, we need to keep the radio buttons in the same level like the st-scroll and on top of the links (they will be invisible though, since we’ll give them 0 opacity). For being able to select the correct panel, we give IDs to them and to the radio buttons.

The reason why we use links and not, like usually, labels, is that we want to be able to create some kind of “fallback” for non-supportive browsers (sibling combinators don’t work in older browsers). The links have the href value of the panels’ IDs, so for the fallback, we’ll simply hide the radio buttons, making the links clickable which will make it possible to “jump” to the right panel.

I know that it might be desirable to have the respective hashtag in the URL once we click on an input but with this technique that we’ll use for this layout it is not (yet) possible with CSS-only (parent selectors would be really nice to have!).

OK, let’s style this thing!

Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.

The CSS

Now, how do we make this layout flexible and its panels exactly the size of the screen? The trick is to make the main container absolute with a width and height of 100% while setting the panels and their wrapper to position relative. But they will also have a width and height of 100%. This will make each panel be exactly the size of the screen (since the main container and the panel wrapper are) but allow an overflow of the content, stacking the panels in the classic way.

Since we’ll do the content navigation by animating the panel wrapper, we’ll set the body overflow to hidden:

body {
	overflow: hidden;
}

Let’s take a look at the main container’s style:

.st-container {
	position: absolute;
	width: 100%;
	height: 100%;
	top: 0;
	left: 0;
	font-family: 'Josefin Slab', 'Myriad Pro', Arial, sans-serif;
}

We’ll put the “navigation” at the bottom of the page by giving it a fixed position. Note that we are setting the same width and height for both, the input and the link. The idea is to overlay the radio button on the link elements so that they are clickable, but giving them 0 opacity so that they are not visible. Ans because of that it’s also important that we set the z-index of the radio buttons higher than the one of the link elements:

.st-container > input,
.st-container > a {
	position: fixed;
	bottom: 0px;
	width: 20%;
	cursor: pointer;
	font-size: 16px;
	height: 34px;
	line-height: 34px;
}

.st-container > input {
	opacity: 0;
	z-index: 1000;
}

.st-container > a {
	z-index: 10;
	font-weight: 700;
	background: #e23a6e;
	color: #fff;
	text-align: center;
	text-shadow: 1px 1px 1px rgba(151,24,64,0.2);
}

Since we are using percentages to spread the links and inputs across the width of the screen, we might get into some rounding trouble that will make some gaps appear. In order to hide that, well use a pseudo element that will be under the links and inputs. It will have the same background color like the link elements:

.st-container:before {
	content: '';
	position: fixed;
	width: 100%;
	height: 34px;
	background: #e23a6e;
	z-index: 9;
	bottom: 0;
}

Our links and inputs are still not positioned, so let’s give them their respective left values:

#st-control-1, #st-control-1 + a {
	left: 0;
}

#st-control-2, #st-control-2 + a {
	left: 20%;
}

#st-control-3, #st-control-3 + a {
	left: 40%;
}

#st-control-4, #st-control-4 + a {
	left: 60%;
}

#st-control-5, #st-control-5 + a {
	left: 80%;
}

As you can see, we are using the adjacent sibling selector to “reach” the direct sibling of an input which is the related link element.

Using the same principle, we will define a “selected” state for the link elements. Once we click on an input, we will give the sibling link element a different background color:


.st-container > input:checked + a,
.st-container > input:checked:hover + a{
	background: #821134;
}

Let’s also add a little triangle using the pseudo-class :after and give it the same color:

.st-container > input:checked + a:after,
.st-container > input:checked:hover + a:after{
	bottom: 100%;
	border: solid transparent;
	content: '';
	height: 0;
	width: 0;
	position: absolute;
	pointer-events: none;
	border-bottom-color: #821134;
	border-width: 20px;
	left: 50%;
	margin-left: -20px;
}

You can check out CSS Arrow, Please! if you want a quick way to create those arrows.

Let’s also define a hover state for the link element:

.st-container > input:hover + a{
	background: #AD244F;
}

.st-container > input:hover + a:after {
	border-bottom-color: #AD244F;
}

The wrapper for the panels and the panels will have relative position and we’ll give them a width and height of 100%. The panel wrapper will also get a top and left position of 0 while we don’t touch the the values for the panels (it will be auto).

The transition will be for animating the transform property value to the respective position:

.st-scroll,
.st-panel {
	position: relative;
	width: 100%;
	height: 100%;
}

.st-scroll {
	top: 0;
	left: 0;
	transition: all 0.6s ease-in-out;
	
	/* Let's enforce some hardware acceleration */
	-webkit-transform: translate3d(0, 0, 0);
	-webkit-backface-visibility: hidden;
}

.st-panel{
	background: #fff;
	overflow: hidden;
} 

Although I usually don’t add any vendor prefixed properties, I did wanted to leave these Webkit ones since they will help tremendously in creating a smooth experience.

Let’s define the positions for the st-scroll wrapper for each checked radio button. Since we know that every panel has a height of 100% we know the exact positions. We will use the transform property to translate the panel wrapper in the Y-dimension (up and down):

#st-control-1:checked ~ .st-scroll {
	transform: translateY(0%);
}
#st-control-2:checked ~ .st-scroll {
	transform: translateY(-100%);
}
#st-control-3:checked ~ .st-scroll {
	transform: translateY(-200%);
}
#st-control-4:checked ~ .st-scroll {
	transform: translateY(-300%);
}
#st-control-5:checked ~ .st-scroll {
	transform: translateY(-400%);
}

Now, let’s style the content elements. For the upper triangle with the icon we’ll simply rotate and translate the st-deco division. We’ll position it in the center top of the screen by setting the top to 0 and the left to 50% while giving it a left margin of minus half of it’s width. Translating it -50% will make only half of the box appear thus creating a triangle:

.st-deco{
	width: 200px;
	height: 200px;
	position: absolute;
	top: 0px;
	left: 50%;
	margin-left: -100px;
	background: #fa96b5;
	transform: translateY(-50%) rotate(45deg);
}

For the icon we’ll use the Raphaël Icon-Set via @font-face and the data-attribute/pseudo-class technique. The content of the pseudo-element :after will be the data-icon value that we’ve set in the HTML for that element. Note, that we need to rotate it back into the opposite direction of the parent element in order to have it back to “normal”:

[data-icon]:after {
    content: attr(data-icon);
    font-family: 'RaphaelIcons';
    color: #fff;
	text-shadow: 1px 1px 1px rgba(151,24,64,0.2);
	position: absolute;
	width: 200px;
	height: 200px;
	line-height: 200px;
	text-align: center;
	font-size: 90px;
	top: 50%;
	left: 50%;
	margin: -100px 0 0 -100px;
	transform: rotate(-45deg) translateY(25%);
}

The heading will be placed in the center of the screen with a negative top margin in order to “pull” it up a bit:

.st-panel h2 {
	color: #e23a6e;
	text-shadow: 1px 1px 1px rgba(151,24,64,0.2);
	position: absolute;
	font-size: 54px;
	font-weight: 900;
	width: 80%;
	left: 10%;
	text-align: center;
	line-height: 50px;
	margin: -70px 0 0 0;
	padding: 0;
	top: 50%;
	-webkit-backface-visibility: hidden;
}

Every time we click on an input, we want the respective heading to run an animation. It will animate a bit from the top and fade in at the same time. In order to select the correct heading, we will use the general sibling combinator:

#st-control-1:checked ~ .st-scroll #st-panel-1 h2,
#st-control-2:checked ~ .st-scroll #st-panel-2 h2,
#st-control-3:checked ~ .st-scroll #st-panel-3 h2,
#st-control-4:checked ~ .st-scroll #st-panel-4 h2,
#st-control-5:checked ~ .st-scroll #st-panel-5 h2{
	animation: moveDown 0.6s ease-in-out 0.2s backwards;
}

@keyframes moveDown{
	0% { 
		transform: translateY(-40px); 
		opacity: 0;
	}
	100% { 
		transform: translateY(0px);  
		opacity: 1;
	}
}

The paragraph will have the following style:

.st-panel p {
	position: absolute;
	text-align: center;
	font-size: 16px;
	line-height: 22px;
	color: #8b8b8b;
	z-index: 2;
	padding: 0;
	width: 50%;
	left: 25%;
	top: 50%;
	margin: 10px 0 0 0;
	-webkit-backface-visibility: hidden;
}

While the heading of a panel will move down, the paragraph will move up:

#st-control-1:checked ~ .st-scroll #st-panel-1 p,
#st-control-2:checked ~ .st-scroll #st-panel-2 p,
#st-control-3:checked ~ .st-scroll #st-panel-3 p,
#st-control-4:checked ~ .st-scroll #st-panel-4 p,
#st-control-5:checked ~ .st-scroll #st-panel-5 p{
	animation: moveUp 0.6s ease-in-out 0.2s backwards;
}

@keyframes moveUp{
	0% { 
		transform: translateY(40px); 
		opacity: 0;
	}
	100% { 
		transform: translateY(0px);  
		opacity: 1;
	}
}

In order to make out layout a bit more fun, we’ll add a color class and “invert” the colors for those panels and its content elements:

/* Colored sections */

.st-color,
.st-deco{
	background: #fa96b5;
}
.st-color [data-icon]:after {
	color: #fa96b5;
}
.st-color .st-deco {
	background: #fff;
}
.st-color h2 {
	color: #fff;
	text-shadow: 1px 1px 1px rgba(0,0,0,0.1);
} 
.st-color p {
	color: rgba(255,255,255,0.8);
}

Last, but not least, we will add some media queries to control the position and font size of the elements for smaller screens:

@media screen and (max-width: 520px) {
	.st-panel h2 {
		font-size: 42px;
	}
	
	.st-panel p {
		width: 90%;
		left: 5%;
		margin-top: 0;
	}
	
	.st-container > a {
		font-size: 13px;
	}
}

@media screen and (max-width: 360px) {
	.st-container > a {
		font-size: 10px;
	}
	
	.st-deco{
		width: 120px;
		height: 120px;
		margin-left: -60px;
	}
	
	[data-icon]:after {
		font-size: 60px;
		transform: rotate(-45deg) translateY(15%);
	}
}

For older browsers that don’t support some of the selectors we want to fall back to the classic “target jump”. We can do that by altering some of the style (simple.css). In particular, we will set the overflow of the body to “auto” and hide the inputs, making the link elements clickable (as href they have the ID of the respective panels):

body {
	overflow: auto;
}
.st-container > input{
	display: none;
}

And that’s all! I hope you enjoyed this tutorial and find it inspiring!

Manoela Ilic

Manoela is the main tinkerer at Codrops. With a background in coding and passion for all things design, she creates web experiments and keeps frontend professionals informed about the latest trends.

Stay in the loop: Get your dose of frontend twice a week

Fresh news, inspo, code demos, and UI animations—zero fluff, all quality. Make your Mondays and Thursdays creative!