JavaFX Binding, Events & Styling

Styling with CSS

18 min Lesson 7 of 12

Styling with CSS

JavaFX has a first-class CSS engine built directly into the scene graph. Every node in the scene carries a style class, can hold inline -fx- properties, and participates in a cascade almost identical to browser CSS. The result is a clean separation between your application logic and its visual presentation — the same principle web developers apply every day, but targeting a desktop scene graph instead of a browser DOM.

How JavaFX CSS Works

When the JavaFX runtime lays out a scene it also runs a CSS pass. It loads stylesheets in order — the default Modena theme first, then any application stylesheets, then inline style attributes — and resolves the final set of visual properties for every node. All CSS properties in JavaFX are prefixed with -fx- to avoid clashing with standard CSS properties.

Modena is the default theme. It is a CSS file bundled inside the JavaFX runtime JAR. You never need to load it yourself; it is always the lowest-priority stylesheet. Your application CSS overrides it selectively.

Adding a Stylesheet to a Scene

Call getStylesheets().add() on any Scene or Parent. Use getClass().getResource() to resolve the path relative to your class — that makes the app work whether it is run from an IDE, a JAR, or a module path.

// In your Application subclass or controller Scene scene = new Scene(root, 900, 600); scene.getStylesheets().add( getClass().getResource("/css/app.css").toExternalForm() );

You can also scope a stylesheet to a subtree by adding it to a Parent node (like a VBox or AnchorPane). Styles added there apply only to that node and its descendants.

Selectors: Class, ID, and Type

JavaFX CSS supports three selector types mapped to JavaFX concepts:

  • Type selector — matches a JavaFX control class. .button { } targets all Button nodes. The selector name follows the JavaFX CSS convention (lowercase with hyphens).
  • Style-class selector (.name) — matches nodes that have that style class in their getStyleClass() list. This is the main tool for semantic grouping.
  • ID selector (#name) — matches the single node whose id property equals the string. Use sparingly; IDs should be unique per scene.
/* All Button nodes */ .button { -fx-background-color: #3498db; -fx-text-fill: white; -fx-font-size: 14px; -fx-padding: 8 20 8 20; -fx-cursor: hand; -fx-background-radius: 6; } /* Semantic style class */ .danger-button { -fx-background-color: #e74c3c; } /* Unique ID */ #submit-btn { -fx-font-weight: bold; }

In Java code you add and remove style classes at runtime, just as you would toggle CSS classes in JavaScript:

Button btn = new Button("Delete"); btn.getStyleClass().add("danger-button"); // add btn.getStyleClass().remove("danger-button"); // remove btn.setId("submit-btn"); // set ID

Pseudo-Classes: Hover, Focused, and Disabled

JavaFX automatically applies pseudo-classes to nodes based on their state. You style them the same way as in web CSS, using the : colon syntax:

.button:hover { -fx-background-color: #2980b9; -fx-effect: dropshadow(gaussian, rgba(0,0,0,0.25), 6, 0, 0, 2); } .button:focused { -fx-border-color: #1abc9c; -fx-border-width: 2; -fx-border-radius: 6; } .button:disabled { -fx-opacity: 0.5; -fx-cursor: default; } /* Pressed state */ .button:pressed { -fx-background-color: #1a6ea8; -fx-translate-y: 1; }
Define custom pseudo-classes for your own controls. Use PseudoClass.getPseudoClass("error") and node.pseudoClassStateChanged(errorClass, true) to apply :error { } CSS from your validation logic — no style-class toggling needed.

Common -fx- Properties

The most frequently used CSS properties in JavaFX applications:

  • -fx-background-color — solid colour, gradient, or layered paint list.
  • -fx-text-fill — text colour for labels and buttons.
  • -fx-font-family, -fx-font-size, -fx-font-weight — typography.
  • -fx-padding — insets (top right bottom left, same shorthand as CSS).
  • -fx-background-radius / -fx-border-radius — rounded corners.
  • -fx-border-color, -fx-border-width — border decoration.
  • -fx-effect — drop shadows, blurs, inner-glow.
  • -fx-cursor — cursor shape (hand, crosshair, wait, etc.).

Theming: Light and Dark Modes

A clean theming strategy uses CSS variables (looked-up colours in JavaFX terminology). Define named colours at the root level and reference them everywhere else. Switching the root overrides the entire theme instantly.

/* In light-theme.css */ .root { -app-bg: #ffffff; -app-surface: #f4f6f8; -app-primary: #3498db; -app-on-primary: #ffffff; -app-text: #2c3e50; -app-border: #dce1e7; } /* In dark-theme.css */ .root { -app-bg: #1e1e2e; -app-surface: #2a2a3e; -app-primary: #7c9ef8; -app-on-primary: #1e1e2e; -app-text: #cdd6f4; -app-border: #45475a; } /* Shared component file — references the variables */ .card { -fx-background-color: -app-surface; -fx-border-color: -app-border; -fx-border-radius: 8; -fx-background-radius: 8; -fx-padding: 16; } .label { -fx-text-fill: -app-text; }

To toggle the theme at runtime, swap the stylesheet:

private boolean darkMode = false; public void toggleTheme(Scene scene) { ObservableList<String> sheets = scene.getStylesheets(); String light = getClass().getResource("/css/light-theme.css").toExternalForm(); String dark = getClass().getResource("/css/dark-theme.css").toExternalForm(); if (darkMode) { sheets.remove(dark); sheets.add(light); } else { sheets.remove(light); sheets.add(dark); } darkMode = !darkMode; }
Order matters in the stylesheet list. The last stylesheet added wins in a conflict. Add your theme stylesheet first and your component stylesheet second so component rules can still override theme defaults where needed.

Inline Styles and Programmatic Styling

For one-off overrides you can set an inline style directly on a node. Inline styles always beat external stylesheets in the cascade — use them only for truly dynamic values (colours computed at runtime, sizes derived from data).

Label badge = new Label(status.toUpperCase()); String colour = status.equals("ACTIVE") ? "#27ae60" : "#e74c3c"; badge.setStyle( "-fx-background-color: " + colour + ";" + "-fx-text-fill: white;" + "-fx-padding: 3 8 3 8;" + "-fx-background-radius: 4;" );
Avoid heavy inline styling in production code. It scatters visual logic through your controllers and makes theming nearly impossible. Reserve setStyle() for values that are genuinely dynamic. For everything else, use style classes and swap them programmatically.

Custom Pseudo-Classes: Practical Example

Suppose you have a TextField that should show a red border when validation fails. Define a custom pseudo-class and wire it to your validation logic:

import javafx.css.PseudoClass; import javafx.scene.control.TextField; public class ValidatedField extends TextField { private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error"); public void setError(boolean hasError) { pseudoClassStateChanged(ERROR, hasError); } }
/* In your CSS */ .text-field:error { -fx-border-color: #e74c3c; -fx-border-width: 2; -fx-border-radius: 4; -fx-background-color: #fff5f5; }

Now your controller calls emailField.setError(true) and the CSS engine does the rest — no inline style juggling, no style-class removal races.

Summary

JavaFX CSS gives you full control over the visual layer without touching Java code. Use external stylesheets loaded via getStylesheets(), target nodes with type, style-class, and ID selectors, leverage pseudo-classes for interactive states, and build themes around looked-up colour variables. Keep inline styles exceptional. This clean separation makes your UI maintainable, testable, and easy to hand off to a designer.

ES
Edrees Salih
1 hour ago

We are still cooking the magic in the way!