3D Restaurant Menu Concept

A responsive 3D menu concept for a restaurant website. The idea is to show the menu as a folded flyer and unfold it in order to show the menu items.

3D Restaurant Menu Concept

Applying CSS 3D transforms to components can bring some more realism to normally flat web elements. We’ve experimented with some simple concepts for restaurant websites and came up with a 3D folded menu (a real menu, not a “web” menu). The result is a restaurant website template where the menu will open by unfolding. Clicking the linked items will reveal a modal overlay which contains some more info.

Since this “flyer” structure requires a decent amount of space, and, although we will make this responsive, we’ll add a media query for smaller screens where everything will fall back to a simplified stacked structure. The same will be applied for browsers that don’t support 3D transforms.

The thumbnails and delicious recipes are by Michael Natkin from herbivoracious.com (all the ones that have a link).

Please note: this only works as intended in browsers that support the respective CSS properties.

Alright, so let’s get started!

The CSS will not contain any vendor prefixes, but you will find them in the files.

The Markup

Our structure will consist of a main container with the class “rm-container” and we’ll have a wrapper inside. The wrapper will contain the three panels. Initially, we only want to see the cover of the folded menu; that is the division with the class “rm-cover”. The last panel is the one with the class “rm-right”. The middle panel is the one we see in the middle when we open the menu:

<div id="rm-container" class="rm-container">

	<div class="rm-wrapper">
		<div class="rm-cover"></div>
		<div class="rm-middle"></div>
		<div class="rm-right"></div>
	</div><!-- /rm-wrapper -->

</div><!-- /rm-container -->

Inside of the rm-cover and the rm-right division, we will have a front- and a backface:

<div class="rm-front">

	<div class="rm-content">
		<!-- Some content -->
	</div><!-- /rm-content -->
	
</div><!-- /rm-front -->

<div class="rm-back">

	<div class="rm-content">
		<!-- Some content -->
	</div><!-- /rm-content -->

	<div class="rm-overlay"></div>

</div><!-- /rm-back -->

We’ll also add an element for an overlay gradient that will give the menu parts a bit more realism. Note that the front will be empty in the last element.

The middle part of the menu will simply have another wrapper inside, no front or back part:

<div class="rm-inner">

	<div class="rm-content">
		<!-- Some content -->
	</div><!-- /rm-content -->

	<div class="rm-overlay"></div>

</div><!-- /rm-inner -->

We will never see the back part of this part of the menu, so we won’t need that structure.

The content will consist of some text elements, like definition lists for the dishes and headlines:

<div class="rm-content">

	<h4>Appetizers</h4>

	<dl>
		<dt>Bella's Artichokes</dt>
		<dd>Roasted artichokes with chipotle aioli and cream cheese</dd>

		<dt><a href="http://herbivoracious.com/2011/11/crostini-with-young-pecorino-grilled-figs-and-arugula-mint-pesto-recipe.html" class="rm-viewdetails" data-thumb="images/1.jpg">Green Love Crostini</a></dt>
		<dd>Crostini with young pecorino, grilled figs and arugula & mint pesto</dd>
		
		<dt>Focaccia di Carciofi</dt>
		<dd>Artichoke focaccia with fresh thyme</dd>

		<!-- ... -->
	</dl>

	<h4>Salads & More</h4>
	
	<dl>
		<!-- ... -->
	</dl>

</div><!-- /rm-content -->

Notice the anchor with the class “rm-viewdetails” and the data attribute “data-thumb”. We will use this as the content for a modal overlay box that will appear when clicking on the link:

<div class="rm-modal">
	<div style="background-image: url(images/1.jpg)" class="rm-thumb"></div>
	<h5>Green Love Crostini</h5>
	<p>Crostini with young pecorino, grilled figs and arugula & mint pesto</p>
	<a href="http://herbivoracious.com/2011/11/crostini-with-young-pecorino-grilled-figs-and-arugula-mint-pesto-recipe.html">See the recipe</a>
	<span class="rm-close-modal">x</span>
</div>

It will contain the same headline and description that we get from the followed dd element (definition description), the thumbnail that we get from the data-attribute and the link.

And that’s the HTML, now, let’s take a look at the style.

Note that we won’t go into the styling of the text elements or inner content parts. Instead we will focus on the 3D structure, transition classes and media queries.

The CSS

We want to make the whole thing fluid, so we will give the main container a percentage width, and, since we want some 3D awesomeness, some perspective:

.rm-container {
	width: 33%;
	height: 700px;
	max-width: 370px;
	margin: 0 auto 40px auto;
	position: relative;
	perspective: 1600px;
	color: #2a323f;
}

The wrapper and its children divisions (the cover, middle part and right part) will all have the full width and height and we’ll position them absolutely:

.rm-wrapper,
.rm-wrapper > div {
	width: 100%;
	height: 100%;
	left: 0;
	top: 0;
	position: absolute;
	text-align: center;
	transform-style: preserve-3d;
}

Since we will be working in 3D perspective, we want the transform style of these elements to be “preserve-3d”.

The cover needs a higher z-index than all the other parts and we’ll set the transform origin to be on the left edge (left center). The transition delay is for when we close the menu. The transition to the open state will contain another value, but let’s look at that a bit later:

.rm-wrapper .rm-cover {
	z-index: 100;
	transform-origin: 0% 50%;
	transition-delay: 0.2s;
}

The middle part will have the lowest z-index of all three parts and we’ll add a subtle box shadow:

.rm-wrapper .rm-middle {
	z-index: 50;
	box-shadow: 0 4px 10px rgba(0,0,0,0.7);
}

The right most part will have a z-index that is higher than the middle part one but lower than the cover one. The transform origin will be on the right edge (right center) and there will be no transition delay when we close the menu:

.rm-wrapper .rm-right {
	z-index: 60;
	transform-origin: 100% 50%;
	transition-delay: 0s;
}

The inner divisions which are the ones with the classes “rm-front”, “rm-back” and “rm-inner” will have a paper texture as background and we’ll add a inset box shadow that will simulate a multi-line decorative border:

.rm-wrapper > div > div {
	background: #fff url(../images/white_paperboard.jpg);
	width: 100%;
	height: 100%;
	position: absolute;
	padding: 30px;
	box-shadow: 
		inset 0 0 0 16px #fff, 
		inset 0 0 0 17px #e6b741, 
		inset 0 0 0 18px #fff, 
		inset 0 0 0 19px #e6b741, 
		inset 0 0 0 20px #fff, 
		inset 0 0 0 21px #e6b741;
}

Now, let’s add the important 3D properties. The front and the back face need backface-visibility set to hidden:

.rm-container .rm-front,
.rm-container .rm-back {
	backface-visibility: hidden;
}

Let’s rotate the backfaces so that they are behind the front parts:

.rm-container .rm-back {
	transform: rotateY(-180deg);
}

The overlay will generally be an almost transparent gradient from light to a bit darker:

.rm-overlay {
	position: absolute;
	width: 100%;
	height: 100%;
	top: 0;
	left: 0;
	pointer-events: none;
	background: linear-gradient(to right, rgba(0,0,0,0) 0%, rgba(0,0,0,0.05) 100%);
}

For the middle overlay we will turn the gradient to the other side:

.rm-middle .rm-overlay {
	background: linear-gradient(to right, rgba(0,0,0,0) 64%, rgba(0,0,0,0.05) 100%);
}

Let’s add some padding the the content divisions:

.rm-content {
	padding: 20px;
}

The modal layer will be invisible: we’ll set the opacity to 0, the pointer-event to none and we’ll translate it on the Z-axis:

.rm-modal {
	position: absolute;
	z-index: 10000;
	width: 120%;
	margin-left: -10%;
	top: 50%;
	padding: 40px;
	background: #fff url(../images/white_paperboard.jpg);
	box-shadow: 
		inset 0 0 0 16px #fff, 
		inset 0 0 0 17px #e6b741, 
		inset 0 0 0 18px #fff, 
		inset 0 0 0 19px #e6b741, 
		inset 0 0 0 20px #fff, 
		inset 0 0 0 21px #e6b741,
		0 4px 20px rgba(0,0,0,0.4);
	opacity: 0;
	pointer-events: none;
	transform: translateZ(1000px);
}

The idea is to reveal the modal when we click on one of the links on the menu. We’ll scale the menu down and make the modal appear from “above”. This concept is inspired by Hakim El Hattab’s modal concept Avgrund.

Let’s add some transitions and define some classes for opening the menu.

First, we’ll give a transition to the wrapper (for scaling it when opening the modal) and to the children of the wrapper, i.e. our three menu parts (for animating the “opening”/rotation):

.rm-wrapper, .rm-wrapper > div { transition: all 0.6s ease-in-out; } The modal will also have a transition for the transformation (to scale down) and its opacity:
.rm-modal {
	transition: 
		transform 0.6s ease-in-out,
		opacity 0.6s ease-in-out;
}

The idea is to add a class called “rm-open” which will trigger the menu to unfold. This class will be added with JavaScript when we click on the “View the menu” link.

When adding the class, we will define what will happen to all the elements when we open the menu.

The children of the wrapper will all get a box shadow:

.rm-container.rm-open .rm-wrapper > div {
	box-shadow: 0 4px 5px -3px rgba(0,0,0,0.6);
}

The cover will rotate (without any delay) -180 degrees on the Y-axis. Since we defined the transform origin to be on the left center, it will open to the left:

.rm-container.rm-open .rm-cover {
	transform: rotateY(-180deg);
	transition-delay: 0s;
}

The right part will rotate 180 degrees, but here we will add a transition delay of 0.2s because we want the cover to open a bit first:

.rm-container.rm-open .rm-right {
	transform: rotateY(180deg);
	transition-delay: 0.2s;
}

When we click on one of the menu item that is an anchor, we will add another class called “rm-in” to the container and move the wrapper down on the Z-axis:

.rm-container.rm-in .rm-wrapper {
	transform: translateZ(-500px);
}

The cover and the right part will be rotated a bit more to the inside:

.rm-container.rm-in .rm-cover {
	transform: rotateY(-150deg);
}

.rm-container.rm-in .rm-right {
	transform: rotateY(150deg);
}

We need to set the transition delay to 0 seconds for this case:

.rm-container.rm-in .rm-cover, 
.rm-container.rm-in .rm-right,
.rm-container.rm-nodelay .rm-right {
	transition-delay: 0s;
}

The class “rm-nodelay” is an extra class that we’ll need for the right menu part when we close the modal. Remember, it had a transition delay for when we open the menu.

The modal will appear by translating it to 0px on the Z-axis and increasing the opacity to 1:

.rm-container.rm-in .rm-modal {
	transform: translateZ(0px);
	opacity: 1;
	pointer-events: auto;
}

Let’s define some media queries for smaller screens and devices. The first media query will simply resize the wrapper:

@media screen and (max-width: 1110px) {
	.rm-container {
		height: 800px;
	}
}

Since we made the wrapper fluid, it will need a larger height in order to contain the text.

From 960 pixels on, we no longer want to show the menu as a folded flyer but as normal, scrollable content.
For that, we’ll simply set the height of the wrapper to auto, give it 100% of width and define a maximum width:

@media screen and (max-width: 960px) {

	.rm-container {
		width: 100%;
		height: auto;
		max-width: 460px;
	}

All the inner divisions will no longer be absolute, but relative and we’ll give them 100% of width and set the height to auto:

	.rm-wrapper, 
	.rm-wrapper > div,
	.rm-wrapper > div > div {
		position: relative;
		width: 100%;
		height: auto;
	}

The content divisions will need some margin:

	.rm-wrapper > div > div{
		margin-bottom: 10px;
		box-shadow: 
			inset 0 0 0 16px #fff, 
			inset 0 0 0 17px #e6b741, 
			inset 0 0 0 18px #fff, 
			inset 0 0 0 19px #e6b741, 
			inset 0 0 0 20px #fff, 
			inset 0 0 0 21px #e6b741,
			0 3px 5px rgba(0,0,0,0.2);
	}

Let’s remove the rotations:

	.rm-container .rm-back,
	.rm-container.rm-open .rm-cover,
	.rm-container.rm-open .rm-right {
		transform: rotateY(0deg);
	}

We don’t need the overlays any more:

	.rm-overlay, .rm-middle .rm-overlay {
		display: none;
	}

And we set the position of the modal to fixed, so that it sticks on top when we scroll:

	.rm-container .rm-modal {
		position: fixed;
		width: 80%;
		top: 100px;
		left: 50%;
		margin: 0 0 0 -40%;
		transform: translateZ(0px);
		transition: opacity 0.6s ease-in-out 0s;
	}

When we click on a menu item link and the modal appears we won’t rotate anything any more:

	.rm-container.rm-in .rm-cover,
	.rm-container.rm-in .rm-right,
	.rm-container.rm-in .rm-wrapper {
		transform: rotateY(0deg);
		transition-delay: 0s;
	}
}

And that’s all the important style. For browsers that don’t support 3D transforms, we will use almost the same styling like we do for this last media query. Since we use Modernizr, we’ll just add those classes again, preceded by .no-csstransforms3d (which will be added to the body).

Now, let’s add some lines of JavaScript.

The JavaScript

We will start by caching some elements:

	// main container
var $container = $( '#rm-container' ),						
	// the cover, middle and right panels
	$cover = $container.find( 'div.rm-cover' ),
	$middle = $container.find( 'div.rm-middle' ),
	$right = $container.find( 'div.rm-right' ),
	// open and close elements
	$open = $cover.find('a.rm-button-open'),
	$close = $right.find('span.rm-close'),
	// the links for each recipe (photo and details)
	$details = $container.find( 'a.rm-viewdetails' ),

The events for opening/closing the menu and also for showing each item’s details (modal) are initialized:

init = function() {

		initEvents();

	},
initEvents = function() {

	$open.on( 'click', function( event ) {

		openMenu();
		return false;

	} );

	$close.on( 'click', function( event ) {

		closeMenu();
		return false;

	} );

	$details.on( 'click', function( event ) {

		$container.removeClass( 'rm-in' ).children( 'div.rm-modal' ).remove();
		viewDetails( $( this ) );
		return false;

	} );
	
},

To open/close the menu we will be adding/removing the class ‘rm-open’ from the $container. Remember, this is the class where the transitions are defined.

Note that on close, we are also removing the classes ‘rm-nodelay’ and ‘rm-in’. These are classes that are added if we click to see the menu item’s details.

openMenu = function() {

	$container.addClass( 'rm-open' );

},
closeMenu = function() {

	$container.removeClass( 'rm-open rm-nodelay rm-in' );

},

Finally, if we click to see an item’s details, a modal box will be shown with the image and the text for that item. We translate the main container on the Z-axis (and adjust the top margin in order to center it), and slightly rotate the left and right panels:

viewDetails = function( recipe ) {

	var title = recipe.text(),
		img = recipe.data( 'thumb' ),
		description = recipe.parent().next().text(),
		url = recipe.attr( 'href' );

	var $modal = $( '
' + title + '
' + description + ' See the recipex
' ); $modal.appendTo( $container ); var h = $modal.outerHeight( true ); $modal.css( 'margin-top', -h / 2 ); setTimeout( function() { $container.addClass( 'rm-in rm-nodelay' ); $modal.find( 'span.rm-close-modal' ).on( 'click', function() { $container.removeClass( 'rm-in' ); } ); }, 0 ); };

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

Tagged with:

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 up to date with the latest web design and development news and relevant updates from Codrops.