One of the benefits of creating a user interface in JavaFX is its ability to run on multiple devices. But how do you make sure your app looks great on every device – even between screen sizes?
The simple answer is to use a responsive layout. That means multiple, often very different views for different screen-sizes and displays.
JavaFX doesn’t support responsive layouts natively. However, by using the layout panes already provided by JavaFX, and with a few simple lines of code, it’s possible to make JavaFX completely responsive.
All it takes is 200 lines of code – and you get them all for free (obviously).
A few reasons, but when it was created in 2007, the same year the original iPhone came out, optimising your layout for phones was not a priority..
pre-2007…
Even the Apple website wasn’t responsive.
Pay particular attention to the awful wrapping behaviour of the headers…

…and now…
Apple now go for a simple, responsive design with a giant Hero image.

The Options For Making A Responsive JavaFX
Any attempt to make JavaFX behave responsively must start by registering a listener to the widthProperty
of the Stage
object. Changes to the view are then be initiated using conditional logic based on the new width of the Stage.
We can actually initiate changes in a number of ways. The absolute most complicated way to do this is to create custom code that rearranges your scene.
Manually rearranging contents
In a simple implementation, responsive behaviour can be generated by manually re-building the view, either by reassigning the contents between multiple containers, or by loading an alternative view using FXML.
This can be effective if the view size is unlikely to change once the user has started. However, it’s really inefficient for use cases where the screen size will change a lot.
On top of that, it might work well on a simple use case, but when you try to roll it out to a crowded scene, the complexity suddenly sky-rockets. If your UI has more than 2 or 3 responsive components, you may spend more code shifting elements than you do defining useful behaviour.
Instead, I think the sensible approach is to define the behaviour first, independent of scene complexity.
Automated responsive behaviour
Overall, it’s more efficient to manage responsive behaviour by designing a layout pane that handles the logic of rearranging the components within it for you.
There are a few ways to do that. Possibly the cleanest (but not the easiest) is to create a completely custom layout pane. The Pane
class is extensible, and you’re welcome to use the algorithms below to work through the layout logic yourself. However, there are a lot of tricky layout features like margins and padding to be aware of when creating a completely new layout pane.
A simpler approach (and the one I’ll use here) is to extend one of the existing layout panes, baking in the behaviour using its already well-coded layout logic. That way, you don’t have to worry about coding out CSS styling, or handling padding and insets.
Making JavaFX behave like a Bootstrap Grid
JavaFX can be easily modified to simulate the behaviour of Twitter Bootstrap by replicating the grid
, row
and column
elements of the Bootstrap Grid system. The grid object should extend a JavaFX layout such as a GridPane
, whilst additional classes should be created to handle row and column behaviours.
I’ll walk through how we’ll define each below, but this is the basic behaviour we’ll achieve. Each coloured block represents a column within the grid.
The behaviours we’ll replicate in Java code
The Bootstrap Grid system (now at version 5) is fairly mature, so it has a lot of custom behaviours like modifiable visibility and alignment.
What I want to replicate here are just the layout behaviours – how to get something to be a different width at varying screen or window sizes. The basic layout elements of the grid system, which we’ll be mimicking, implement the following behaviours:
- The system is defined by one Grid, which may have one or more rows.
- Rows are laid out vertically within the grid in the order they are added.
- Each row contains one or more columns,
- Columns have a width defined from 1 to 12, where 12 represents 100% of the width of the Bootstrap Grid.
- If a row has columns with a total width of greater than 12, the contents wrap.
- Columns are usually containers rather than components. Containers can be empty.
- The width of a column may be defined for multiple screen sizes (there are 5 screen sizes (XS, S, M, L, XL).
- If no behaviour is defined for one screen size, behaviour is searched for in order of decreasing size.
Creating Grid, Row and Column Objects in Java
Alright, we’re going to create 3 Java objects – I won’t repeat what they are because your eyes are probably bleeding from the repetition. But here are the steps:
- The
grid
is going to lay out the rows from top to bottom

- The
row
is going to lay out columns from left to right, wrapping where necessary

- The
column
is going to keep track of how wide it’s supposed to be at each breakpoint.

Once that’s all sorted, we should be able to add objects, and have the system handle how to place them without any further input.
1. Grid
It’s almost too simple, but to replicate the behaviour of the Bootstrap Grid, I’m going to extend the GridPane
layout pane. To make it work, we’ll need:
- Default top-center alignment
- A
GridPane
with 12 columns of equal width. - A register on the width property that rearranges nodes when the breakpoint changes
public class BootstrapPane extends GridPane { private Breakpoint currentWindowSize = Breakpoint.XSMALL; public BootstrapPane() { super(); setAlignment(Pos.TOP_CENTER); setColumnConstraints(); setWidthEventHandlers(); } private void setWidthEventHandlers() { this.widthProperty().addListener((observable, oldValue, newValue) -> { Breakpoint newBreakpoint = Breakpoint.XSMALL; if (newValue.doubleValue() > 576) newBreakpoint = Breakpoint.SMALL; if (newValue.doubleValue() > 768) newBreakpoint = Breakpoint.MEDIUM; if (newValue.doubleValue() > 992) newBreakpoint = Breakpoint.LARGE; if (newValue.doubleValue() > 1200) newBreakpoint = Breakpoint.XLARGE; if (newBreakpoint != currentWindowSize) { currentWindowSize = newBreakpoint; calculateNodePositions(); } }); } private void setColumnConstraints() { //Remove all current columns. getColumnConstraints().clear(); //Create 12 equally sized columns for layout double width = 100.0 / 12.0; for (int i = 0; i < 12; i++) { ColumnConstraints columnConstraints = new ColumnConstraints(); columnConstraints.setPercentWidth(width); getColumnConstraints().add(columnConstraints); } } private void calculateNodePositions() { int currentGridPaneRow = 0; for (BootstrapRow row : rows) { currentGridPaneRow += row.calculateRowPositions(currentGridPaneRow, currentWindowSize); } } }
Here, the constructor sets the alignment as Pos.TOP_CENTER
Then, it calls setColumnConstraints()
, which creates 12 columns of equal width. This is perfect for a bootstrap grid.
Finally, it registers an event handler on its own widthProperty to initiate changes to its layout structure when the width of the window passes a breakpoint.
Note that all calculateNodePositions()
is going to do is invoke each BootstrapRow
to layout its BootstrapColumn
objects. It will be triggered every time the BootstrapPane
detects the current window size (known as a breakpoint) has changed.
We’ll also need methods to add and remove BootstrapRow
objects as they’re added and removed.
private final List<BootstrapRow> rows = new ArrayList<>(); public void addRow(BootstrapRow row) { if (rows.contains(row)) return; //prevent duplicate children error rows.add(row); calculateNodePositions(); for (BootstrapColumn column : row.getColumns()) { getChildren().add(column.getContent()); GridPane.setFillWidth(column.getContent(), true); GridPane.setFillHeight(column.getContent(), true); } } public void removeRow(BootstrapRow row) { rows.remove(row); calculateNodePositions(); for (BootstrapColumn column : row.getColumns()) { getChildren().remove(column.getContent()); } }
2. Row
The key behaviour in this implementation is to take the incoming GridPane row from the row before it, layout all of its contents and return the new GridPane row position – essentially keeping track of where we are.
public class BootstrapRow { public int calculateRowPositions(int lastGridPaneRow, Breakpoint currentWindowSize) { int inputRow = lastGridPaneRow; if (this.getColumns().isEmpty()) return 0; int currentGridPaneColumn = 0; //start in the first column for (BootstrapColumn column : this.getColumns()) { int contentWidth = column.getColumnWidth(currentWindowSize); if (currentGridPaneColumn + contentWidth > 12) { lastGridPaneRow++; currentGridPaneColumn = 0; } GridPane.setConstraints( column.getContent(), currentGridPaneColumn, lastGridPaneRow, contentWidth, 1 ); currentGridPaneColumn += contentWidth; } return lastGridPaneRow - inputRow + 1; } }
Now, the BootstrapRow
is really just a middle manager – I want the BootstrapPane
to be able to actually display the columns. So we need to give the BootstrapPane access to the columns.
Then, when a row is added, it can add all the columns as children to allow JavaFX to display them in the scene.
public List<BootstrapColumn> getColumns(){ return Collections.unmodifiableList(columns); }
I don’t want users to be able to modify the list directly, but I’m also wildly allergic to package-private statements unless they’re completely necessary. To get around that, I’ve used the Collections.unmodifiableList
wrapper. There are other ways to do it, but I’ve chosen that one.
Now, from a user’s perspective, the row will need methods to allow users to add and remove BootstrapColumn
objects.
private final List<BootstrapColumn> columns = new ArrayList<>(); public void addColumn(BootstrapColumn column){ if(column == null) return; columns.add(column); } public void removeColumn(BootstrapColumn column){ columns.remove(column); } public void clear(){ columns.clear(); //remove all columns }
3. Column
Finally, we need a column that keeps track of how wide it’s meant to be, and feed that information back to it’s associated BootstrapRow
when prompted.
Again, I’m going to use a pretty simple implementation – we’ll store the column widths in an array, and set/unset the values (with -1 indicating an unset value) as needed. For simplicity, default column width is 1.
public class BootstrapColumn { private final Node content; int[] columnWidths = new int[]{ 1, //XS (default) -1, //Sm -1, //Md -1, //Lg -1 //XL }; public BootstrapColumn(Node content) { this.content = content; } public void setBreakpointColumnWidth(Breakpoint breakPoint, int width) { columnWidths[breakPoint.getValue()] = MathUtils.clamp(width, 1, 12); } public void unsetBreakPoint(Breakpoint breakPoint) { columnWidths[breakPoint.getValue()] = -1; } public void unsetAllBreakPoints() { this.columnWidths = new int[]{ 1, //XS (default) -1, //Sm -1, //Md -1, //Lg -1 //XL }; } public int getColumnWidth(Breakpoint breakPoint) { //Iterate through breakpoints, beginning at the specified bp, travelling down. Return first valid bp value. for (int i = breakPoint.getValue(); i >= 0; i--) { if (isValid(columnWidths[i])) return columnWidths[i]; } //If none are valid, return 1 return 1; } public Node getContent() { return content; } private boolean isValid(int value) { return value > 0 && value <= 12; } }
4. Breakpoints
I know, I know, I said we were going to create three objects, but to create a simple, type-safe way to link a breakpoint with its position in the BootstrapColumn
‘s columnWidths
array.
The following enum allows for five named breakpoints, with their corresponding list position.
public enum Breakpoint { XSMALL(0), SMALL(1), MEDIUM(2), LARGE(3), XLARGE(4); private int value; Breakpoint(int value) { this.value = value; } public int getValue() { return value; } }
The result:
The resulting logic allows us to create and define columns, setting their widths by invoking setBreakpointColumnWidth()
, and then load them into rows and grids that will then automatically adjust their position and size based on a clear set of pre-defined rules.
The following quick build is a really simple demonstration of the type of behaviour you can achieve using really small amounts of code.
package com.edencoding; import com.edencoding.layouts.BootstrapColumn; import com.edencoding.layouts.BootstrapPane; import com.edencoding.layouts.BootstrapRow; import com.edencoding.layouts.Breakpoint; import javafx.application.Application; import javafx.geometry.Insets; import javafx.geometry.Orientation; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.control.Separator; import javafx.scene.image.Image; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.shape.Circle; import javafx.stage.Stage; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.List; import java.util.Timer; import java.util.TimerTask; public class ToDoResponsive extends Application { public static void main(String[] args) { launch(args); } @Override public void start(Stage primaryStage) { BootstrapPane root = makeView(); root.getStylesheets().add( getClass().getResource("/css/styles.css").toExternalForm()); ScrollPane scrollPane = new ScrollPane(root); scrollPane.setFitToWidth(true); scrollPane.setFitToHeight(true); scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); primaryStage.setTitle("Responsive ToDo List"); primaryStage.setScene(new Scene(scrollPane, 300, 650)); primaryStage.getIcons().add(new Image( getClass().getResourceAsStream("/img/EdenCodingIcon.png") )); primaryStage.show(); } private BootstrapPane makeView() { BootstrapPane bootstrapPane = new BootstrapPane(); bootstrapPane.setPadding(new Insets(15)); bootstrapPane.getStyleClass().add("background"); bootstrapPane.setVgap(25); bootstrapPane.setHgap(25); BootstrapRow row = new BootstrapRow(); row.addColumn(createTitleColumn()); row.addColumn(createColumn(createWidget("Do now", seedUrgentImportant()))); row.addColumn(createColumn(createWidget("Do later", seedImportant()))); row.addColumn(createColumn(createWidget("Delegate", seedUrgent()))); row.addColumn(createColumn(createWidget("Avoid if possible", seedUnimportant()))); bootstrapPane.addRow(row); return bootstrapPane; } private BootstrapColumn createTitleColumn() { Label title = new Label("To Do List"); //style title.getStyleClass().add("app-title"); //add to column BootstrapColumn titleColumn = new BootstrapColumn(title); titleColumn.setBreakpointColumnWidth(Breakpoint.XSMALL, 12); return titleColumn; } private BootstrapColumn createColumn(Node widget) { BootstrapColumn column = new BootstrapColumn(widget); column.setBreakpointColumnWidth(Breakpoint.XSMALL, 12); column.setBreakpointColumnWidth(Breakpoint.SMALL, 6); column.setBreakpointColumnWidth(Breakpoint.LARGE, 3); return column; } private Node createWidget(String title, List<ToDo> items) { VBox widget = new VBox(); widget.getStyleClass().add("widget"); widget.getChildren().add(new Label(title)); widget.getChildren().add(new Separator(Orientation.HORIZONTAL)); for (ToDo todo : items) { widget.getChildren().add(createItem(todo)); } return widget; } private Node createItem(ToDo todo) { HBox item = new HBox(); item.getStyleClass().add("item"); HBox left = new HBox(); HBox.setHgrow(left, Priority.ALWAYS); left.getChildren().add(new Label(todo.title)); HBox right = new HBox(); right.setSpacing(15); right.setMinWidth(80); right.setAlignment(Pos.CENTER_RIGHT); HBox.setHgrow(right, Priority.NEVER); right.getChildren().add(new Label(todo.dueBy.format(DateTimeFormatter.ofPattern("dd-MMM")))); right.getChildren().add(new Circle(5, todo.status)); item.getChildren().addAll(left, right); return item; } private List<ToDo> seedUrgentImportant() { return Arrays.asList( new ToDo("Feed my kids", LocalDate.now(), Color.GREEN), new ToDo("Gym", LocalDate.now().plusDays(1), Color.ORANGERED), new ToDo("Weekly shop", LocalDate.now().plusDays(3), Color.RED) ); } private List<ToDo> seedImportant() { return Arrays.asList( new ToDo("Create bootstrap responsive grid", LocalDate.now(), Color.GREEN), new ToDo("Write EdenCoding article", LocalDate.now(), Color.ORANGERED) ); } private List<ToDo> seedUrgent() { return Arrays.asList( new ToDo("Schedule dental work", LocalDate.now(), Color.GREEN), new ToDo("Book hotel for conference", LocalDate.now(), Color.GREEN) ); } private List<ToDo> seedUnimportant() { return Arrays.asList( new ToDo("Drink more water", LocalDate.now(), Color.RED), new ToDo("Buy stuff you don't need", LocalDate.now(), Color.GREEN), new ToDo("Check Facebook", LocalDate.now(), Color.GREEN) ); } private class ToDo { String title; LocalDate dueBy; Color status; ToDo(String title, LocalDate dueBy, Color status) { this.title = title; this.dueBy = dueBy; this.status = status; } } }
The full code can be found in my responsive-javafx github repo, including the style sheets used to create the dark-style design.
Conclusions
Creating a responsive JavaFX feel is much simpler than it seems. In 4 classes and just over 200 lines of code, it’s possible to completely reproduce the simple layout behaviours demonstrated by Twitter Bootstrap.
With a little more code, the visibility and alignment features could be added for a complete Flex-box replica within JavaFX.
Hopefully this gave you some inspiration to think about ways you could stretch JavaFX across multiple devices. If you have any ideas for how to improve the features in the code I’ve written, please reach out using the contact form in the right hand menu, I’d love to hear from you.