Custom Controls & Reuse
Custom Controls & Reuse
JavaFX ships with a rich library of built-in controls, but real applications almost always need something the library does not provide out of the box: a labelled spinner, a card widget with a header and body, a reusable form row that ties a label to its field and its validation message. This lesson teaches you the mechanics and design decisions involved in building such components so that they can be dropped into any scene just like a Button or a TextField.
Two Approaches: Composition vs. Subclassing
Before writing any code you need to decide on your implementation strategy. JavaFX gives you two clean options.
- Composition (preferred): Extend an existing layout pane (
HBox,VBox,StackPane, …) and add child nodes inside its constructor. The result is a pane that you can place anywhere aNodeis accepted. - Subclassing a Control: Extend
Controland pair it with aSkinimplementation. This is the correct path for complex, skinnable widgets, but it requires more boilerplate.
For the majority of day-to-day custom widgets, composition is the right answer. You get layout behaviour for free and can focus entirely on the component's API and logic.
Building a Reusable LabeledField Component
A LabeledField is a very common requirement: a label above (or beside) a text field, plus an optional error message below it — all three nodes moving together as one unit.
labelText and errorText as StringProperty fields and returning them from labelTextProperty(), callers can use the full binding API — including Bindings.when(…) expressions and listeners — exactly as they would with any built-in control.
Using the Component in a Scene
Once the class exists you use it like any other node:
Loading FXML Into a Custom Control
For controls with complex layouts, maintaining the structure in an FXML file is far cleaner than constructing the scene graph in pure Java. The pattern is to load the FXML inside the component's own constructor, with the component acting as both the root and the controller.
<fx:root> — not <VBox> — as the root element in the FXML file. This is the signal to FXMLLoader that the root object is provided externally (via setRoot(this)). Without it the loader creates a second VBox instance and your @FXML fields never get injected into the component.
Exposing a CSS API
A well-designed control is styleable via CSS without requiring source changes. There are two levels of support you should provide.
- Style classes: Assign meaningful style-class strings so users can target your component in a stylesheet.
getStyleClass().add("labeled-field"); label.getStyleClass().add("labeled-field-label"); field.getStyleClass().add("labeled-field-input"); errorLabel.getStyleClass().add("labeled-field-error");
- CSS pseudo-classes: For states like invalid, use
PseudoClassso the control responds to:invalidin CSS.import javafx.css.PseudoClass; private static final PseudoClass INVALID = PseudoClass.getPseudoClass("invalid"); // toggle it when the error changes errorText.addListener((obs, old, val) -> { boolean hasError = val != null && !val.isEmpty(); errorLabel.setVisible(hasError); errorLabel.setManaged(hasError); pseudoClassStateChanged(INVALID, hasError); });In the application stylesheet:.labeled-field:invalid .labeled-field-input { -fx-border-color: #e53935; }
Keeping Controls Reusable: Design Guidelines
- Encapsulate layout, expose data. Internal layout decisions (spacing, padding, node structure) should not leak. Callers should only see a clean property API.
- Never hard-code dimensions inside the component. Let the parent layout or the CSS control sizing. Use
setMaxWidth(Double.MAX_VALUE)on child nodes you want to grow with the container. - Prefer
ObjectProperty/StringProperty/BooleanPropertyover plain fields. This enables binding and makes the component work naturally with the rest of JavaFX. - Provide a no-argument constructor in addition to convenience constructors. FXML cannot invoke constructors that take parameters unless you use
@NamedArg, so a default constructor keeps the component usable from FXML.
@FXML-injected fields in the constructor body — use @FXML initialize() instead. When the FXML loader calls your constructor the injected fields are null; they are populated only after the XML is parsed. Set up bindings and listeners that reference those fields inside the initialize() method, which the loader calls once injection is complete.
Packaging Controls for Reuse Across Projects
Once you have several custom controls it is worth packaging them as a standalone JAR (or a Java module). Place each control class and its FXML resource in the same package. In a modular project, expose the package in module-info.java:
The opens … to javafx.fxml directive is mandatory because FXMLLoader uses reflection to inject @FXML-annotated fields, and the module system blocks reflective access by default.
Summary
Custom controls are built by extending a layout pane (composition) or Control (when full skinning is needed), exposing a property-based API, loading FXML with the <fx:root> pattern when the layout is complex, and offering CSS hooks via style classes and pseudo-classes. The result is a node that is indistinguishable — from the scene graph's perspective — from any built-in JavaFX control, and that any screen in the application can reuse without duplication.