Layouting

The main layouting primitive Bonito offers is Grid, Column and Row. They are all based on css display: grid and Bonito offers a small convenience wrapper around it.

We recommend reading through the great introduction to Styles grids by Josh Comeau: interactive guide to grid, for a better understanding of how grids work. It's recommended to read this before following this tutorial, since the examples are styled much nicer and explain everything in much greater detail.

To easier apply the tutorial to Bonito, we ported all the examples of the tutorial, while only describing them with the bare minimum. To get the full picture, please refer to the linked, original tutorial!

Let's start with the docstring for Grid:

Bonito.GridFunction
Grid(
    elems...;
    gap="10px",
    width="100%",
    height="100%",
    # All below Attributes are set to the default CSS values:
    columns="none",
    rows="none",
    areas="none",
    justify_content="normal",
    justify_items="legacy",
    align_content="normal",
    align_items="legacy",
    style::Styles=Styles(),
    div_attributes...,
)

A Grid is a container that lays out its children in a grid, based on the powerful css display: grid property.

It pretty much just sets the css attributes to some defaults, but everything can be overwritten or extended by passing your own style=Styles(...) object. All Styles objects inside one App will be merged into a single stylesheet, so using many grids with the same keyword arguments will only generate one entry into the global stylesheet. You can read more about this in the styling section.

There's also Row and Col which uses Grid under the hood:

Bonito.RowFunction
Row(elems...; grid_attributes...)

Places objects in a row, based on Grid.

Bonito.ColFunction
Col(elems...; grid_attributes...)

Places objects in a column, based on Grid.

Implicit Grids

If we don't specify any attributes for the Grid, the default will be one dynamic column, where every item gets their own row:

DemoCard(content=DOM.div(); style=Styles(), attributes...) = Card(content; backgroundcolor=:silver, border_radius="2px", style=Styles(style, "color" => :white),attributes...)


App() do sess
    s = Bonito.Slider(1:5)
    cards = map(s.value) do n
        cards = [DemoCard(height="50px", width="300px") for i in 1:n]
        return Grid(cards;)
    end
    return Bonito.record_states(sess, Grid(s, cards))
end

If we specify a height, while not specifying a height for the elements, the space will be partitioned for the n children:

App() do sess
    s = Bonito.Slider(1:5)
    cards = map(s.value) do n
        cards = [DemoCard(width="300px") for i in 1:n]
        Grid(cards; height="300px")
    end
    return Bonito.record_states(sess, Grid(s, cards))
end

To introduce new columns, we can specify a width for each column via the column keyword, which supports any css unit:

App() do
    grid = Grid(DemoCard(), DemoCard();
        height="300px", columns="25% 75%")
    return grid
end

Grid also introduced a new css unit namely fr, which represents the fraction of the leftover space in the grid container.

To see what that means, the below example shows how the percent based unit will shrink proportionally, while fr divides the space more evenly and can take the size of the content into it's constraint solver:

App() do sess
    container_width = Bonito.Slider(5:0.1:100)
    container_width[] = 100
    imstyle = Styles(
        "display" => :block, "position" => :relative, "width" => "100px",
        "max-width" => :none # needs to be set so it's not overwritten by others
    )
    img = DOM.img(; src="https://docs.makie.org/stable/assets/makie_logo_transparent.svg", style=imstyle)
    style = Styles("position" => :relative, "display" => :flex, "justify-content" => :center, "align-items" => :center)


    function example_grid(cols)
        grid = Grid(DemoCard(img; style=style), DemoCard(DOM.div(); style=style); columns=cols)
        container = DOM.div(grid; style=Styles("height" => "200px", "width" => "500px"))
        return Grid(container; rows="1fr", justify_content="center"), container
    end
    pgrid, p1 = example_grid("25% 75%")
    frgrid, p2 = example_grid("1fr 3fr")
    grid_percent = DOM.div(Grid(pgrid; rows="1fr"))
    grid_fr = DOM.div(Grid(frgrid; rows="1fr"))


    onjs(sess, container_width.value, js"w=> {$(p1).style.width = (5 * w) + 'px';}")
    onjs(sess, container_width.value, js"w=> {$(p2).style.width = (5 * w) + 'px';}")
    title_percent = DOM.h2("Grid(...; columns=\"25% 75%\")")
    title_fr = DOM.h2("Grid(...; columns=\"1fr 3fr\")")
    return Grid(container_width, title_percent, grid_percent, title_fr, grid_fr; columns="1fr")
end

Grid(...; columns="25% 75%")

Grid(...; columns="1fr 3fr")

Now, what happens if we add more then 2 items to a Grid with 2 columns?

# Little helper to create a Card with centered content
centered(c; style=Styles(), kw...) = DemoCard(Centered(DOM.h4(c; style=Styles("color" => :white))); style=style)


App() do
    cards = [centered(i) for i in 1:3]
    grid = Grid(cards...; columns="1fr 3fr")
    return DOM.div(grid; style=Styles("margin" => "20px"))
end

1

2

3

Specifying the size of the rows works exactly the same as with columns:

App() do
    cards = [centered(i) for i in 1:4]
    grid = Grid(cards...; columns="1fr 3fr", rows="5rem 1fr")
    return DOM.div(grid; style=Styles("width" => "400px", "height" => "300px", "margin" => "20px"))
end

1

2

3

4

Now, if we want to do something with lots of rows/columns, e.g. a calendar, it may get tiring to write those out, which is why css offers the repeat function:

App() do
    cards = [centered(i) for i in 1:31]
    grid = Grid(cards...; columns="repeat(7, 1fr)")
    return DOM.div(grid; style=Styles("width" => "400px", "margin" => "5px"))
end

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

Assigning children

Children can be assigned slots in the layout explicitly, and it's also possible to assign them to multiple slots. The css syntax for this is:

start_column = 1
end_column = 3
start_row = 1
end_row = 3


style = Styles(
    "grid-column" => "$start_column / $end_column",
    "grid-row" => "$start_row / $end_row"
)
child = DOM.div(style=style) # assign child from slot 1-2

To illustrate how this works, here is an interactive app where you can select the slots in the grid, and see the corresponding grid/column assignments:

function centered2d(i, j;)
    return centered("($i, $j)"; dataCol="$i,$j", style=Styles("user-select" => :none))
end


App() do
    cards = [centered2d(i, j) for i in 1:4 for j in 1:4]


    hover_style = Styles(
        "background-color" => :blue,
        "opacity" => 0.2,
        "z-index" => 1,
        "display" => :none,
        "user-select" => :none,
    )


    hover = DOM.div(; style=hover_style)


    grid_style = Styles("position" => :absolute, "top" => 0, "left" => 0)
    size = "300px"
    background_grid = Grid(cards...; columns="repeat(4, 1fr)", gap="0px",
        style=grid_style, height=size, width=size)


    rows = [DOM.div() for i in 1:15]


    selected_grid = Grid(hover, rows...; columns="repeat(4, 1fr)", gap="0px",
        style=grid_style, height=size, width=size)


    style_display = centered("Styles(...)"; width="100%")


    hover_js = js"""
        const hover = $(hover);
        const grid = $(selected_grid);
        const style_display = $(style_display);
        const h2_node = style_display.children[0].children[0];


        let is_dragging = false;
        let start_position = null;
        function get_element(e) {
            const elements = document.elementsFromPoint(e.clientX, e.clientY);
            for (let i = 0; i < elements.length; i++) {
                const element = elements[i];
                if (element.getAttribute('data-col')) {
                    return element;
                }
            }
            return
        }


        function handle_click(current) {
            // Check if the current element is a child of the container
            const index = current.getAttribute('data-col')
            if (index) {
                const start = start_position.split(",").map(x=> parseInt(x))
                const end = index.split(",").map(x=> parseInt(x))


                const [start_row, end_row] = [start[0], end[0]].sort()
                const [start_col, end_col] = [start[1], end[1]].sort()
                const row = (start_row) + " / " + (end_row + 1)
                const col = (start_col) + " / " + (end_col + 1)
                hover.style["grid-row"] = row
                hover.style["grid-column"] = col
                const nelems = 16 - ((end_row - start_row+ 1) * (end_col - start_col+ 1))
                for (let i = 0; i < grid.children.length; i++) {
                    console.log(i)
                    const child = grid.children[i];
                    if (child == hover) {
                        continue
                    }
                    child.style.display = nelems >= i ? "block" : "none";
                }


                h2_node.innerText = 'Styles(\n\"grid-row\" => \"' + row + '\",\n \"grid-column\" => \"' + col + '\"\n)';
            }
        }


        grid.addEventListener('mousedown', (e) => {
            if (!hover) {
                return
            }
            is_dragging = true;
            const current = get_element(e);
            const index = current.getAttribute('data-col')
            if (!index) {
                return;
            }
            start_position = index
            hover.style.display = "block"; // unhide hover element
            handle_click(current);
        });


        grid.addEventListener('mousemove', (e) => {
            if (!is_dragging) return;
            const current = get_element(e);
            handle_click(current);
        });


        document.addEventListener('mouseup', () => {
            is_dragging = false;
        });


    """
    grids = DOM.div(background_grid, selected_grid, hover_js; style=Styles("position" => :relative, "height" => size))


    return Grid(style_display, grids; columns="1fr 2fr", width="100%")
end

Styles(...)

(1, 1)

(1, 2)

(1, 3)

(1, 4)

(2, 1)

(2, 2)

(2, 3)

(2, 4)

(3, 1)

(3, 2)

(3, 3)

(3, 4)

(4, 1)

(4, 2)

(4, 3)

(4, 4)

Grid areas

We can now easily create complex layouts like this:

App() do


    sidebar = DemoCard(
        "SIDEBAR",
        style = Styles(
            "grid-column" =>  "1",
            "grid-row" =>  "1 / 3",
        )
    )


    header = DemoCard(
        "HEADER",
        style = Styles(
            "grid-column" =>  "2",
            "grid-row" =>  "1",
        )
    )


    main = DemoCard(
        "MAIN",
        style = Styles(
            "grid-column" =>  "2",
            "grid-row" =>  "2",
        )
    )


    grid = Grid(
        sidebar, header, main,
        columns = "2fr 5fr",
        rows = "50px 1fr"
    )
    return DOM.div(grid; style=Styles("height" => "600px", "margin" => "20px", "position" => :relative))
end
SIDEBAR
HEADER
MAIN

With areas, in css grid-template-areas this can be made even simpler:

App() do


    sidebar = DemoCard(
        "SIDEBAR",
        style = Styles("grid-area" =>  "sidebar")
    )


    header = DemoCard(
        "HEADER",
        style = Styles("grid-area" =>  "header")
    )


    main = DemoCard(
        "MAIN",
        style = Styles("grid-area" =>  "main")
    )


    grid = Grid(
        sidebar, header, main,
        columns = "2fr 5fr",
        rows = "50px 1fr",
        areas = """
            'sidebar header'
            'sidebar main';
        """
    )
    return DOM.div(grid; style=Styles("height" => "600px", "margin" => "20px", "position" => :relative))
end
SIDEBAR
HEADER
MAIN

The syntax is quite similar to Julia's matrix syntax, just wrapping all rows into '...row...'! To span multiple rows or columns, the name can be repeated multiple times.

Alignment

App() do
    grid = Grid(
        DemoCard(), DemoCard(),
        columns = "90px 90px",
    )
    return DOM.div(grid; style=Styles("height" => "200px", "margin" => "20px", "position" => :relative, "background-color" => "#F88379", "padding" => "5px"))
end
App() do session
    justification = Bonito.Dropdown(["space-evenly", "center", "end", "space-between", "space-around", "space-evenly"], style=Styles("width" => "200px"))


    grid = Grid(
        DemoCard(), DemoCard(),
        columns = "90px 90px"
    )
    onjs(session, justification.option_index, js""" (idx) => {
        grid = $(grid)
        grid.style["justify-content"] = $(justification.options[])[idx-1]
    }""")
    area_style = Styles("height" => "200px", "width" => "600px", "margin" => "20px", "position" => :relative, "background-color" => "#F88379", "padding" => "5px")
    grid_area = DOM.div(grid; style=area_style)
    return Grid(justification, grid_area; justify_items="center")
end
App() do session
    content = Bonito.Dropdown(["space-evenly", "center", "end", "space-between", "space-around"])
    items = Bonito.Dropdown(["stretch", "start", "center", "end"])
    grid = Grid(
        DemoCard(), DemoCard(),
        DemoCard(), DemoCard(),
        columns = "90px 90px"
    )
    onjs(session, content.option_index, js""" (idx) => {
        grid = $(grid)
        const val = $(content.options[])[idx-1]
        grid.style["justify-content"] = val
    }""")
    onjs(session, items.option_index, js""" (idx) => {
        grid = $(grid)
        const val = $(items.options[])[idx-1]
        grid.style["justify-items"] = val
    }""")
    grid_area = DOM.div(grid; style=Styles("height" => "200px", "width" => "600px", "margin" => "20px", "position" => :relative, "background-color" => "#F88379", "padding" => "5px",
        "grid-column" => "1 / 3", "grid-row" => "2"))
    return Grid(content, items, grid_area; width="500px", justify_items="space-around")
end
App() do session
    content = Bonito.Dropdown(["space-evenly", "center", "end", "space-between", "space-around"])
    items = Bonito.Dropdown(["stretch", "start", "center", "end"])
    align_content = Bonito.Dropdown(["space-evenly", "center", "end", "space-between", "space-around"])
    align_items = Bonito.Dropdown(["stretch", "start", "center", "end"])
    grid_style = Styles("position" => :absolute, "top" => 0, "left" => 0)
    grid = Grid(
        centered("One"), centered("Two"),
        centered("Three"), centered("Four"),
        columns = "100px 100px",
        rows = "100px 100px",
        style=grid_style
    )


    grid_col() = DOM.div(style=Styles("border" => "2px dashed white", "width"=>"100px"))
    grid_row() = DOM.div(style=Styles("border" => "2px dashed white", "height"=>"100px"))


    shadow_cols = Grid(
        grid_col(), grid_col(),
        columns = "100px 100px",
        style=grid_style
    )
    shadow_rows = Grid(
        grid_row(), grid_row(),
        rows = "100px 100px",
        style=grid_style
    )


    onjs(session, content.option_index, js""" (idx) => {
        const grids = [$(grid), $(shadow_cols)]
        const val = $(content.options[])[idx-1]
        grids.forEach(x=> x.style["justify-content"] = val)
    }""")


    onjs(session, items.option_index, js""" (idx) => {
        const grids = [$(grid), $(shadow_cols)]
        const val = $(items.options[])[idx-1]
        grids.forEach(x=> x.style["justify-items"] = val)
    }""")


    onjs(session, align_content.option_index, js""" (idx) => {
        const grids = [$(grid), $(shadow_rows)]
        const val = $(align_content.options[])[idx-1]
        grids.forEach(x=> x.style["align-content"] = val)
    }""")


    onjs(session, align_items.option_index, js""" (idx) => {
        const grids = [$(grid), $(shadow_rows)]
        const val = $(align_items.options[])[idx-1]
        grids.forEach(x=> x.style["align-items"] = val)
    }""")


    grid_area = DOM.div(grid, shadow_cols, shadow_rows; style=Styles(
        "height" => "400px", "width" => "400px",
        "margin" => "20px",
        "padding" => "5px",
        "position" => :relative,
        "background-color" => "#F88379",
        "grid-column" => "1 / 3", "grid-row" => "4"))


    text(t) = DOM.div(t; style=Styles("font-size" => "1.3rem", "font-weight" => "bold"))
    final_grid = Grid(
        text("Row Alignment"), text("Col Justification"),
        align_content, content,
        align_items, items,
        grid_area;
        rows = "2rem 1rem 2rem 1fr",
        align_items = "center",
        justify_items="begin", justify_content="center",
        width="400px")


    return DOM.div(final_grid, style=Styles("padding" => "20px"))
end
Row Alignment
Col Justification

One

Two

Three

Four