{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Layout and Styling of Jupyter widgets\n", "\n", "This section presents how to layout and style Jupyter interactive widgets to build rich and *reactive* widget-based applications." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The `layout` attribute.\n", "\n", "Jupyter interactive widgets have a `layout` attribute exposing a number of CSS properties that impact how widgets are laid out.\n", "\n", "### Exposed CSS properties\n", "\n", "
\n", "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.\n", "
\n", "\n", "\n", "#### Sizes\n", "\n", "- `height`\n", "- `width`\n", "- `max_height`\n", "- `max_width`\n", "- `min_height`\n", "- `min_width`\n", "\n", "#### Display\n", "\n", "- `visibility`\n", "- `display`\n", "- `overflow`\n", "- `overflow_x` (deprecated in `7.5`, use `overflow` instead)\n", "- `overflow_y` (deprecated in `7.5`, use `overflow` instead)\n", "\n", "#### Box model\n", "\n", "- `border` \n", "- `margin`\n", "- `padding`\n", "\n", "#### Positioning\n", "\n", "- `top`\n", "- `left`\n", "- `bottom`\n", "- `right`\n", "\n", "#### Image/media\n", "\n", "- `object_fit`\n", "- `object_position`\n", "\n", "#### Flexbox\n", "\n", "- `order`\n", "- `flex_flow`\n", "- `align_items`\n", "- `flex`\n", "- `align_self`\n", "- `align_content`\n", "- `justify_content`\n", "- `justify_items`\n", "\n", "#### Grid layout\n", "\n", "- `grid_auto_columns`\n", "- `grid_auto_flow`\n", "- `grid_auto_rows`\n", "- `grid_gap`\n", "- `grid_template_rows`\n", "- `grid_template_columns`\n", "- `grid_template_areas`\n", "- `grid_row`\n", "- `grid_column`\n", "- `grid_area`\n", "\n", "### Shorthand CSS properties\n", "\n", "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.\n", "\n", "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.\n", "\n", "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`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Simple examples" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from ipywidgets import Button, Layout\n", "\n", "b = Button(description='(50% width, 80px height) button',\n", " layout=Layout(width='50%', height='80px', border='2px dotted blue'))\n", "b" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `layout` property can be shared between multiple widgets and assigned directly." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "Button(description='Another button with the same layout', layout=b.layout)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Natural sizes, and arrangements using HBox and VBox\n", "\n", "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:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from ipywidgets import Button, HBox, VBox\n", "\n", "words = ['correct', 'horse', 'battery', 'staple']\n", "items = [Button(description=w) for w in words]\n", "left_box = VBox([items[0], items[1]])\n", "right_box = VBox([items[2], items[3]])\n", "HBox([left_box, right_box])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Flexbox and Grid\n", "\n", "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.\n", "\n", "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.\n", "\n", "### For more information about Flexbox and Grid\n", "\n", "The are notebooks with more detail about [widgets and the Flexbox model](reference_guides/guide-flex-box.ipynb) and [widgets and the Grid model](reference_guides/guide-grid-box.ipynb). The code examples from each of those notebooks is included here also.\n", "\n", "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](https://developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout). The Flexbox and Grid articles each have links to more extensive guides at the end of the article." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The Flexbox layout\n", "\n", "The `HBox` and `VBox` classes above are special cases of the `Box` widget.\n", "\n", "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.\n", "\n", "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.\n", "\n", "### Acknowledgement\n", "\n", "The following flexbox tutorial on the flexbox layout follows the lines of the article [A Complete Guide to Flexbox](https://css-tricks.com/snippets/css/a-guide-to-flexbox/) by Chris Coyier, and uses text and various images from the article [with permission](https://css-tricks.com/license/).\n", "\n", "### Basics and terminology\n", "\n", "The flexbox layout spectrum is excellent for laying out items in a single direction, either horizontally or vertically. \n", "\n", "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\").\n", "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.\n", "\n", "![Flexbox](./images/flexbox.png)\n", "\n", "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`).\n", "\n", "- `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).\n", "- `main-start | main-end` - The flex items are placed within the container starting from main-start and going to main-end.\n", "- `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.\n", "cross axis - The axis perpendicular to the main axis is called the cross axis. Its direction depends on the main axis direction.\n", "- `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.\n", "- `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.\n", "\n", "### The VBox and HBox helpers\n", "\n", "The `VBox` and `HBox` helper classes provide simple defaults to arrange child widgets in vertical and horizontal boxes. They are roughly equivalent to:\n", "\n", "```Python\n", "def VBox(*pargs, **kwargs):\n", " \"\"\"Displays multiple widgets vertically using the flexible box model.\"\"\"\n", " box = Box(*pargs, **kwargs)\n", " box.layout.display = 'flex'\n", " box.layout.flex_flow = 'column'\n", " box.layout.align_items = 'stretch'\n", " return box\n", "\n", "def HBox(*pargs, **kwargs):\n", " \"\"\"Displays multiple widgets horizontally using the flexible box model.\"\"\"\n", " box = Box(*pargs, **kwargs)\n", " box.layout.display = 'flex'\n", " box.layout.align_items = 'stretch'\n", " return box\n", "```\n", "\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Examples\n", "\n", "**Four buttons in a VBox. Items stretch to the maximum width, in a vertical box taking `50%` of the available space.**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from ipywidgets import Layout, Button, Box\n", "\n", "items_layout = Layout(width='auto') # override the default width of the button to 'auto' to let the button grow\n", "\n", "box_layout = Layout(display='flex',\n", " flex_flow='column', \n", " align_items='stretch', \n", " border='solid',\n", " width='50%')\n", "\n", "words = ['correct', 'horse', 'battery', 'staple']\n", "items = [Button(description=word, layout=items_layout, button_style='danger') for word in words]\n", "box = Box(children=items, layout=box_layout)\n", "box" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Three buttons in an HBox. Items flex proportionally to their weight.**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from ipywidgets import Layout, Button, Box, VBox\n", "\n", "# Items flex proportionally to the weight and the left over space around the text \n", "items_auto = [\n", " Button(description='weight=1; auto', layout=Layout(flex='1 1 auto', width='auto'), button_style='danger'),\n", " Button(description='weight=3; auto', layout=Layout(flex='3 1 auto', width='auto'), button_style='danger'),\n", " Button(description='weight=1; auto', layout=Layout(flex='1 1 auto', width='auto'), button_style='danger'),\n", " ]\n", "\n", "# Items flex proportionally to the weight \n", "items_0 = [\n", " Button(description='weight=1; 0%', layout=Layout(flex='1 1 0%', width='auto'), button_style='danger'),\n", " Button(description='weight=3; 0%', layout=Layout(flex='3 1 0%', width='auto'), button_style='danger'),\n", " Button(description='weight=1; 0%', layout=Layout(flex='1 1 0%', width='auto'), button_style='danger'),\n", " ]\n", "box_layout = Layout(display='flex',\n", " flex_flow='row', \n", " align_items='stretch', \n", " width='70%')\n", "box_auto = Box(children=items_auto, layout=box_layout)\n", "box_0 = Box(children=items_0, layout=box_layout)\n", "VBox([box_auto, box_0])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**A more advanced example: a reactive form.**\n", "\n", "The form is a `VBox` of width '50%'. Each row in the VBox is an HBox, that justifies the content with space between.." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from ipywidgets import Layout, Button, Box, FloatText, Textarea, Dropdown, Label, IntSlider\n", "\n", "form_item_layout = Layout(\n", " display='flex',\n", " flex_flow='row',\n", " justify_content='space-between'\n", ")\n", "\n", "form_items = [\n", " Box([Label(value='Age of the captain'), IntSlider(min=40, max=60)], layout=form_item_layout),\n", " Box([Label(value='Egg style'), \n", " Dropdown(options=['Scrambled', 'Sunny side up', 'Over easy'])], layout=form_item_layout),\n", " Box([Label(value='Ship size'), \n", " FloatText()], layout=form_item_layout),\n", " Box([Label(value='Information'), \n", " Textarea()], layout=form_item_layout)\n", "]\n", "\n", "form = Box(form_items, layout=Layout(\n", " display='flex',\n", " flex_flow='column',\n", " border='solid 2px',\n", " align_items='stretch',\n", " width='50%'\n", "))\n", "form" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**A more advanced example: a carousel.**" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from ipywidgets import Layout, Button, Box, Label\n", "\n", "item_layout = Layout(height='100px', min_width='40px')\n", "items = [Button(layout=item_layout, description=str(i), button_style='warning') for i in range(40)]\n", "box_layout = Layout(overflow_x='scroll',\n", " border='3px solid black',\n", " width='500px',\n", " height='',\n", " flex_flow='row',\n", " display='flex')\n", "carousel = Box(children=items, layout=box_layout)\n", "VBox([Label('Scroll horizontally:'), carousel])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### *Compatibility note*\n", "\n", "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`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## A widget for exploring layout options\n", "\n", "The widgets below was originally written by ipywidgets user [Doug Redden (@DougRzz)](https://github.com/DougRzz). If you want to look through the source code to see how it works, take a look at this [notebook he contributed](cssJupyterWidgetStyling-UI.ipynb).\n", "\n", "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.\n", "\n", "A questions to answer after the demonstration of this (see the [detailed flexbox guide for a longer discussion](reference_guides/guide-flex-box.ipynb)):\n", "\n", "1. What does changing `justify_content` affect? You may find it easier to answer this if you set `wrap` to `wrap`.\n", "2. What does `align_items` affect? \n", "3. How is `align_content` different than `align_items`?\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from layout_preview import layout\n", "layout" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Exercises" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Four buttons in a box revisted: Change order and orientation**\n", "\n", "This example, from earlier in this notebook, lays out 4 buttons vertically.\n", "\n", "Flexbox allows you to change the order and orientation of the children items in the flexbox without changing the children themselves.\n", "\n", "1. Change the `flex_flow` so that the buttons are displayed in a single column in *reverse order*.\n", "2. Change the `flex_flow` so that the buttons are displayed in a single *row* instead of a column.\n", "3. Try setting a few values of `align_items` and describe how it affects the display of the buttons. \n", "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.\n", "\n", "Feel free to figure out the layout using the tool above and copy/paste the layout here!" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from ipywidgets import Layout, Button, Box\n", "\n", "items_layout = Layout(width='auto') # override the default width of the button to 'auto' to let the button grow\n", "\n", "box_layout = Layout(display='flex',\n", " flex_flow='column', \n", " align_items='stretch', \n", " border='solid',\n", " width='20%')\n", "\n", "words = ['correct', 'horse', 'battery', 'staple']\n", "items = [Button(description=word, layout=items_layout, button_style='danger') for word in words]\n", "box = Box(children=items, layout=box_layout)\n", "box" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Carousel revisted: item layout**\n", "\n", "The code that generated the carousel is reproduced below. Run the cell, then continue reading." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from ipywidgets import Layout, Button, Box, Label\n", "\n", "item_layout = Layout(height='100px', min_width='40px')\n", "items = [Button(layout=item_layout, description=str(i), button_style='warning') for i in range(40)]\n", "box_layout = Layout(overflow_x='scroll',\n", " border='3px solid black',\n", " width='500px',\n", " height='',\n", " flex_flow='row',\n", " display='flex')\n", "carousel = Box(children=items, layout=box_layout)\n", "VBox([Label('Scroll horizontally:'), carousel])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**To do:**\n", "\n", "+ 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?\n", "+ Change the `height` of *only* the first button. *Hint:* It needs its own `Layout`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "items[0].layout.min_width = 'FILL IN WITH A WIDTH'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## The Grid layout\n", "\n", "The `GridBox` class is a special case of the `Box` widget.\n", "\n", "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.\n", "\n", "A more detailed description of the [Grid layout is available](reference_guides/guide-grid-box.ipynb).\n", "\n", "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.\n", "\n", "The following flexbox tutorial on the flexbox layout follows the lines of the article [A Complete Guide to Grid](https://css-tricks.com/snippets/css/complete-guide-grid/) by Chris House, and uses text and various images from the article [with permission](https://css-tricks.com/license/).\n", "\n", "### Basics\n", "\n", "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.\n", "\n", "### Important terminology\n", "\n", "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.\n", "\n", "**Grid Container**\n", "\n", "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.\n", "\n", "```html\n", "
\n", "
\n", "
\n", "
\n", "
\n", "```\n", "\n", "**Grid Item**\n", "\n", "The children (e.g. direct descendants) of the grid container. Here the item elements are grid items, but sub-item isn't.\n", "\n", "```html\n", "
\n", "
\n", "
\n", " \t

\n", "
\n", "
\n", "
\n", "```\n", "\n", "**Grid Line**\n", "\n", "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.\n", "\n", "![grid-line](images/grid-line.png)\n", "\n", "**Grid Track**\n", "\n", "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.\n", "\n", "![grid-track](images/grid-track.png)\n", "\n", "A more detailed description of the [Grid layout is available](reference_guides/guide-grid-box.ipynb). The [Grid layout guide on MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout#Guides) is also excellent." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from ipywidgets import Button, GridBox, Layout, ButtonStyle" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The first example defines a 3x3 grid and places 9 buttons into the grid. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "GridBox(children=[Button(description=str(i), layout=Layout(width='auto', height='auto'),\n", " style=ButtonStyle(button_color='darkseagreen')) for i in range(9)\n", " ],\n", " layout=Layout(\n", " width='50%',\n", " grid_template_columns='100px 50px 100px',\n", " grid_template_rows='80px auto 80px', \n", " grid_gap='5px 10px')\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Exercises" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Add more buttons**\n", "\n", "Modify the code above to place more buttons in the `GridBox` (do *not* mdify the layout). Any number of buttons larger than 9 is fine.\n", "\n", "1. What happens to the extra buttons? Are they laid out like the first 9 buttons?\n", "\n", "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.\n", "\n", "2. Set `grid_auto_rows=\"10px\"` and rerun the example with more than 9 buttons.\n", "\n", "3. Set `grid_auto_rows` so that the automatically added rows have the same format as the templated rows." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### An alternate way of defining the grid\n", "\n", "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.\n", "\n", "Widgets are assigned to each of these areas by setting the widgets's layout `grid_area` to the name of the area.\n", "\n", "```\n", " \"header header header header\"\n", " \"main main . sidebar \"\n", " \"footer footer footer footer\"\n", "```" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "header = Button(description='Header',\n", " layout=Layout(width='auto', grid_area='header'),\n", " style=ButtonStyle(button_color='lightblue'))\n", "main = Button(description='Main',\n", " layout=Layout(width='auto', grid_area='main'),\n", " style=ButtonStyle(button_color='moccasin'))\n", "sidebar = Button(description='Sidebar',\n", " layout=Layout(width='auto', grid_area='sidebar'),\n", " style=ButtonStyle(button_color='salmon'))\n", "footer = Button(description='Footer',\n", " layout=Layout(width='auto', grid_area='footer'),\n", " style=ButtonStyle(button_color='olive'))\n", "\n", "GridBox(children=[header, main, sidebar, footer],\n", " layout=Layout(\n", " width='50%',\n", " grid_template_rows='auto auto auto',\n", " grid_template_columns='25% 25% 25% 25%',\n", " grid_template_areas='''\n", " \"header header header header\"\n", " \"main main . sidebar \"\n", " \"footer footer footer footer\"\n", " ''')\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Exercises" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Make the main area larger**\n", "\n", "1. Add another row or two to the template area so that the main area is 3 rows high and 2 columns wide. " ] } ], "metadata": { "kernelspec": { "display_name": "widgets-tutorial", "language": "python", "name": "widgets-tutorial" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.3" } }, "nbformat": 4, "nbformat_minor": 2 }