Monday, April 1, 2013

Reviewing CSS Layout Techniques for Controling Content Size to Fit the Page

Sometimes going back to basics is not a bad thing. Generally, I learn something new every time I start over. In this case, I decided to spend some time looking at how to improve/simplify setting up the basic layout I might use to build an application. When I think about the problems I need to consider when creating my layout, I generally can break them down into these basic issues:


  • Creating various "regions" to hold content that are either fixed or resize to fit the remaining space

  • Keeping all the content inside the viewable browser window bounds. Any scrolling will be dealt with inside the different content "regions"

  • Ensuring the desired layout works well with widget libraries like jQuery UI



I used the term "region" to be as generic as possible. These can mean columns, headers, rows, panels - anything that holds some functional part of the application. That can translate into menus, tables, toolboxes, or various other widgets. For this discussion, I chose a basic 3-column layout where the middle column expands to fill the remaining space not used by the two fixed outside columns. The whole layout should also fill the entire available space on the page (both height and width). I'm going to start by solving the columns layout, move to the height issue, and then review how adding several widgets might require additional tweaking.

Creating a 3-Column Layout




I've found several approaches for creating multi-column layouts with both fixed and fluid columns. For a 3-column setup, the most common are two fixed column (left and right) with a fluid middle column.

css-layout-3-columns-fixed-fluid-fixed

Most references use floating elements but I also saw solutions for absolute positioned elements. In the end, I chose to use absolute positioning. Others may be more comfortable with floats, however, I prefer avoiding them. The setup for the columns is fairly simple - set the left and right to the fixed amount and then set the left/right margin of the middle element to match. Then use absolute positioning to line each element up across the page. When you resize the page, the left/right stay constant and the middle will automatically fill the remaining space.


<div class="col-wrapper">

<div class="col-left">
<div class="col-content">
</div>
</div>

<div class="col-mid">
<div class="col-content">
</div>
</div>

<div class="col-right">
<div class="col-content">
</div>
</div>
</div>



.col-wrapper {
position: relative;
width: 100%;
font-size: 1.2em;
}

.col-left, .col-mid, .col-right {
position: absolute;
}

.col-left {
left: 0;
width: 300px;
background-color: yellow;
}

.col-mid {
left: 0;
margin-left: 300px;
margin-right: 300px;
background-color: lightblue;
}

.col-right {
right: 0;
width: 300px;
background-color: pink;
}

.col-content {
margin: 0.8em;
}




Notice that .col-mid defines a margin the same size as the right/left width. The result looks like this page. That pattern will work well for 2 and 3-column layouts. Moving to four or more might pose a bigger challenge.

Filling the Available Height




So far, the content in the columns will flow down the page and the browser will scroll the whole view. In an application, this probably is not desirable since one region may contain a menu you want to remain in view when the user is scrolling through a list of data. This means you need to ensure you contain the content to the height and width of the browser. The width is generally easy to manage, block elements naturally fill the width of their parent - its the height that always causes problems. As much as I'd like to keep this a pure CSS solution, I've found that using a tiny bit of Javascript is really the best way to deal with the height. Not only can you set it when the page finishes loading, you can also reset it every time the browser window is resized:


$(function() {
$(window).on('resize', function () {
$('.col-wrapper').outerHeight($(window).outerHeight());
}).trigger('resize');
});


This takes care of the outer wrapper for the columns, however, the individual columns are not sized to fill that height. The simple solution seems to just make the height equal 100%. Which will work until you add any margin or padding on the element that you set this way. Since the columns are just going to define the location of other elements and not anything more, I can safely use height: 100% in my style. However, the content inside those columns also needs to fill the space and have margin to create some white space. If I apply the 100% height rule to those element, I get this resulting layout. The goal is to get something that looks like this:

css-layout-full-height-columns

I stumbled upon a site with various HTML/CSS layout patterns. The one I found that solved the problem is named "Stretched" and used absolutely positioned elements with the left/right/top/bottom styles set to force the element to fill in the available space with the margin included in the calculation. This approach avoids the problem with the inner element overrunning the bounds of the parent element because the height/width is 100% and the margin/padding causes the element to push outside the parent. Applying that concept to my layout example fixes the problem and the content fits perfectly with the desired margins.

Adding Widgets to the Design




Now that we have the two main pieces to our layout, let's try building something a little more useful. I took the same 3-column layout and added jQuery UI widgets to the different elements. One of the things to consider when doing this is that most of the widgets will add certain styles to the target elements (or even wrap them). These styles need to be accounted for in the layout so everything looks lined up and uncluttered (no extra borders, etc).

css-layout-full-height-columns-jqueryui

Before diving into specific issues related to the widgets, I factored out the important parts of the layout so it was reusable across anything I needed to use it with. The columns where in pretty good shape already, but the stretching styles can be separated into one class that can mixed with other class styles as needed:



/* Setup container to fill column area and not overflow the bounds */
.fill-box {
position: absolute;
height: auto;
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow: hidden;
}



As long as there is a relative parent, this will cause the target element to completely fill the available space created by the parent element. Other styles can define any margin/padding/border and not leave the bounding box.

Next we can add specific styles for each column. The left column will hold an Accordion widget which will have Draggable elements in each grouping. The middle will hold a Sortable widget that will also accept the Draggable elements, and the right column will hold some additional content to fill the space.



/* Define specific styles for each column container */
.toolbox {
margin: 5px;
margin-top: 3px;
padding-bottom: 1px;
}

.sortable {
margin: 5px;
border: 1px solid #aaa;
overflow-y: auto;
}

.content {
margin: 5px;
border: 1px solid #aaa;
padding: 5px;
overflow-y: auto;
}



The markup has the normal column elements plus a container for the widgets. The sortable and content area do not need any other wrappers, however, the accordion does to ensure it sizes correctly. Without the extra DIV, it was cut off at the bottom.



<div class="column-wrapper">

<div class="column-left">
<div class="fill-box toolbox">
<div>
/* Need to wrap the accordion so it sizes correctly */
<h3>Stuff A</h3>
<div>
/* DIVs for the Draggable widget */
</div>
<h3>Stuff B</h3>
<div>
/* DIVs for the Draggable widget */
</div>
<h3>Stuff C</h3>
<div>
/* DIVs for the Draggable widget */
</div>
<div>
</div>
</div>

<div class="column-middle">
<div class="fill-box sortable ui-corner-all">
/* DIVs for the Sortable widget */
</div>
</div>

<div class="column-right">
<div class="fill-box content ui-corner-all">
/* Paragraphs of text */
</div>
</div>

</div>



Now, the code to initialize each widget may need to be modified to handle the sizing we would like. The Sortable doesn't need any special consideration, however, both the Draggable and Accordion do require a few adjustments to account for the width/height we are targeting.

First, the accordion widget has a heightStyle which can be set to "fill" so the widget will match the height of its parent. This calculation is only done one upon initialization of the widget so any subsequent screen resizes will not cause the widget to fill the height properly. Since there is already a handler in place for the window resize, the accordion can be refreshed in that handler to resize it as well.


/* Track the window height and set the outer column wrapper to match */
$(window).on('resize', function () {
$('.column-wrapper').outerHeight($(window).outerHeight());

/* Make the accordion recalc the height */
$('.toolbox > div').filter('.ui-accordion').accordion( 'refresh' );
}).trigger('resize');

$('.toolbox')
.accordion({ heightStyle: 'fill' });


Next, the draggable needs some tweaking to move correctly from the accordion widget to the sortable widget. Whenever the source element is cloned to be dragged to the sortable, it will fill the width of the parent it will be appended to (it needs to be in the column-wrapper so it doesn't get lost from the overflow). Since the helper can take a function, we can clone the element and set its width to match so it retains its size during the drag:


$('.draggable')
.draggable({
connectToSortable: '.sortable',
helper: function() {
return $(this).clone().outerWidth($(this).outerWidth());
},
appendTo: '.column-wrapper'
}).disableSelection();


This approach provides a reusable pattern for crafting sections of a page that require filling specific dimensions. Its repeatable from the top of the page all the way down to images and buttons. If you've been struggling trying to keep portions of your page fluid without having to manage it with code or hard-coded sizing rules, then some of the ideas here might make things a little easier.