Skip to content
CalliCoder

Styling JavaFX Applications Using CSS

JavaFX 12 min read

JavaFX CSS is not web CSS: properties are -fx- prefixed, selectors match scene-graph classes rather than HTML tags, and there is no box model. What transfers, what does not, and how to debug a rule that will not apply.

JavaFX styles its scene graph with CSS, and the similarity to web CSS is close enough to be misleading. The syntax is the same. The property names, the selectors and the layout model are not.

Written against JavaFX 21 and Java 17.

What is different, up front

Four differences account for most wasted time:

  • Every JavaFX-specific property is prefixed -fx-. -fx-background-color, not background-color. An unprefixed property is silently ignored.
  • Selectors match node types, not tags. .button, .label, .text-field, the style class every control carries by default. There is no button { } element selector.
  • There is no box model. No margin, no display, no float. Position and spacing come from the layout container (VBox, HBox, GridPane) and its spacing and padding.
  • Ignored rules fail silently. A misspelled property produces no error and no warning. That is the single biggest practical difference from a browser, which at least greys the declaration out in devtools.

A minimal application

public class StyledApp extends Application {

    @Override
    public void start(Stage stage) {
        Label heading = new Label("Notes");
        heading.getStyleClass().add("heading");

        TextField search = new TextField();
        search.setPromptText("Search notes");
        search.setId("search-field");

        Button save = new Button("Save");
        save.getStyleClass().add("primary");

        Button cancel = new Button("Cancel");

        HBox actions = new HBox(8, save, cancel);
        VBox root = new VBox(12, heading, search, actions);
        root.getStyleClass().add("panel");

        Scene scene = new Scene(root, 360, 220);
        scene.getStylesheets().add(
                getClass().getResource("/styles/app.css").toExternalForm());

        stage.setScene(scene);
        stage.setTitle("Notes");
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

toExternalForm() on a getResource result, not a bare path. getStylesheets().add("app.css") is interpreted as a URL and fails at runtime with NullPointerException on the resource, or silently loads nothing. Depending on how it fails. If styles do not apply at all, check this line first.

The stylesheet

/* selector by style class */
.panel {
    -fx-background-color: #f5f7f2;
    -fx-padding: 16;
    -fx-border-color: #dce3d6;
    -fx-border-width: 0 0 1 0;      /* top right bottom left */
}

.heading {
    -fx-font-size: 20px;
    -fx-font-weight: bold;
    -fx-text-fill: #283618;
}

/* every Button in the scene */
.button {
    -fx-background-radius: 3;
    -fx-padding: 6 14 6 14;
    -fx-cursor: hand;
}

/* a specific one, by added class */
.button.primary {
    -fx-background-color: #157933;
    -fx-text-fill: white;
}

/* by id — one node */
#search-field {
    -fx-prompt-text-fill: #808a76;
}

/* pseudo-classes */
.button:hover  { -fx-background-color: derive(#157933, -12%); }
.button:pressed { -fx-background-color: derive(#157933, -25%); }
.text-field:focused {
    -fx-border-color: #157933;
    -fx-border-width: 1;
}

Note -fx-text-fill rather than color, and -fx-background-color on a control rather than background. derive(colour, percent) brightens or darkens without hardcoding a second value — positive toward white, negative toward black, and ladder, linear-gradient and radial-gradient are available too.

Padding and border widths take one to four values in CSS order, top-right-bottom-left, unitless numbers meaning pixels.

Looked-up colours

The feature with no direct web equivalent, and the reason JavaFX theming is manageable. Define a variable on an ancestor and any descendant resolves it:

.root {
    -fx-accent-colour: #157933;
    -fx-ink: #283618;
}

.button.primary { -fx-background-color: -fx-accent-colour; }
.heading        { -fx-text-fill: -fx-ink; }

The lookup is dynamic and follows the scene graph, so overriding it on a subtree re-themes everything beneath:

.dark-panel {
    -fx-accent-colour: #3fc77a;
    -fx-ink: #e8efe2;
}

.root is the style class the scene’s root node carries automatically, which makes it the natural place for application-wide values.

The built-in stylesheet uses this mechanism, so setting -fx-base or -fx-accent on .root shifts the whole default look:

.root { -fx-base: #3c4a34; }     /* every control derives from this */

Where a stylesheet can be attached

Three levels, narrowest wins:

Application.setUserAgentStylesheet(url);   // replaces the platform default
scene.getStylesheets().add(url);           // whole scene
node.getStylesheets().add(url);            // that node and its descendants
node.setStyle("-fx-background-color: red;"); // inline, highest priority

Inline setStyle beats every stylesheet, which makes it useful for a one-off and unhelpful for anything reusable: a style set in code cannot be overridden by CSS later.

Hot reloading during development is a case for the node-level list: clear it and re-add it on a key press and you see edits without restarting.

Specificity works differently

JavaFX resolves conflicts differently from a browser, and this is the second-most common reason a rule does not apply. The order, strongest first:

  1. inline setStyle
  2. author stylesheets (scene and parent, nearest wins)
  3. the user-agent (default) stylesheet
  4. values set from Java code with a setter

Point 4 is the surprising one. label.setTextFill(Color.RED) in Java is treated as a default, so a stylesheet rule overrides it. That is the opposite of what most people assume, and it is why mixing programmatic styling with CSS produces results that seem random. Pick one per property.

Within author stylesheets there is no weighting by selector type as in web CSS, a later rule of equal origin wins.

Pseudo-classes, including your own

The built-in set covers state: :hover, :pressed, :focused, :disabled, :selected, :empty, and :first-child / :last-child on some containers.

You can define your own and toggle it from code, which is the idiomatic way to style a domain state:

private static final PseudoClass OVERDUE = PseudoClass.getPseudoClass("overdue");

// on the row or cell node
node.pseudoClassStateChanged(OVERDUE, task.isOverdue());
.list-cell:overdue {
    -fx-background-color: #fde8e8;
    -fx-text-fill: #8b1a1a;
}

Better than adding and removing style classes by hand, because the toggle is a boolean and cannot drift out of sync.

Substructure

Composite controls expose their internal parts as style classes, and styling a control often means styling a part rather than the control:

.scroll-bar .thumb          { -fx-background-color: #a9bc9a; }
.scroll-bar .track          { -fx-background-color: transparent; }
.table-view .column-header  { -fx-background-color: #283618; }
.table-view .table-row-cell:odd { -fx-background-color: #f5f7f2; }
.check-box .box             { -fx-background-radius: 2; }
.combo-box .arrow           { -fx-background-color: #606c38; }

These names are not guessable. They come from the default stylesheet, modena.css, which ships inside the JavaFX controls module, extract it and read it:

$ unzip -p javafx-controls-21.jar com/sun/javafx/scene/control/skin/modena/modena.css > modena.css

That file is the actual reference for what is styleable, and it is more reliable than any tutorial including this one.

Debugging a rule that will not apply

In order of how often each is the cause:

  1. Missing -fx- prefix. background-color does nothing.
  2. The stylesheet never loaded. Print scene.getStylesheets() and confirm the URL resolves.
  3. A Java setter is fighting it, but note the direction: CSS wins over a setter, so the symptom is usually the setter appearing not to work.
  4. Wrong selector. The node’s style classes are printable: node.getStyleClass().
  5. Styling the control when the part needs it, the thumb, not the scroll bar.

For anything past that, ScenicView attaches to a running application and shows the scene graph with the CSS resolved per node, which is as close to browser devtools as this gets.

There is also a strict-mode system property that turns silently ignored declarations into console warnings during development, which is worth having on in a debug run.

Frequently asked questions

Why is my CSS not being applied at all?

Usually the stylesheet URL. Use getClass().getResource("/styles/app.css").toExternalForm(); a bare filename does not resolve.

Why does background-color do nothing?

JavaFX properties are prefixed. It is -fx-background-color, and unprefixed properties are silently ignored.

How do I set text colour?

-fx-text-fill, not color.

Why can I not use margin or display?

There is no box model. Spacing comes from the layout container’s spacing and padding; visibility from setVisible/setManaged in code.

What selector matches a Button?

.button, the default style class. There are no element selectors, because the scene graph has no tags.

Why is my setTextFill being overridden by CSS?

Because that is the defined precedence: values set from Java code rank below author stylesheets. Style a given property from CSS or from code, not both.

How do I find the style class for a control’s inner parts?

Read modena.css from the javafx-controls jar. It is the authoritative list of styleable substructure.

How do I style based on my own state?

Define a PseudoClass and toggle it with pseudoClassStateChanged, then select :your-state in CSS.

Can I reload CSS without restarting?

Yes: clear and re-add the stylesheet on the scene or a node. Binding that to a key makes iteration much faster.

How do I re-theme the whole application?

Set looked-up colours such as -fx-base and -fx-accent on .root, or define your own and reference them from your rules.

Where should I go next?

If the application itself is new, the first JavaFX application covers the module path and the lifecycle, and FXML moves the layout out of Java so the stylesheet has stable selectors to target. The JavaFX guides cover the rest of the toolkit, and the Java guides cover the language underneath it.