ADDITIONAL SYSTEM INFORMATION :
- JavaFX Version: 25
- Java Version: OpenJDK Runtime Environment Zulu25.28+85-CA (build 25+36-LTS)
- Operating System: macOS Sequoia 15.6
A DESCRIPTION OF THE PROBLEM :
I'm experiencing a silent failure of the JavaFX CSS engine to apply an external stylesheet value to a custom StyleableObjectProperty on a subclass of Control, even when all standard best practices (correct CssMetaData, proper module access, high specificity, and the latest JavaFX version) are followed.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Compile and run the provided minimal JavaFX application
2. Observe the ToolBarSeparator control.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The ToolBarSeparator line color should be RED, as defined by the external stylesheet (style.css), overriding the internal Java default of Color.BLUE.
ACTUAL -
The ToolBarSeparator line color remains BLUE, confirming that the CssMetaData default value is read, but the external CSS rule is silently rejected and not applied to the property.
---------- BEGIN SOURCE ----------
module-info.java:
module problem.demo {
requires javafx.base;
requires javafx.controls;
requires javafx.fxml;
requires transitive javafx.graphics;
opens problem.demo to javafx.base, javafx.controls, javafx.graphics;
exports problem.demo;
}
Launcher.java:
package problem.demo;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Launcher extends Application {
public static void main (String[] args) {
launch(args);
}
@Override
public void start(Stage stage) {
VBox root = new VBox();
Scene scene = new Scene(root, 900, 600);
scene.getStylesheets().add(getClass().getClassLoader().getResource("style.css").toExternalForm());
stage.setScene(scene);
ToolBarSeparator tbs = new ToolBarSeparator();
Button button = new Button("Hello");
root.getChildren().addAll(tbs, button);
stage.show();
}
}
ToolBarSeparator.java:
package problem.demo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Control;
import javafx.scene.control.SkinBase;
public class ToolBarSeparator extends Control {
private static final CssMetaData<ToolBarSeparator, Paint> STROKE_META_DATA =
new CssMetaData<ToolBarSeparator, Paint>(
"-my-app-separator-line-color",
javafx.css.converter.PaintConverter.getInstance(),
Color.GREEN,
false
) {
@Override
public StyleableProperty<Paint> getStyleableProperty(ToolBarSeparator styleable) {
return (StyleableProperty<Paint>) styleable.strokeProperty();
}
@Override
public boolean isSettable(ToolBarSeparator styleable) {
return true;
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CSS_META_DATA;
static {
List<CssMetaData<? extends Styleable, ?>> parentMetaData = Control.getClassCssMetaData();
List<CssMetaData<? extends Styleable, ?>> customMetaData = new ArrayList<>(parentMetaData.size() + 1);
customMetaData.addAll(parentMetaData);
customMetaData.add(STROKE_META_DATA);
CSS_META_DATA = Collections.unmodifiableList(customMetaData);
}
private final StyleableObjectProperty<Paint> stroke;
public ToolBarSeparator() {
this.stroke = new SimpleStyleableObjectProperty<>(
STROKE_META_DATA,
this,
"stroke"
);
setMinHeight(28);
setPrefHeight(28);
setMaxHeight(28);
setMinWidth(5);
setPrefWidth(5);
setMaxWidth(5);
setId("my-separator-unique-id");
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return CSS_META_DATA;
}
@Override
protected javafx.scene.control.Skin<?> createDefaultSkin() {
applyCss();
if (getStroke() == null) {
setStroke(Color.BLUE);
}
return new ToolBarSeparatorSkin(this);
}
@Override
protected double computePrefWidth(double height) {
return Region.USE_COMPUTED_SIZE;
}
public final StyleableObjectProperty<Paint> strokeProperty() {
return stroke;
}
public final Paint getStroke() { return strokeProperty().get(); }
public final void setStroke(Paint value) { strokeProperty().set(value); }
public class ToolBarSeparatorSkin extends SkinBase<ToolBarSeparator> {
private final Canvas canvas;
public ToolBarSeparatorSkin(ToolBarSeparator control) {
super(control);
this.canvas = new Canvas();
getChildren().add(canvas);
control.widthProperty().addListener((obs, oldVal, newVal) -> {
draw(newVal.doubleValue(), control.getHeight());
});
control.heightProperty().addListener((obs, oldVal, newVal) -> {
draw(control.getWidth(), newVal.doubleValue());
});
control.strokeProperty().addListener((obs, oldVal, newVal) -> {
System.out.println("STYLE CHANGE");
draw(control.getWidth(), control.getHeight());
});
}
@Override
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
super.layoutChildren(contentX, contentY, contentWidth, contentHeight);
canvas.setWidth(contentWidth);
canvas.setHeight(contentHeight);
draw(contentWidth, contentHeight);
}
public void draw(double width, double height) {
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.clearRect(0, 0, width, height);
Color color = Color.GRAY;
Paint p = getSkinnable().getStroke();
if (p instanceof Color c) {
color = c;
System.out.println("*** p is Color ***");
} else {
System.out.println("*** p is NOT Color ***");
}
gc.setStroke(color);
gc.setLineWidth(1);
for (double i = 0; i < height; i +=3) {
gc.strokeLine(0, i, width, i);
}
}
}
}
style.css:
.button {
-fx-background-color: yellow;
}
#my-separator-unique-id {
-my-app-separator-line-color: red !important;
}
---------- END SOURCE ----------
- JavaFX Version: 25
- Java Version: OpenJDK Runtime Environment Zulu25.28+85-CA (build 25+36-LTS)
- Operating System: macOS Sequoia 15.6
A DESCRIPTION OF THE PROBLEM :
I'm experiencing a silent failure of the JavaFX CSS engine to apply an external stylesheet value to a custom StyleableObjectProperty on a subclass of Control, even when all standard best practices (correct CssMetaData, proper module access, high specificity, and the latest JavaFX version) are followed.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Compile and run the provided minimal JavaFX application
2. Observe the ToolBarSeparator control.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The ToolBarSeparator line color should be RED, as defined by the external stylesheet (style.css), overriding the internal Java default of Color.BLUE.
ACTUAL -
The ToolBarSeparator line color remains BLUE, confirming that the CssMetaData default value is read, but the external CSS rule is silently rejected and not applied to the property.
---------- BEGIN SOURCE ----------
module-info.java:
module problem.demo {
requires javafx.base;
requires javafx.controls;
requires javafx.fxml;
requires transitive javafx.graphics;
opens problem.demo to javafx.base, javafx.controls, javafx.graphics;
exports problem.demo;
}
Launcher.java:
package problem.demo;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Launcher extends Application {
public static void main (String[] args) {
launch(args);
}
@Override
public void start(Stage stage) {
VBox root = new VBox();
Scene scene = new Scene(root, 900, 600);
scene.getStylesheets().add(getClass().getClassLoader().getResource("style.css").toExternalForm());
stage.setScene(scene);
ToolBarSeparator tbs = new ToolBarSeparator();
Button button = new Button("Hello");
root.getChildren().addAll(tbs, button);
stage.show();
}
}
ToolBarSeparator.java:
package problem.demo;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Control;
import javafx.scene.control.SkinBase;
public class ToolBarSeparator extends Control {
private static final CssMetaData<ToolBarSeparator, Paint> STROKE_META_DATA =
new CssMetaData<ToolBarSeparator, Paint>(
"-my-app-separator-line-color",
javafx.css.converter.PaintConverter.getInstance(),
Color.GREEN,
false
) {
@Override
public StyleableProperty<Paint> getStyleableProperty(ToolBarSeparator styleable) {
return (StyleableProperty<Paint>) styleable.strokeProperty();
}
@Override
public boolean isSettable(ToolBarSeparator styleable) {
return true;
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CSS_META_DATA;
static {
List<CssMetaData<? extends Styleable, ?>> parentMetaData = Control.getClassCssMetaData();
List<CssMetaData<? extends Styleable, ?>> customMetaData = new ArrayList<>(parentMetaData.size() + 1);
customMetaData.addAll(parentMetaData);
customMetaData.add(STROKE_META_DATA);
CSS_META_DATA = Collections.unmodifiableList(customMetaData);
}
private final StyleableObjectProperty<Paint> stroke;
public ToolBarSeparator() {
this.stroke = new SimpleStyleableObjectProperty<>(
STROKE_META_DATA,
this,
"stroke"
);
setMinHeight(28);
setPrefHeight(28);
setMaxHeight(28);
setMinWidth(5);
setPrefWidth(5);
setMaxWidth(5);
setId("my-separator-unique-id");
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return CSS_META_DATA;
}
@Override
protected javafx.scene.control.Skin<?> createDefaultSkin() {
applyCss();
if (getStroke() == null) {
setStroke(Color.BLUE);
}
return new ToolBarSeparatorSkin(this);
}
@Override
protected double computePrefWidth(double height) {
return Region.USE_COMPUTED_SIZE;
}
public final StyleableObjectProperty<Paint> strokeProperty() {
return stroke;
}
public final Paint getStroke() { return strokeProperty().get(); }
public final void setStroke(Paint value) { strokeProperty().set(value); }
public class ToolBarSeparatorSkin extends SkinBase<ToolBarSeparator> {
private final Canvas canvas;
public ToolBarSeparatorSkin(ToolBarSeparator control) {
super(control);
this.canvas = new Canvas();
getChildren().add(canvas);
control.widthProperty().addListener((obs, oldVal, newVal) -> {
draw(newVal.doubleValue(), control.getHeight());
});
control.heightProperty().addListener((obs, oldVal, newVal) -> {
draw(control.getWidth(), newVal.doubleValue());
});
control.strokeProperty().addListener((obs, oldVal, newVal) -> {
System.out.println("STYLE CHANGE");
draw(control.getWidth(), control.getHeight());
});
}
@Override
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
super.layoutChildren(contentX, contentY, contentWidth, contentHeight);
canvas.setWidth(contentWidth);
canvas.setHeight(contentHeight);
draw(contentWidth, contentHeight);
}
public void draw(double width, double height) {
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.clearRect(0, 0, width, height);
Color color = Color.GRAY;
Paint p = getSkinnable().getStroke();
if (p instanceof Color c) {
color = c;
System.out.println("*** p is Color ***");
} else {
System.out.println("*** p is NOT Color ***");
}
gc.setStroke(color);
gc.setLineWidth(1);
for (double i = 0; i < height; i +=3) {
gc.strokeLine(0, i, width, i);
}
}
}
}
style.css:
.button {
-fx-background-color: yellow;
}
#my-separator-unique-id {
-my-app-separator-line-color: red !important;
}
---------- END SOURCE ----------