Java Swing
Swing can build apps that look current and handle huge data, but its defaults point the other way: a 2005 look, one thread for everything, and components designed for small data at 1x scaling. This skill is how to get from the defaults to a good app.
Workflow for any UI work
- Find what's there: look and feel, layout style, the background-task helper, the test setup, the app's spacing and colors. Follow them.
- New app? Start from
templates/desktop-app/: FlatLaf, a sidebar/header/content/status layout, empty states, dark mode, settings, remembered window bounds, keyboard shortcuts, and a screenshot test. Rename and build on it. - Build it following the rules below and
references/design.md. - Look at it. You can't see the window, so render it:
examples/Snapshot.javawrites PNGs of any panel, without a display. Render light and dark, 1x and 2x, the empty state, a narrow window and long text. Open the images and review them with the checklist at the end ofreferences/design.md. Fix and render again. Don't call UI work done without having looked. - Test it: logic in plain unit tests, components painted headless, the real window
in a visual test (
references/testing.md).
Taste: the short version
Read references/design.md before building or restyling a screen. In brief:
- FlatLaf, one accent color, one corner radius. Never Metal for a user-facing app.
- A clear window anatomy: sidebar for places, header with a large title and the one primary action, content with room, a quiet status bar.
- Spacing from one scale (4, 8, 12, 16, 24, 32), 24–32 px from window edges. Group with
space and small headings, never
TitledBorder/etched boxes. - Type hierarchy from two or three sizes (FlatLaf
h1…h4,small) and one muted color for secondary text. Sentence case. Verbs on buttons. - Every view has an empty state with a line of explanation and the action that fills it. Loading is inline, never a modal "Please wait". Errors appear inline next to what failed, and say what to do.
- Few dialogs. Inline editing and undo instead of "Are you sure?".
- Keyboard: every action in a menu, frequent ones with platform shortcuts shown in menus and tooltips.
The five engineering rules
- Touch components only on the Event Dispatch Thread (EDT). Create, change, read and
lay out components inside
SwingUtilities.invokeLater, including the first window inmain. Models (TableModel,Document,ListModel) count as components. - Never block the EDT. Every file, network, process, database or lock wait goes to a
background thread, even if it "should be fast".
File.exists()on a network drive,InetAddress.getByName,Process.waitFor,Files.sizeon a cloud folder and a JDBCconnecthave all frozen real windows. Results come back withinvokeLater. - Collapse floods of updates. A background thread that calls
invokeLaterper item (per line, per row, per progress tick) buries the EDT. Post at most one pending update that reads the latest state (examples/Coalescer.java). - Paint only what is visible, and never wait in
paintComponent. If data isn't in memory, draw a placeholder, load it in the background,repaint()when it arrives. - Resolve colors, fonts and sizes at paint time. Colors stored in fields break dark-mode switches; pixel math assuming 1x breaks at 125%/150% scaling.
Doing common jobs right
| Job | Do | Don't |
|---|---|---|
| Long task with a result | SwingWorker or your executor, then invokeLater(() -> show(result)) | Run it in actionPerformed |
| Progress from a worker | Store progress in an atomic field, post one coalesced update | invokeLater per item |
| Add/remove components | revalidate() then repaint() on the parent | Only repaint(), or validate() off the EDT |
| Switch views | CardLayout.show(parent, name) | Put one component in two containers (it leaves the first) |
| Shortcuts | KeyStroke with getMenuShortcutKeyMaskEx() (⌘ on macOS, Ctrl elsewhere) | Hard-code CTRL_DOWN_MASK |
| Key bindings | InputMap/ActionMap, or an Action with an accelerator in a menu | KeyListener |
| Custom painting | Override paintComponent, call super, g.create() and dispose() | Override paint, or change state in paint |
| Theme colors | FlatLaf styles with $Key references, UIManager.getColor in paint, or examples/LiveColor.java | new Color(...) fields set once |
| List/table rows with several parts | A renderer panel that sets every property per call, and is opaque with its background set (FlatLaf then rounds the selection) | A panel per row in a BoxLayout |
| Text in a grid (editors, logs) | Measure at 1x, draw with drawGlyphVector (examples/GridText.java) | drawString at fractional scales |
| Millions of rows | A view whose scroll bar counts rows (examples/VirtualRowView.java) | JTable with DefaultTableModel |
| Files opened from Finder | Desktop.setOpenFileHandler(...) | Only main(String[] args) |
| One window per app | examples/SingleInstance.java (file lock + loopback socket) | A second window per double-click |
Reference files
Read the one that matches the task:
references/design.md: taste: window anatomy, spacing, type, color, controls, icons, empty/loading/error states, dialogs, wording, review checklist.references/threading.md: EDT rules, SwingWorker limits, executors, stale results, coalescing, modal dialogs from workers, finding violations and stalls, shutdown.references/big-data.md: where JTable/JList stop, compact models, sorting millions of rows, paint that never waits, virtual views, wide tables, overview bars.references/painting-and-hidpi.md:paintComponent, rendering hints, fractional scaling, text on a grid, crisp lines, icons and images, animation, charts.references/theming.md: FlatLaf, howUIResourcedecides colors, dark mode switching, palettes, fonts, FlatLaf client properties.references/layout.md: choosing layouts, sizes from fonts,revalidate, one parent per component,CardLayout,JSplitPane,JScrollPane, wrapping rows, RTL.references/tables-lists-trees.md: renderers, models, view vs. model indexes, sorting, editing, columns, selection, lazy trees.references/text-components.md: choosing a text component, documents and threads, validation, search fields, highlights, undo, HTML in Swing.references/dialogs-focus-keyboard.md: dialogs done right, focus, key bindings, accessibility, tooltips, popup menus.references/dnd-clipboard.md: dropping files (including Linuxtext/uri-list), dragging inside the app, clipboard.references/testing.md: unit, headless component and visual tests, waiting instead of sleeping, the displayable trap, responsiveness tests, xvfb in CI, packaged-app tests.references/desktop-integration.md: macOS, Windows and Linux specifics, single instance, jpackage, startup time, settings, updates and errors.references/performance.md: finding the cause, fast painting, less EDT work, budgets.
Copyable helpers (examples/)
Dependency-free classes, compiled and checked by tests/SelfTest.java in CI. Copy one and
change its package:
| File | What it does |
|---|---|
Snapshot.java | Renders any panel or window to PNG, at any scale, without a display |
OnEdt.java | Test helpers: run on the EDT with results and exceptions, waitFor with a message, settle |
Coalescer.java | At most one pending EDT update for a flood of changes |
EdtWatchdog.java | Measures EDT responsiveness and records where it was stuck |
EdtViolationChecker.java | Reports components changed off the EDT (debug/tests) |
LiveColor.java | A Color that follows light/dark switches |
GridText.java | Monospaced text that stays on its grid at 125%/150%/175% |
VirtualRowView.java | Scrolls hundreds of millions of rows; paint never waits for data |
SingleInstance.java | One window for the app; later launches hand over their files |
Traps that waste hours
| Symptom | Cause |
|---|---|
| Rows of a list/table are blank in a headless screenshot or test | The tree isn't displayable, so renderer panels never lay out. Snapshot.render calls addNotify(); do the same. |
| FlatLaf shows no rounded selection in a list with a custom renderer | The renderer isn't opaque, or its background isn't set to the selection color when selected |
| A color stays light after switching to dark | It was set with a plain Color; use a $Key style, UIManager at paint time, or LiveColor |
| Text drifts from its highlights at 125% on Windows | FontMetrics and drawString disagree at fractional scales; use GridText |
setDividerLocation(0.3) does nothing | Called before the split pane has a size; call after the window is shown |
| A component vanished from one panel | It was added to another; a component has one parent |
| Window freezes only for some users | File or network calls on the EDT that are slow on their drives or network |
| The app stays running after the window closed | A non-daemon thread, a running javax.swing.Timer, or an undisposed window |
A KeyListener shortcut works only sometimes | It needs focus; use key bindings or menu accelerators |
| Sorted table edits the wrong row | View index used as model index; convertRowIndexToModel |
When reviewing Swing code
Look for, in order of how often they hurt users:
- I/O,
sleep,join,waitFor, locks orinvokeAndWaitinside listeners,paintComponentor anything they call. - Components, models or
Documents touched from executor threads,java.util.Timer, library callbacks (HTTP, SSH, JDBC, file watchers). invokeLaterinside a loop over data.- Work in
paintComponentthat grows with the data rather than the visible area. - Colors, fonts, borders or icons computed once and stored; titled/etched borders; Metal.
- Pixel math assuming 1x;
FontMetricsfrom one context, drawing in another. - Tests that sleep and then assert, or check focus/tooltips without waiting.
grep -rn "invokeAndWait" src/main # can it run on the EDT, or while holding a lock?
grep -rn "Thread.sleep" src/main # any on the EDT?
grep -rn "java.util.Timer\b" src/main # callbacks run off the EDT
grep -rnE "new Color\(|Color\.(WHITE|BLACK|GRAY)" src/main/java # fixed colors that ignore dark mode
grep -rnE "TitledBorder|EtchedBorder|createTitledBorder" src/main/java # dated grouping
grep -rn "MetalLookAndFeel\|getCrossPlatformLookAndFeel" src/main/java