Usually when you think HTML and CSS you don’t imagine desktop apps, but nowadays there are more and more examples of desktop apps being created using HTML and CSS. Visual Studio Code and Atom are just two examples (built using Electron).
In this blog post I want to share with you how you can create a web page that looks and behaves (in terms of resizing) like a desktop app.
Before we start, here’s a teaser of what the end result will be:
The height
The first challenge is to handle the window’s height in order to always have all of its contents visible without a vertical scroll bar.
The first thing we can try is to add a div
and set its height
to 100%
. Unfortunately this will not work. That’s because if you set an element’s height using a percentage, its actual height will be dictated by its parent element’s height.
The default value for an element’s height is auto
, which means its height will be the sum of the heights of its children. So, if you set a child element’s height to 100%, and the parent’s height is auto, you create a situation where both the child and parent elements needs each other’s heights to compute their own. What happens in this situation is that the percentage is ignored, and the child element’s height defaults to auto.
Setting the height of the body to 100% will not work either. This is because although the body is seemingly the first element on a web page (you can omit the html tag and as long you have the body tag, you have a valid html document), it turns out that it’s the html element that is the root.
If you set the html element’s height to 100% it will get the full height of the browser window. You can think of this as the browser window having a specific height that the html element will get if you set its height to 100%. You also have to set the body’s height to 100%. After that you have a container (the body) that takes up the full height of the window.
You need to be aware that the body usually has a margin (8px if you are using Chrome). This will cause a scroll bar to be displayed. This happens because the default value for an element’s box-sizing
property is content-box
. With box-sizing: content-box
the “space” an element will take is the sum of its height, padding, margin and border. Because of this, the space the body will take is 100% of the window’s height plus 8 pixels, and that’s why you get a scroll bar.
Set the body’s margin to 0 and you’re done with the height. Here are our first styles:
html, body{
height: 100%;
}
body {
margin: 0;
}
Flexbox to the rescue
The next step is to create two fixed areas, one on top for the menu and one at the bottom for the status bar. The height of the area in between should stretch and shrink while the top and bottom remain fixed.
The easiest way to achieve this behavior is to use Flexbox. The way you use Flexbox is by setting an element’s display property to flex
. When you do this, the element is then called a “flex container”.
Any element inside a flex container becomes a “flex item”, and the biggest feature of using a flex container is that you can have those items shrink and expand so that they take all of the container’s available space. Here’s an example where all the elements have equal width:
And here’s a screenshot of what happens if you shrink the window:
Here are the styles:
div.container {
height: 200px;
display: flex;
}
div.item{
flex: 1;
margin: 10px;
height: 100px;
background-color: blue;
}
And markup for this example:
<body>
<div class="container">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
</body>
Notice that this is different than using, for example, float: left
. The items’ width is changing dynamically to match the available width of the container.
The property that makes all items have equal width is flex: 1
. In this case each element’s width is 1/3
of the container’s width. That’s because there are three elements with flex=1 so each gets 1/(1+1+1)
of the container’s width. If the first element had flex: 2
and the other two had flex: 1
, the first element’s width would be 2/(2+1+1)
or 1/2
, while the others would have 1/(2+1+1)
or 1/4
of the container’s width. Effectively making the first element twice as wide as the other two.
Another thing that is important to be aware is that the flex property is a shorthand property. When we set flex: 1
we are actually setting flex-grow: 1
. The other two properties that the flex
property allows you to set are flex-shrink
(by how much the item shrinks in relation to the other flex items) and flex-basis
(which represents the initial size of the flex item). To really understand how to use these values I recommend Dive into Flexbox and A Complete Guide to Flexbox, although they are not necessary for what we are trying to achieve here.
Although all items in a flex container are flex items, not all of them need to grow and shrink. You can set a specific width
in an element and set its flex
property to none
. This way the element will have a fixed width while playing nicely with the other flex items. If in the example above the first element had a fixed width of 50px and flex set to none, it would look like this:
And this is how it would look like if you stretched the window:
Finally, the last important concept that we need to understand is flex-direction
. Right now, our flex items are being laid out from left to right. That is because the default value for flex-direction
is row
. For our web page that looks like a desktop app we need them to be laid out from top to bottom, and for that we need to set the flex-direction
of our flex container to column
. We can apply all of what we’ve discussed above when flex-direction: column
by just thinking about height as we did for width.
Here’s how the above example looks if you set a fixed width, don’t specify a height and set the flex-direction to column:
Here’s the updated CSS for the example:
html, body{
height: 100%;
}
body {
margin: 0;
}
div.container {
height: 100%;
display: flex;
flex-direction: column;
}
div.item{
flex: 1;
margin: 10px;
width: 100px;
background-color: blue;
}
Notice that now the container class has a height of 100% and both html and body have 100% height. If you set a fixed height in the flex container, the item’s height won’t be dynamic, because it’s container height is always the same.
We now have all that is needed to understand our initial simple layout with a top menu, a main area and a status bar at the bottom. We are setting the body as a flex container with flex-direction as column, adding three flex items, two of them have fixed heights (the menu and the status bar) and the main area with flex set to 1.
Here’s the full CSS:
html, body{
height: 100%;
}
body {
margin: 0;
display:flex;
flex-direction: column;
}
div#menu {
height: 25px;
flex: none;
}
div#mainArea{
flex: 1;
min-height: 0; //This is only relevant for Firefox, see note below
}
div#statusBar {
height: 25px;
flex: none;
}
And markup:
<body>
<div id="menu"></div>
<div id="mainArea"></div>
<div id="statusBar"></div>
</body>
You are probably wondering why I’ve added min-height: 0
to “mainArea”. It turns out that in Firefox a flex item’s minimum size is based on their children’s intrinsic size. An element’s intrinsic size is the size they would have if they hadn’t overflow: auto
. We’re adding a child to this flex item later on that we are expecting to overflow. That means that the min-size of “mainArea”, which defaults to min-size: auto
in Firefox, can potentially be bigger than the total available height. Setting min-height to 0 solves this. See this stack overflow question for more information or you can try reading the flexbox’s spec for min-size. Note that this is the behavior described in the spec, so it is likely that it will come to the other browsers as well.
Flex inside Flex
What we need now is a sidebar on the left and a content area on the right. And they should be inside what we’ve called our main area (div with id=”mainArea”).
To do this we’ll set the div with id=”mainArea” as a flex container with a flex-direction of row (the default, so if want you can even omit this property). We are going to add two new flex items inside this new flex container.
The first will have a fixed width and won’t expand or contract with its container, so it’s flex property will be none. It will be our sidebar.
We want the second item to stretch and shrink so that it takes all of the available space, so we are setting its flex property to 1. Also, because this will be the item that contains the contents of our “web page that looks like an app”, and that content might take more space than what’s available in the window, we’ll set its overflow property to auto so that the scroll bar is displayed automatically (remember the min-size:0
in the mainArea, this is the reason for it).
Let’s call the first item sidebar and make it a div, and the second content and make it a div.
Here’s the update CSS rules:
html,
body {
height: 100%;
}
body {
margin: 0;
display: flex;
flex-direction: column;
}
div#menu {
height: 25px;
flex: none;
}
div#mainArea {
flex-grow: 1;
display: flex;
min-size: 0;
}
div#sidebar {
width: 150px;
flex: none;
}
div#content {
flex: 1;
overflow: auto;
}
div#statusBar {
height: 25px;
flex: none;
}
And the final markup:
<body>
<div id="menu"></div>
<div id="mainArea">
<div id="sidebar"></div>
<div id="content"></div>
</div>
<div id="statusBar"></div>
</body>
If you use these styles and markup exactly as it is you’ll get a blank page. That’s to be expected because there’s actually no content on the page. But you can add this css rule so that you can “see” the layout:
div{
border: 1px dashed blue;
}
Or you can just get the markup for the example at the beginning of the post, fully styled, from github:
git clone https://github.com/ruidfigueiredo/webpagethatlookslikeadesktopapp.git
If you made it this far, here’s a nice thing to try: Go to Electron’s quick start page and follow the instructions to get the sample running. After that, replace the index.html that comes with the sample with the one you’ve created following the instructions from this blog post (don’t forget to add the css file with the styles) and run the sample. There you go, you’ve just created the skeleton of your first Electron app that runs on Windows, OSX and Linux.