Layout and Styling of Jupyter widgets

This section presents how to layout and style Jupyter interactive widgets to build rich and reactive widget-based applications.

The layout attribute.

Jupyter interactive widgets have a layout attribute exposing a number of CSS properties that impact how widgets are laid out.

Exposed CSS properties

The following properties map to the values of the CSS properties of the same name (underscores being replaced with dashes), applied to the top DOM elements of the corresponding widget.

Sizes

  • height
  • width
  • max_height
  • max_width
  • min_height
  • min_width

Display

  • visibility
  • display
  • overflow
  • overflow_x (deprecated in 7.5, use overflow instead)
  • overflow_y (deprecated in 7.5, use overflow instead)

Box model

  • border
  • margin
  • padding

Positioning

  • top
  • left
  • bottom
  • right

Image/media

  • object_fit
  • object_position

Flexbox

  • order
  • flex_flow
  • align_items
  • flex
  • align_self
  • align_content
  • justify_content
  • justify_items

Grid layout

  • grid_auto_columns
  • grid_auto_flow
  • grid_auto_rows
  • grid_gap
  • grid_template_rows
  • grid_template_columns
  • grid_template_areas
  • grid_row
  • grid_column
  • grid_area

Shorthand CSS properties

You may have noticed that certain CSS properties such as margin-[top/right/bottom/left] seem to be missing. The same holds for padding-[top/right/bottom/left] etc.

In fact, you can atomically specify [top/right/bottom/left] margins via the margin attribute alone by passing the string '100px 150px 100px 80px' for a respectively top, right, bottom and left margins of 100, 150, 100 and 80 pixels.

Similarly, the flex attribute can hold values for flex-grow, flex-shrink and flex-basis. The border attribute is a shorthand property for border-width, border-style (required), and border-color.

Simple examples

The following example shows how to resize a Button so that its views have a height of 80px and a width of 50% of the available space. It also includes an example of setting a CSS property that requires multiple values (a border, in thise case):


In [1]:
from ipywidgets import Button, Layout

b = Button(description='(50% width, 80px height) button',
           layout=Layout(width='50%', height='80px', border='2px dotted blue'))
b


The layout property can be shared between multiple widgets and assigned directly.


In [2]:
Button(description='Another button with the same layout', layout=b.layout)


Is simple layout really simple?

The cell below adds a min_width and max_width to the button layout. The effect may be surprising; in CSS if max/min width are present they override the width.


In [3]:
b.layout.min_width='10%'
b.layout.max_width='20%'

Natural sizes, and arrangements using HBox and VBox

Most of the core-widgets have default heights and widths that tile well together. This allows simple layouts based on the HBox and VBox helper functions to align naturally:


In [4]:
from ipywidgets import Button, HBox, VBox

words = ['correct', 'horse', 'battery', 'staple']
items = [Button(description=w) for w in words]
left_box = VBox([items[0], items[1]])
right_box = VBox([items[2], items[3]])
HBox([left_box, right_box])


Flexbox and Grid

The Flexbox CSS specification is great for laying out items in a single direction, horizontally or vertically. As we saw in the previous example, two dimensional layout can be done with flexbox by using a combination of horizontal and vertical components.

The Grid CSS specifation is designed to be used for two dimensional layout. There are properties for specifying the number of items in each row or column, how they should be sized, and how items should be aligned.

For more information about Flexbox and Grid

The are notebooks with more detail about widgets and the Flexbox model and widgets and the Grid model. The code examples from each of those notebooks is included here also.

If you want to learn more about CSS layout after this tutorial, take a look at this excellent set of articles on CSS layout at MDN. The Flexbox and Grid articles each have links to more extensive guides at the end of the article.

The Flexbox layout

The HBox and VBox classes above are special cases of the Box widget.

The Box widget enables the entire CSS flexbox spec as well as the Grid layout spec, enabling rich reactive layouts in the Jupyter notebook. It aims at providing an efficient way to lay out, align and distribute space among items in a container.

Again, the whole flexbox spec is exposed via the layout attribute of the container widget (Box) and the contained items. One may share the same layout attribute among all the contained items.

Acknowledgement

The following flexbox tutorial on the flexbox layout follows the lines of the article A Complete Guide to Flexbox by Chris Coyier, and uses text and various images from the article with permission.

Basics and terminology

The flexbox layout spectrum is excellent for laying out items in a single direction, either horizontally or vertically.

Since flexbox is a whole module and not a single property, it involves a lot of things including its whole set of properties. Some of them are meant to be set on the container (parent element, known as "flex container") whereas the others are meant to be set on the children (known as "flex items"). If regular layout is based on both block and inline flow directions, the flex layout is based on "flex-flow directions". Please have a look at this figure from the specification, explaining the main idea behind the flex layout.

Basically, items will be laid out following either the main axis (from main-start to main-end) or the cross axis (from cross-start to cross-end).

  • main axis - The main axis of a flex container is the primary axis along which flex items are laid out. Beware, it is not necessarily horizontal; it depends on the flex-direction property (see below).
  • main-start | main-end - The flex items are placed within the container starting from main-start and going to main-end.
  • main size - A flex item's width or height, whichever is in the main dimension, is the item's main size. The flex item's main size property is either the ‘width’ or ‘height’ property, whichever is in the main dimension. cross axis - The axis perpendicular to the main axis is called the cross axis. Its direction depends on the main axis direction.
  • cross-start | cross-end - Flex lines are filled with items and placed into the container starting on the cross-start side of the flex container and going toward the cross-end side.
  • cross size - The width or height of a flex item, whichever is in the cross dimension, is the item's cross size. The cross size property is whichever of ‘width’ or ‘height’ that is in the cross dimension.

The VBox and HBox helpers

The VBox and HBox helper classes provide simple defaults to arrange child widgets in vertical and horizontal boxes. They are roughly equivalent to:

def VBox(*pargs, **kwargs):
    """Displays multiple widgets vertically using the flexible box model."""
    box = Box(*pargs, **kwargs)
    box.layout.display = 'flex'
    box.layout.flex_flow = 'column'
    box.layout.align_items = 'stretch'
    return box

def HBox(*pargs, **kwargs):
    """Displays multiple widgets horizontally using the flexible box model."""
    box = Box(*pargs, **kwargs)
    box.layout.display = 'flex'
    box.layout.align_items = 'stretch'
    return box

Examples

Four buttons in a VBox. Items stretch to the maximum width, in a vertical box taking 50% of the available space.


In [5]:
from ipywidgets import Layout, Button, Box

items_layout = Layout(width='auto')     # override the default width of the button to 'auto' to let the button grow

box_layout = Layout(display='flex',
                    flex_flow='column', 
                    align_items='stretch', 
                    border='solid',
                    width='50%')

words = ['correct', 'horse', 'battery', 'staple']
items = [Button(description=word, layout=items_layout, button_style='danger') for word in words]
box = Box(children=items, layout=box_layout)
box


Three buttons in an HBox. Items flex proportionally to their weight.


In [6]:
from ipywidgets import Layout, Button, Box, VBox

# Items flex proportionally to the weight and the left over space around the text 
items_auto = [
    Button(description='weight=1; auto', layout=Layout(flex='1 1 auto', width='auto'), button_style='danger'),
    Button(description='weight=3; auto', layout=Layout(flex='3 1 auto', width='auto'), button_style='danger'),
    Button(description='weight=1; auto', layout=Layout(flex='1 1 auto', width='auto'), button_style='danger'),
 ]

# Items flex proportionally to the weight 
items_0 = [
    Button(description='weight=1; 0%', layout=Layout(flex='1 1 0%', width='auto'), button_style='danger'),
    Button(description='weight=3; 0%', layout=Layout(flex='3 1 0%', width='auto'), button_style='danger'),
    Button(description='weight=1; 0%', layout=Layout(flex='1 1 0%', width='auto'), button_style='danger'),
 ]
box_layout = Layout(display='flex',
                    flex_flow='row', 
                    align_items='stretch', 
                    width='70%')
box_auto = Box(children=items_auto, layout=box_layout)
box_0 = Box(children=items_0, layout=box_layout)
VBox([box_auto, box_0])


A more advanced example: a reactive form.

The form is a VBox of width '50%'. Each row in the VBox is an HBox, that justifies the content with space between..


In [7]:
from ipywidgets import Layout, Button, Box, FloatText, Textarea, Dropdown, Label, IntSlider

form_item_layout = Layout(
    display='flex',
    flex_flow='row',
    justify_content='space-between'
)

form_items = [
    Box([Label(value='Age of the captain'), IntSlider(min=40, max=60)], layout=form_item_layout),
    Box([Label(value='Egg style'), 
         Dropdown(options=['Scrambled', 'Sunny side up', 'Over easy'])], layout=form_item_layout),
    Box([Label(value='Ship size'), 
         FloatText()], layout=form_item_layout),
    Box([Label(value='Information'), 
         Textarea()], layout=form_item_layout)
]

form = Box(form_items, layout=Layout(
    display='flex',
    flex_flow='column',
    border='solid 2px',
    align_items='stretch',
    width='50%'
))
form


A more advanced example: a carousel.


In [8]:
from ipywidgets import Layout, Button, Box, Label

item_layout = Layout(height='100px', min_width='40px')
items = [Button(layout=item_layout, description=str(i), button_style='warning') for i in range(40)]
box_layout = Layout(overflow_x='scroll',
                    border='3px solid black',
                    width='500px',
                    height='',
                    flex_flow='row',
                    display='flex')
carousel = Box(children=items, layout=box_layout)
VBox([Label('Scroll horizontally:'), carousel])


Compatibility note

The overflow_x and overflow_y options are deprecated in ipywidgets 7.5. Instead, use the shorthand property overflow='scroll hidden'. The first part specificies overflow in x, the second the overflow in y.

A widget for exploring layout options

Use the dropdowns and sliders in the widget to change the layout of the box containing the colored buttons. Many of the CSS layout options described above are available, and the Python code to generate a Layout object reflecting the settings is in a TextArea in the widget.

A few questions to answer after the demonstration of this (see the detailed flexbox guide for a longer discussion):

  1. What does changing justify_content affect? You may find it easier to answer this if you set wrap to wrap.
  2. What does align_items affect?
  3. How is align_content different than align_items?

In [ ]:
from layout_preview import layout
layout

Exercises

Four buttons in a box revisted: Change order and orientation

This example, from earlier in this notebook, lays out 4 buttons vertically.

Flexbox allows you to change the order and orientation of the children items in the flexbox without changing the children themselves.

  1. Change the flex_flow so that the buttons are displayed in a single column in reverse order.
  2. Change the flex_flow so that the buttons are displayed in a single row instead of a column.
  3. Try setting a few values of align_items and describe how it affects the display of the buttons.
  4. Make the box narrower by changing the width, then change flex_flow to lay out the buttons in rows that wrap so that there is a 2x2 grid of buttons.

Feel free to figure out the layout using the tool above and copy/paste the layout here!


In [ ]:
from ipywidgets import Layout, Button, Box

items_layout = Layout(width='auto')     # override the default width of the button to 'auto' to let the button grow

box_layout = Layout(display='flex',
                    flex_flow='column',  
                    align_items='stretch', 
                    border='solid',
                    width='20%')

words = ['correct', 'horse', 'battery', 'staple']
items = [Button(description=word, layout=items_layout, button_style='danger') for word in words]
box = Box(children=items, layout=box_layout)
box

Carousel revisted: item layout

The code that generated the carousel is reproduced below. Run the cell, then continue reading.


In [ ]:
from ipywidgets import Layout, Button, Box, Label

item_layout = Layout(height='100px', min_width='40px')
items = [Button(layout=item_layout, description=str(i), button_style='warning') for i in range(40)]
box_layout = Layout(overflow_x='scroll',
                    border='3px solid black',
                    width='500px',
                    height='',
                    flex_flow='row',
                    display='flex')
carousel = Box(children=items, layout=box_layout)
VBox([Label('Scroll horizontally:'), carousel])

To do:

  • Change the min_width for one of the items, say the first one. Does it affect only the first one, or all of them? Why?
  • Change the height of only the first button. Hint: It needs its own Layout.

In [ ]:
items[0].layout.min_width = 'FILL IN WITH A WIDTH'

The Grid layout

The GridBox class is a special case of the Box widget.

The Box widget enables the entire CSS flexbox spec, enabling rich reactive layouts in the Jupyter notebook. It aims at providing an efficient way to lay out, align and distribute space among items in a container.

A more detailed description of the Grid layout is available.

The whole grid layout spec is exposed via the layout attribute of the container widget (Box) and the contained items. One may share the same layout attribute among all the contained items.

The following flexbox tutorial on the flexbox layout follows the lines of the article A Complete Guide to Grid by Chris House, and uses text and various images from the article with permission.

Basics

To get started you have to define a container element as a grid with display: grid, set the column and row sizes with grid-template-rows, grid-template-columns, and grid_template_areas, and then place its child elements into the grid with grid-column and grid-row. Similarly to flexbox, the source order of the grid items doesn't matter. Your CSS can place them in any order, which makes it super easy to rearrange your grid with media queries. Imagine defining the layout of your entire page, and then completely rearranging it to accommodate a different screen width all with only a couple lines of CSS. Grid is one of the most powerful CSS modules ever introduced.

Important terminology

Before diving into the concepts of Grid it's important to understand the terminology. Since the terms involved here are all kinda conceptually similar, it's easy to confuse them with one another if you don't first memorize their meanings defined by the Grid specification. But don't worry, there aren't many of them.

Grid Container

The element on which display: grid is applied. It's the direct parent of all the grid items. In this example container is the grid container.

<div class="container">
  <div class="item item-1"></div>
  <div class="item item-2"></div>
  <div class="item item-3"></div>
</div>

Grid Item

The children (e.g. direct descendants) of the grid container. Here the item elements are grid items, but sub-item isn't.

<div class="container">
  <div class="item"></div> 
  <div class="item">
    <p class="sub-item"></p>
  </div>
  <div class="item"></div>
</div>

Grid Line

The dividing lines that make up the structure of the grid. They can be either vertical ("column grid lines") or horizontal ("row grid lines") and reside on either side of a row or column. Here the yellow line is an example of a column grid line.

Grid Track

The space between two adjacent grid lines. You can think of them like the columns or rows of the grid. Here's the grid track between the second and third row grid lines.

A more detailed description of the Grid layout is available. The Grid layout guide on MDN is also excellent.


In [ ]:
from ipywidgets import Button, GridBox, Layout, ButtonStyle

The first example defines a 3x3 grid and places 9 buttons into the grid.


In [ ]:
GridBox(children=[Button(description=str(i), layout=Layout(width='auto', height='auto'),
                         style=ButtonStyle(button_color='darkseagreen')) for i in range(9)
                 ],
        layout=Layout(
            width='50%',
            grid_template_columns='100px 50px 100px',
            grid_template_rows='80px auto 80px', 
            grid_gap='5px 10px')
       )

Exercises

Add more buttons

Modify the code above to place more buttons in the GridBox (do not modify the layout). Any number of buttons larger than 9 is fine.

  1. What happens to the extra buttons? Are they laid out like the first 9 buttons?

The grid template defines a 3x3 grid. If additional children are placed in the grid their properties are determined by the layout properties grid_auto_columns, grid_auto_rows and grid_auto_flow properties.

  1. Set grid_auto_rows="10px" and rerun the example with more than 9 buttons.

  2. Set grid_auto_rows so that the automatically added rows have the same format as the templated rows.

An alternate way of defining the grid

The grid can also be set up using a description words. The layout below defines a grid with 4 columns and 3 rows. The first row is a header, the bottom row is a footer, and the middle row has content in the first two columns, then an empty cell, followed by a sidebar.

Widgets are assigned to each of these areas by setting the widgets's layout grid_area to the name of the area.

            "header header header header"
            "main main . sidebar "
            "footer footer footer footer"

In [ ]:
header  = Button(description='Header',
                 layout=Layout(width='auto', grid_area='header'),
                 style=ButtonStyle(button_color='lightblue'))
main    = Button(description='Main',
                 layout=Layout(width='auto', grid_area='main'),
                 style=ButtonStyle(button_color='moccasin'))
sidebar = Button(description='Sidebar',
                 layout=Layout(width='auto', grid_area='sidebar'),
                 style=ButtonStyle(button_color='salmon'))
footer  = Button(description='Footer',
                 layout=Layout(width='auto', grid_area='footer'),
                 style=ButtonStyle(button_color='olive'))

GridBox(children=[header, main, sidebar, footer],
        layout=Layout(
            width='50%',
            grid_template_rows='auto auto auto',
            grid_template_columns='25% 25% 25% 25%',
            grid_template_areas='''
            "header header header header"
            "main main . sidebar "
            "footer footer footer footer"
            ''')
       )

Exercises

Make the main area larger

  1. Add another row or two to the template area so that the main area is 3 rows high and 2 columns wide.