Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Java  Module 18 – Desktop GUIs with JavaFX (and Swing for legacy code)

JavaFX FXML, controllers, CSS and Scene Builder

Move a JavaFX layout into FXML with a controller, style it with JavaFX CSS and dark-mode media queries, and design screens in Scene Builder.

  • Intermediate
  • 24 minutes
  • Examples run with Java 25.0.4 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 9 of 24 minutes free
Free: From Java code to FXML and a controller · JavaFX CSS in one stylesheet · Scene Builder, and when FXML pays off. With a pass: 2 in-depth parts and 3 interview answers, about 15 minutes.

What you will learn

  • Move layout into FXML with a controller that uses @FXML fields and handler methods
  • Design an application's look with JavaFX CSS stylesheets, pseudo-classes and media queries
  • Design screens in Scene Builder and review the FXML it writes like any other source file

Before you start

What you’ll learn · Before you start

What you will learn

  • Move layout into FXML with a controller that uses @FXML fields and handler methods
  • Design an application's look with JavaFX CSS stylesheets, pseudo-classes and media queries
  • Design screens in Scene Builder and review the FXML it writes like any other source file

Before you start

On this page

JavaFX FXML is an XML file that describes a screen: its panes, its controls and which method handles which event. An FXMLLoader reads the file, builds those nodes and creates the screen’s controller, a plain Java class whose @FXML fields receive the nodes with a matching fx:id and whose methods handle events named like onAction="#register". Because controllers are ordinary classes, they can be tested and reused like any other code. CSS decides the look: JavaFX CSS rules with -fx- properties, in stylesheets added to a scene or a pane, style the nodes by class, id and state, and since JavaFX 25 a media query can switch to a dark theme. Scene Builder, Gluon’s open-source visual editor, lets you lay screens out by dragging and writes the FXML for you. Together they split a screen into three files: the layout in FXML, the look in CSS and the behaviour in the controller.

From Java code to FXML and a controller

FXMLLoader reads an FXML file, builds the nodes, creates and fills the controller, and stylesheets style the result.Registration.fxmlelements, fx:id, onAction="#register"FXMLLoader.load()Scene graph + controllerready to showStylesheetsModena, then yours1. creates the controller(fx:controller)2. builds the nodes3. injects @FXML fieldsby fx:id4. wires #registerto the method5. calls initialize()applied beforethe next frame

What FXMLLoader does when it loads a file

Text description of the diagram

The diagram runs from top to bottom.

"Registration.fxml", with its elements, fx:id attributes and onAction="#register", goes into FXMLLoader.load(), which works in five steps:

  1. it creates the controller named by fx:controller on the root element;
  2. it builds the nodes the elements describe;
  3. it injects the nodes into the controller's @FXML fields, matching each fx:id to a field name;
  4. it wires the handler #register to the controller's register method;
  5. it calls the controller's initialize() method.

The result is a scene graph and a controller, ready to show. A separate box, "Stylesheets: Modena, then yours", points at the result: the styles are applied before the next frame is drawn.

Here is a course registration form as FXML, with its controller. The controller prints when its constructor and its initialize() method run:

Registration.fxml and RegistrationController.java

Registration.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.VBox?>

<!-- A course registration form: two fields, a button and a message, wired to RegistrationController. -->
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
      fx:controller="RegistrationController" spacing="12" styleClass="form">
  <padding><Insets top="16" right="16" bottom="16" left="16"/></padding>
  <Label text="Course registration" styleClass="title"/>
  <GridPane hgap="8" vgap="8">
    <Label text="_Name" mnemonicParsing="true" labelFor="$name" GridPane.rowIndex="0" GridPane.columnIndex="0"/>
    <TextField fx:id="name" promptText="As on your ID card" GridPane.rowIndex="0" GridPane.columnIndex="1"/>
    <Label text="_Branch" mnemonicParsing="true" labelFor="$branch" GridPane.rowIndex="1" GridPane.columnIndex="0"/>
    <ComboBox fx:id="branch" GridPane.rowIndex="1" GridPane.columnIndex="1"/>
  </GridPane>
  <Button fx:id="register" text="Register" defaultButton="true" onAction="#register"/>
  <Label fx:id="message"/>
</VBox>

RegistrationController.java

import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;

/** The controller of Registration.fxml: FXMLLoader creates it and fills the @FXML fields from the fx:id attributes. */
public class RegistrationController {
    @FXML private TextField name;
    @FXML private ComboBox<String> branch;
    @FXML private Button register;
    @FXML private Label message;

    /** Whether the constructor and initialize() say when they run (LoadCost.java turns it off). */
    static boolean printSteps = true;

    public RegistrationController() {
        if (printSteps) {
            System.out.println("  constructor: name field is " + name); // nothing is injected yet
        }
    }

    /** Called once, after every @FXML field has been injected. */
    @FXML
    private void initialize() {
        if (printSteps) {
            System.out.println("  initialize(): name field is a " + name.getClass().getSimpleName());
        }
        branch.getItems().setAll("Computer Engineering", "Information Technology", "Electrical Engineering");
        branch.getSelectionModel().selectFirst();
        register.disableProperty().bind(name.textProperty().isEmpty());
    }

    /** The handler that onAction="#register" names. */
    @FXML
    private void register() {
        message.setText("Welcome " + name.getText().strip() + ", " + branch.getValue());
    }
}

ShowForm.java loads the file, types a name and presses Register. Each program is launched straight from its source file: fx.args lists the module path of the JavaFX 27 SDK and the modules to load, here with javafx.fxml, and FxRun starts the toolkit without an Application subclass:

ShowForm.java and its helpers

ShowForm.java

import java.nio.file.Path;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;

/** Loads Registration.fxml, prints what the loader built, then types a name and presses Register. */
public class ShowForm {
    public static void main(String[] args) throws Exception {
        FxRun.start();
        FxRun.onFx(() -> {
            try {
                System.out.println("loading Registration.fxml");
                FXMLLoader loader = new FXMLLoader(Path.of("Registration.fxml").toUri().toURL());
                Parent root = loader.load();
                System.out.println("root: " + root.getClass().getSimpleName() + " with " + root.getChildrenUnmodifiable().size() + " children; controller: " + loader.getController().getClass().getSimpleName());
                Button register = (Button) loader.getNamespace().get("register"); // every fx:id is also in the namespace
                System.out.println("Register disabled while the name is empty: " + register.isDisabled());
                ((TextField) loader.getNamespace().get("name")).setText("Kabir Mehta");
                register.fire();
                System.out.println("after typing and pressing Register: " + ((Label) loader.getNamespace().get("message")).getText());
            } catch (java.io.IOException e) {
                throw new java.io.UncheckedIOException(e);
            }
        });
        Platform.exit();
    }
}

FxRun.java

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javafx.application.Platform;

/** Starts JavaFX without an Application subclass and runs code on the JavaFX Application Thread. */
final class FxRun {
    private FxRun() {
    }

    /** Starts the JavaFX runtime and waits until its thread is running. */
    static void start() throws InterruptedException {
        CountDownLatch started = new CountDownLatch(1);
        Platform.startup(started::countDown);
        started.await();
    }

    /** Computes a value on the JavaFX Application Thread and returns it. */
    static <T> T call(java.util.function.Supplier<T> task) throws InterruptedException {
        Object[] result = new Object[1];
        onFx(() -> result[0] = task.get());
        @SuppressWarnings("unchecked")
        T value = (T) result[0];
        return value;
    }

    /** Runs the task on the JavaFX Application Thread and waits for it, at most 10 seconds. */
    static void onFx(Runnable task) throws InterruptedException {
        CountDownLatch done = new CountDownLatch(1);
        Platform.runLater(() -> {
            try {
                task.run();
            } finally {
                done.countDown();
            }
        });
        if (!done.await(10, TimeUnit.SECONDS)) {
            throw new IllegalStateException("the JavaFX Application Thread did not answer");
        }
    }
}

fx.args

--module-path javafx-sdk-27/lib
--add-modules javafx.controls,javafx.fxml
--enable-native-access=javafx.graphics
-Dglass.platform=headless
-Dprism.order=sw
Loading the form and pressing Register Python · form_session.py
from session import javafx, run

javafx()
run("""
java @fx.args ShowForm.java
""")

Output

$ java @fx.args ShowForm.java
loading Registration.fxml
  constructor: name field is null
  initialize(): name field is a TextField
root: VBox with 4 children; controller: RegistrationController
Register disabled while the name is empty: true
after typing and pressing Register: Welcome Kabir Mehta, Computer Engineering

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 form_session.py

The order matters. The loader created the controller first, when no field was set yet, then injected the nodes into the @FXML fields by name, and only then called initialize(), which is why setup code that touches the controls belongs there and not in the constructor. The fields are private: @FXML lets the loader reach them anyway. The handler register is private too, and onAction="#register" found it by name. labelFor="$name" ties each label to its field, so the keyboard mnemonic, Alt+N on Windows and Linux, moves the focus there and a screen reader announces the label with the field.

JavaFX CSS in one stylesheet

app.css gives the form one brand colour, named once and used by name, and sets how far the Register button fades while it is disabled:

app.css CSS · app.css
/* The application's stylesheet: one brand colour, used by name everywhere else. */
.root {
    -brand: #0b6e4f;
    -fx-font-size: 14px;
}

.title {
    -fx-font-size: 20px;
    -fx-font-weight: bold;
}

.button {
    -fx-background-color: -brand;
    -fx-text-fill: white;
}

/* Modena fades disabled controls to 0.4; this form fades its buttons less, so their text stays easier to read. */
.button:disabled {
    -fx-opacity: 0.6;
}

#message {
    -fx-text-fill: -brand;
}
Styled.java Java · Styled.java
import java.nio.file.Path;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.stage.Stage;

/** Loads the form, adds app.css to its scene, and prints the values CSS gave three nodes. */
public class Styled {
    public static void main(String[] args) throws Exception {
        FxRun.start();
        FxRun.onFx(() -> {
            try {
                FXMLLoader loader = new FXMLLoader(Path.of("Registration.fxml").toUri().toURL());
                Parent root = loader.load();
                Scene scene = new Scene(root, 360, 240);
                scene.getStylesheets().add(Path.of("app.css").toUri().toString());
                Stage stage = new Stage();
                stage.setScene(scene);
                stage.show();
                root.applyCss(); // CSS normally runs before the next frame; this applies it now

                Label title = (Label) root.lookup(".title");
                Button register = (Button) loader.getNamespace().get("register");
                Label message = (Label) loader.getNamespace().get("message");
                System.out.println("title: " + title.getFont().getSize() + " px, " + title.getFont().getStyle());
                System.out.println("register: background " + register.getBackground().getFills().get(0).getFill()
                        + ", text " + register.getTextFill() + ", disabled " + register.isDisabled() + ", opacity " + register.getOpacity());
                ((TextField) loader.getNamespace().get("name")).setText("Kabir Mehta");
                root.applyCss();
                System.out.println("with a name typed: disabled " + register.isDisabled() + ", opacity " + register.getOpacity());
                System.out.println("message text colour: " + message.getTextFill());
            } catch (java.io.IOException e) {
                throw new java.io.UncheckedIOException(e);
            }
        });
        Platform.exit();
    }
}
What the stylesheet gave three nodes Python · styled_session.py
from session import javafx, run

javafx()
run("""
java @fx.args Styled.java
""")

Output

$ java @fx.args Styled.java
  constructor: name field is null
  initialize(): name field is a TextField
title: 20.0 px, Bold
register: background 0x0b6e4fff, text 0xffffffff, disabled true, opacity 0.6
with a name typed: disabled false, opacity 1.0
message text colour: 0x0b6e4fff

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 styled_session.py

JavaFX CSS looks like web CSS, with differences that matter: properties carry the -fx- prefix, -brand is a looked-up colour that any rule below .root can use by name, and pseudo-classes describe a node’s state, here :disabled. Every application starts from Modena, the stylesheet that ships with JavaFX, and your stylesheets add to it: the disabled button got the 0.6 of app.css rather than the 0.4 that Modena gives disabled controls. Once a name was typed and the binding enabled the button, the rule stopped matching and the opacity went back to 1.0, without a line of Java code.

Scene Builder, and when FXML pays off

Scene Builder 26.0.0, the release on Gluon’s download page, is an open-source editor under a BSD licence, with installers for Windows, macOS and Linux: you drag panes and controls onto a canvas, set their properties and fx:ids, attach stylesheets, and it saves FXML. The FXML it writes is source code, and it deserves the same review as Java code. FXML also has a cost, as this measurement shows:

LoadCost.java Java · LoadCost.java
import java.net.URL;
import java.nio.file.Path;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;

/** What FXML costs: the same form loaded from Registration.fxml and built in Java code, first time and on average. */
public class LoadCost {
    static Parent inCode() {
        TextField name = new TextField();
        ComboBox<String> branch = new ComboBox<>();
        GridPane grid = new GridPane(8, 8);
        grid.addRow(0, new Label("_Name"), name);
        grid.addRow(1, new Label("_Branch"), branch);
        VBox form = new VBox(12, new Label("Course registration"), grid, new Button("Register"), new Label());
        form.setPadding(new Insets(16));
        return form;
    }

    public static void main(String[] args) throws Exception {
        URL fxml = Path.of("Registration.fxml").toUri().toURL();
        RegistrationController.printSteps = false;
        FxRun.start();
        FxRun.onFx(() -> {
            try {
                long t = System.nanoTime();
                new FXMLLoader(fxml).load();
                long firstFxml = System.nanoTime() - t;
                t = System.nanoTime();
                inCode();
                long firstCode = System.nanoTime() - t;
                int n = 200;
                t = System.nanoTime();
                for (int i = 0; i < n; i++) {
                    new FXMLLoader(fxml).load();
                }
                double fxmlEach = (System.nanoTime() - t) / 1e6 / n;
                t = System.nanoTime();
                for (int i = 0; i < n; i++) {
                    inCode();
                }
                double codeEach = (System.nanoTime() - t) / 1e6 / n;
                System.out.printf("first time: FXML %d ms, Java code %d ms%n", firstFxml / 1_000_000, firstCode / 1_000_000);
                System.out.printf("then, on average over %d: FXML %.2f ms, Java code %.2f ms%n", n, fxmlEach, codeEach);
            } catch (java.io.IOException e) {
                throw new java.io.UncheckedIOException(e);
            }
        });
        Platform.exit();
    }
}
The same form from FXML and from Java code Python · cost_session.py
from session import javafx, run

javafx()
run("""
javac --module-path javafx-sdk-27/lib --add-modules javafx.controls,javafx.fxml -d out LoadCost.java FxRun.java RegistrationController.java
java @fx.args -cp out LoadCost
""")

Output

$ javac --module-path javafx-sdk-27/lib --add-modules javafx.controls,javafx.fxml -d out LoadCost.java FxRun.java RegistrationController.java
$ java @fx.args -cp out LoadCost
first time: FXML 573 ms, Java code 7 ms
then, on average over 200: FXML 14.08 ms, Java code 0.32 ms

This output changes from run to run: The times change from run to run with the load on the computer.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cost_session.py

JavaFX FXML with a controller Java code
Layout readable as a tree; edited in Scene Builder mixed with behaviour unless you keep them apart
Mistakes in names found when the file is loaded found by the compiler
Cost in this run several hundred ms the first time, 10 to 15 ms per load after a few ms the first time, under 1 ms after
Screens built at run time from data awkward natural

The first FXML load of a run paid for loading the FXML machinery itself; later loads of the same form cost 10 to 15 milliseconds each, against well under one millisecond in Java code. That is nothing for a screen a user opens, and too much for a row of a list repeated hundreds of times. A controller factory lets the loader build controllers that need arguments, such as a service. fx:include reuses one FXML file inside another, with its own controller. Exporting and opening a package to javafx.fxml lets FXML work in a named module.

Check yourself

4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 4 Put what FXMLLoader does with a document and its controller in order.

    Give each item its position, from 1 (first).

    Show the answer to question 1

    Answer:

    1. It builds the nodes the elements describe and creates the controller.
    2. It injects the nodes into the @FXML fields whose names match their fx:id.
    3. It calls the controller's initialize() method.

    The controller exists before any field is set, which is why its constructor saw a null field in the run; the loader injects the fields, and initialize() runs last, when every field is set.

  2. Question 2 of 4 A controller method @FXML private void register() is named in onAction="#register". What happens?

    Choose one answer.

    Show the answer to question 2

    Answer: The loader wires the button to the method; @FXML lets it reach private members.

    @FXML marks private members the loader may use, and the event parameter is optional, so the private method without parameters is wired to the button.

  3. Question 3 of 4 .button:disabled { -fx-opacity: 0.6; } is in the stylesheet. Code enables a disabled button. What does its opacity become?

    Choose one answer.

    Show the answer to question 3

    Answer: 1.0, because the rule stops matching when the :disabled state ends

    A pseudo-class rule matches only while the state holds; when the button is enabled, CSS recomputes the property and the opacity goes back to its normal value, as the run showed with 0.6 and then 1.0.

  4. Question 4 of 4 Which part of a screen is the weakest case for FXML?

    Choose one answer.

    Show the answer to question 4

    Answer: A list cell repeated hundreds of times, built while the user scrolls

    Each FXML load cost about 10 to 15 ms in the run, against well under a millisecond for the same nodes built in code, which matters only when a piece is built very many times, as cells are.

Controllers in depth, with the failures reproduced Pro: opens with a Pro, Premium or Ultimate pass

FXML moves mistakes from compile time to load time, so knowing what each mistake prints saves hours. This part reproduces six of them, three module setups, controller factories with included files and translated texts, and ends with an exercise.

In this part:

  • Six FXML mistakes and the messages they print
  • Modules: exports, opens and the three messages
  • Controller factories, included files and translated texts
  • Your turn: move a hand-coded screen into FXML

9 runnable examples (Address.fxml, AddressController.java, CheckFee.java, FeeController.java, FeeForm.fxml, Fees.java, HandCoded.java, HelloController.java, Java, Main.java, Profile.fxml, ProfileController.java, Python, ShowProfile.java, fee.css, messages_hi.properties, module-info-closed.java, module-info-exported.java, module-info-open.java, starter/FeeController.java, starter/FeeForm.fxml) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Styling with CSS: the cascade, themes and dark mode, measured Pro: opens with a Pro, Premium or Ultimate pass

Most styling bugs come from not knowing which rule wins, and most theme bugs from colours spread across many rules. This part measures the cascade, builds light and dark themes with checked contrast, and reviews FXML that a visual editor wrote.

In this part:

  • Which rule wins: six sources for one colour, measured
  • Light and dark themes with contrast you can check
  • One theme per file with conditional imports
  • Reviewing what a visual editor writes

8 runnable examples (Cascade.java, FxmlReview.java, Imports.java, Python, Themes.java, css/dark.css, css/important.css, css/light.css, css/parent.css, css/scene.css, css/theme.css, css/themes-import.css, review/Enquiry.fxml, review/EnquiryController.java) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    What does FXMLLoader do with fx:controller, fx:id and onAction="#name" when it loads an FXML file?

    It builds the nodes the elements describe and creates an instance of the class named by fx:controller on the root element. For every element with an fx:id it stores the node in the loader's namespace, sets the node's id, and injects it into the controller field with the same name, which may be private if it is annotated with @FXML. An attribute such as onAction="#register" is wired to the controller method of that name, again private if it has @FXML. When the whole document is loaded and injected, the loader calls the controller's initialize() method, so setup that uses the controls goes there, not into the constructor, which runs before any field is set. A wrong handler name or type fails at load time, while an fx:id without a matching field just leaves the field null.

  2. Mid to senior

    A JavaFX app with FXML screens runs from the IDE on the class path. Packaged as a named module, it fails when the first screen loads. A colleague proposes making every @FXML field and handler public. Explain the errors you can expect, the right fix, and why the proposal is wrong.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    To flag an invalid field, a developer calls field.setStyle("-fx-text-fill: red") in one screen and label.setTextFill(Color.RED) in another. After a designer adds a theme stylesheet, the second one stops working and the first one breaks the dark theme. Explain the JavaFX CSS cascade and redesign the error state.

    Model answer, Pro. First step and follow-ups

  4. Senior to staff

    A 40-screen JavaFX app needs a light theme, a dark theme that follows the operating system, and a high-contrast theme users can pick, all without changing screen code. Design where colours live, how themes switch, and how you keep every theme readable as screens change.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • FXML holds the layout, CSS the look and the controller the behaviour.
  • The loader injects @FXML fields by fx:id, wires #handler names and then calls initialize().
  • JavaFX CSS uses -fx- properties, looked-up colours and pseudo-classes such as :disabled.
  • Scene Builder writes FXML; review it like code.
  • FXML costs a load at run time; for a screen a user opens, that cost does not matter.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.