JavaFx has its pros and cons. As for the cons, it still hasn’t managed to implement the ability to control the spacing between letters for the TextFlow component (issue JDK-8092100 is waiting to be resolved for about 14 years now). However, it still has many advantages, such as a cool and extensible CSS engine. This time, I want to show you how to use it to create an application theme changer, which can be achieved by writing just a few lines of code

The entire mechanism of changing the application theme basically comes down to replacing the loaded CSS files for the Scene using the following methods:
scene.getStylesheets().remove("theme1.css");
scene.getStylesheets().add("theme2.css");
And this is where you could stop reading, but if you are looking for a ready-made example solution, read on.
Detailed implementation
As you have noticed, styles are added to scenes, not to applications, so for everything to work properly, you need a mechanism that will store a list of scenes, the selected theme, and a list of available themes. You will also need a class that will store the definition of a given theme.
public class AppThemes {
private final ObservableList<Theme> themes = FXCollections.observableArrayList();
private final ObjectProperty<Theme> selectedTheme = new SimpleObjectProperty<>();
private final Map<Scene, Boolean> scenes = new WeakHashMap<>();
//....
public record Theme(String name, String stylesheet) {
}
}
So I created the AppThemes class and included the above attributes in it. The list of scenes is stored as a WeakHashMap so that the JVM can take care of releasing these objects from memory when the Scene is no longer in use. Finally, I added the theme definition itself, implemented as a simple record with attributes for the name and path to the CSS file.
I will now supplement this class with a list of necessary methods for the mechanism to work.
public class AppThemes {
// ....
public AppThemes() {
themes.addListener((ListChangeListener<Theme>) c -> {
if (!c.next()) {
return;
}
if (c.wasAdded() && selectedTheme.get() == null) {
selectedTheme.set(c.getAddedSubList().getFirst());
} else if (themes.isEmpty()) {
selectedTheme.set(null);
}
});
selectedTheme.addListener((observable, oldValue, newValue) -> {
scenes.keySet().forEach(scene -> applyTheme(scene, newValue));
});
}
public void addScene(Scene scene) {
scenes.put(scene, true);
applyTheme(scene, selectedTheme.get());
}
public AppThemes addThemes(Theme... themes) {
this.themes.addAll(themes);
return this;
}
public ObjectProperty<Theme> selectedThemeProperty() {
return selectedTheme;
}
private void applyTheme(Scene scene, Theme newTheme) {
String customTheme = (String) scene.getProperties().get("customTheme");
if (customTheme != null) {
scene.getStylesheets().remove(customTheme);
}
if (newTheme != null) {
scene.getStylesheets().add(newTheme.stylesheet());
scene.getProperties().put("customTheme", newTheme.stylesheet());
}
}
// ...
}
Starting with the constructor. We listen for new themes to appear. When the first theme appears, it should automatically be set as the selected theme. Then we listen for changes to the selected theme. If a change is triggered, we apply the selected theme to all registered scenes. Next is:
- the method for adding a scene (which will immediately apply the current theme to it after adding),
- the method for registering themes for the application,
- and a property that can be used “from the outside” to find out what the selected theme is and possibly change it to another one.
When applying the selected theme, we expect information about the theme currently applied to the scene. For this purpose, I used an additional property called customTheme
, which will store information about the currently applied CSS file. If the information is available, it means that a theme has already been applied to the scene, which we want to remove before the current one is assigned in the next lines.
To make the theme change mechanism more convenient to use, the current class can be extended with a component that will enable this. Potentially, this is a component that can be available to the user in many places – e.g. in every application window – so it cannot be a single shared component, but rather a method that will create new components.
public class AppThemes {
// ....
public Control createThemeControl() {
Button button = new Button("Switch Theme");
button.setOnAction(event -> {
int index = themes.indexOf(selectedTheme.get());
if (index < themes.size() - 1) {
selectedTheme.set(themes.get(index + 1));
} else {
selectedTheme.set(themes.getFirst());
}
});
return button;
}
// ...
}
The method returns the Control
type without revealing the specific type of the component that will be returned. Below, I have hidden an example implementation of a control as a Button, which, when clicked, will change the theme to the next available one (according to the order in which they were registered). But there is nothing to prevent you from changing it to a ComboBox
for the purposes of your application.
How to use it in an application?
If you want to have one common theme for the entire application, you can, for example, use a global constant in the main application class, which can be easily used in other parts of the application, e.g.:
public class Main {
public static final AppThemes THEMES = new AppThemes()
.addThemes(
new AppThemes.Theme("Light", Main.class.getResource("/css/light-mode.css").toExternalForm()),
new AppThemes.Theme("Dark", Main.class.getResource("/css/dark-mode.css").toExternalForm())
);
// ....
}
Then, in the selected location, add a component to the layout that will allow you to change the theme:
vbox.getChildren().add(Main.THEMES.createThemeControl());
When creating themes, remember not to copy entire CSS files (with replacing specific colors in selected places). Instead, I suggest that the theme should be composed of at least two files, e.g., general.css
, which will contain the general style for components, and light-mode.css
, which will store dedicated settings for a given theme (e.g., a list of color variables). But that’s a topic for another article.
Here you can download the complete source code: