The moment Swing stops feeling “simple” is when you outgrow stock widgets. You add a status badge that needs a subtle gradient, a sparkline that must redraw smoothly, or a control that reacts to keyboard shortcuts the same way across platforms—and suddenly you’re fighting quirks instead of shipping features.
When that happens, I reach for JComponent.
JComponent is the base class for almost every Swing widget you actually place inside a window. It’s abstract, lightweight (painted by the Java runtime rather than the native OS), and intentionally designed as a foundation for custom drawing, input handling, and look-and-feel integration. If you can get comfortable with how JComponent paints itself, sizes itself, and receives input, you can build UI pieces that feel first-class—without writing a whole framework.
Here’s what I’ll walk you through: what JComponent really represents in Swing, how lightweight rendering changes your mental model, how painting works from repaint() to paintComponent(), how sizing interacts with layout managers, and how I wire input in a way that stays maintainable. I’ll also show a complete, runnable custom component you can drop into a real app.
Why JComponent is the real “unit” of Swing
If Swing were a city, JComponent would be the building code. It doesn’t tell you what the building looks like—but it defines the rules that make the building safe, compatible, and easy to connect to utilities.
Formally, the core declaration looks like this:
public abstract class JComponent extends Container implements java.io.Serializable
A few implications matter in practice:
- It’s a
Container. Your component can hold other components. Many custom components don’t need children, but the capability is there. - It’s
Serializable. You can persist UI state, but I treat Swing serialization as an advanced tool with sharp edges (more on that later). - It’s the common ancestor of “almost everything with a
J”. Buttons, labels, text fields, panels, tables—most Swing widgets inherit fromJComponent. - Top-level containers are the exception.
JFrameandJDialogare top-level windows; they don’t extendJComponent. You don’t embed aJFrameinside another component.
This is why learning JComponent pays off: it’s not just another class—it’s where Swing’s conventions live.
Practical examples of features you get “for free” at the JComponent layer:
- Tooltips (via
setToolTipText) - Borders (via
setBorder) and insets - Key bindings (via
InputMap/ActionMap) - Double buffering support
- Client properties (metadata storage)
- Accessibility hooks (via an inner accessible class)
When I design Swing UIs today, I treat custom JComponent work as a normal part of building polished desktop software—especially for internal tools where speed of iteration matters.
A mental model that helps: a custom component is usually “a tiny UI module.” It should own a small slice of state, render that state, and expose a clean API so the rest of your app doesn’t care how the pixels happen.
Lightweight vs heavyweight: what it changes in practice
Swing components are lightweight. That means the Java runtime paints them rather than delegating to a native peer widget. AWT components like java.awt.Button are heavyweight because they generally rely on native peers.
Why you should care:
- Consistent rendering across platforms. Lightweight components are drawn by Java, so you avoid “this looks different on Windows vs macOS” surprises. It won’t be identical everywhere, but the variability is far lower.
- Z-order and mixing rules. Mixing heavyweight AWT components into Swing layouts can cause “holes” where the heavyweight component punches through, ignores transparency, or overlaps incorrectly.
- You own the pixels. A
JComponentcan paint anything: anti-aliased shapes, custom text layout, icons, overlays, and more. That freedom is the reasonpaintComponent()exists.
My rule: if you’re writing Swing in 2026, keep the UI tree purely Swing (javax.swing.*) unless you have a specific requirement that forces AWT interop.
Where people still get forced into heavyweight interop:
- Embedded legacy browser components
- Native video playback surfaces
- Hardware-accelerated canvases
If you do mix, I assume you’ll need to test z-order, focus, and transparency on every target platform. That’s not fear-mongering—just the actual cost.
Painting model: from repaint() to paintComponent()
Painting is where beginners either fall in love with Swing or decide it’s “too weird.” It becomes much friendlier when you internalize one idea:
A JComponent doesn’t paint when you want. It paints when Swing decides it’s safe and necessary.
Think of repaint() as “please schedule a redraw” rather than “draw now.” Swing coalesces repaint requests and paints later on the Event Dispatch Thread (EDT).
The paint() pipeline
You’ll see three key methods in typical custom components:
paintComponent(Graphics g)— paint the interiorpaintBorder(Graphics g)— paint the border (if any)paintChildren(Graphics g)— paint child components
Swing typically calls paint(Graphics g) which calls those in order. In custom components, I almost always override paintComponent() rather than paint().
Why overriding paint() is a trap:
- You can accidentally skip border/children painting.
- You can break opacity handling.
- You can interfere with look-and-feel painting rules.
A good paintComponent() skeleton
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g); // clears background when opaque
Graphics2D g2 = (Graphics2D) g.create();
try {
g2.setRenderingHint(
java.awt.RenderingHints.KEY_ANTIALIASING,
java.awt.RenderingHints.VALUEANTIALIASON
);
// Your drawing here
} finally {
g2.dispose();
}
}
Why I like g.create() + dispose():
- It isolates transforms, clips, composite modes, and rendering hints.
- It prevents “mystery bugs” where one paint operation affects the next.
Opaqueness, background clearing, and flicker
Two facts that prevent a lot of visual glitches:
- If your component is opaque (
setOpaque(true)), Swing expects you to fully paint the background. Callingsuper.paintComponent(g)does that background clear for you. - If your component is non-opaque (
setOpaque(false)), Swing assumes you’re painting with transparency and the parent might show through.
If you see trails or smearing during animation, it’s usually one of:
- you forgot
super.paintComponent(g)on an opaque component, - you’re repainting too small a region or too often,
- you’re drawing outside your clip bounds,
- you’re doing slow work on the EDT.
Performance guidance (realistic, not magical)
On modern machines, small components can repaint in a few milliseconds, but once you start painting lots of text, gradients, images, or you have many components repainting together, you can drift into a 10–25ms frame range and the UI feels “sticky.”
My tactics, in order:
- Paint less. Repaint only the region that changed (use
repaint(x, y, w, h)when it’s easy). - Compute less during paint. Precompute shapes/colors; cache expensive layout calculations.
- Move work off the EDT. Fetch data and compute metrics in a background thread; publish results back to the EDT.
If you remember just one thing: painting should be fast and side-effect free.
A rule I live by: never allocate big objects inside paintComponent() if I can avoid it. A few Color and Shape allocations won’t end the world, but if you create fonts, parse text, load images, hit disk, or call network APIs from paint, you’ll feel it.
Sizing and layout: preferred size, alignment, borders, insets
A custom component that paints beautifully but refuses to size correctly is a classic Swing rite of passage.
Swing layout managers generally care about:
getPreferredSize()getMinimumSize()getMaximumSize()- alignment (
getAlignmentX(),getAlignmentY())
I almost never call setSize()
When I see code that uses setSize() or setBounds() in typical Swing layouts, it’s usually fighting the layout manager.
Instead, I do this:
- implement
getPreferredSize()(or callsetPreferredSize()if fixed) - place the component inside a layout manager that respects preferred sizes
- call
revalidate()when the preferred size changes
One subtle but important detail: setPreferredSize() is a blunt instrument. It’s fine for fixed-size widgets (like an icon badge), but for anything dynamic, overriding getPreferredSize() based on current state tends to behave better.
Borders affect layout
A Border consumes space through insets. That means if you draw something “to the edges” without accounting for insets, it can collide with the border.
Pattern I like:
java.awt.Insets insets = getInsets();
int x = insets.left;
int y = insets.top;
int w = getWidth() – insets.left – insets.right;
int h = getHeight() – insets.top – insets.bottom;
Then I draw within (x, y, w, h).
Alignment matters in BoxLayout
If you’ve ever used BoxLayout and watched components stretch weirdly, alignment is usually the reason. Many components default to CENTER_ALIGNMENT. A custom JComponent may need explicit alignment:
setAlignmentX(LEFT_ALIGNMENT);
I don’t force alignment unless the container demands it, but when it does, I set it intentionally.
The “layout contract” I aim for
When I build a reusable component, I try to make it layout-friendly by default:
- It has a sensible preferred size.
- It can grow without looking broken.
- It doesn’t assume a specific font size.
- It respects insets.
That way it works inside BorderLayout, GridBagLayout, BoxLayout, and most custom layouts without special casing.
Event handling and interaction: listeners vs key bindings
JComponent supports a lot of event handling: mouse, keyboard, focus, hierarchy changes, and more. But the best practice for keyboard input in Swing is not KeyListener.
I choose between two approaches:
What I use it for
—
MouseListener / MouseMotionListener pointer interaction, drag, hover
InputMap/ActionMap) shortcuts, component commands
Why key bindings beat KeyListener
A KeyListener only fires when the component has focus and is focusable, and it’s easy to break when nested inside complex containers.
Key bindings let you say:
- “This action triggers when the component is focused.” (
WHEN_FOCUSED) - “This triggers when the component is in the focused window.” (
WHENINFOCUSED_WINDOW) - “This triggers when an ancestor is focused.” (
WHENANCESTOROFFOCUSEDCOMPONENT)
Here’s a clean pattern:
getInputMap(WHENINFOCUSED_WINDOW).put(
javax.swing.KeyStroke.getKeyStroke("ctrl S"),
"save"
);
getActionMap().put("save", new javax.swing.AbstractAction() {
@Override
public void actionPerformed(java.awt.event.ActionEvent e) {
System.out.println("Save triggered");
}
});
That setup survives refactors and container changes far better than scattered listeners.
Tooltips and “small UX wins”
A custom component can provide tooltips with almost no effort:
setToolTipText("Drag to adjust the threshold");
For advanced cases (different tooltip per region), override getToolTipText(MouseEvent e).
A note on the EDT
Any input handler runs on the EDT. If your handler does networking, file I/O, or heavy computation, the UI freezes.
I keep handlers tiny:
- Update local state
- Trigger repaint/revalidate
- Start a background task if needed
If I need a background task, I usually reach for SwingWorker (or a plain executor + SwingUtilities.invokeLater for UI updates). The key is that state changes that affect painting should land back on the EDT.
Look and feel: UI delegates, updateUI(), and client properties
Swing’s look-and-feel (LAF) system is one of its most misunderstood strengths. JComponent participates in it through a UI delegate (a ComponentUI) referenced by the ui field.
You don’t need to write custom UI delegates for most apps, but you should understand two important ideas:
- Your component should repaint cleanly across LAFs. Don’t assume specific fonts, colors, or metrics unless you set them.
- Use
UIManagerdefaults when possible. It helps your component match the application theme.
Example: pulling a theme-friendly color with a fallback:
java.awt.Color track = javax.swing.UIManager.getColor("ProgressBar.trackColor");
if (track == null) track = new java.awt.Color(230, 230, 230);
updateUI() and dynamic theme changes
If your app supports changing look-and-feel at runtime, Swing calls updateUI() on components. If you cache LAF-derived colors or fonts, you should refresh them there.
A simple approach I use:
- Keep “derived” colors in fields.
- Recompute them in
updateUI(). - Call
repaint().
Client properties: attaching metadata without subclassing everything
Client properties are a quiet superpower:
putClientProperty("telemetry.componentName", "TemperatureGauge");
I use them for:
- UI testing hooks
- analytics tags for internal tools
- feature flags
- storing computed layout hints
Because they’re scoped to the component instance, they’re safer than global maps.
Accessibility: the inner accessible class
JComponent provides an inner accessible type (commonly exposed through getAccessibleContext()). Even if you don’t implement a full accessibility layer, you can improve things quickly:
- set a meaningful name and description
- ensure keyboard access exists for primary actions
That’s not “extra polish” for many enterprise environments—it’s a requirement.
Building a custom component: a runnable temperature gauge
When I build custom components, I start with the smallest interactive surface that still feels real. Here’s a complete JComponent that paints a temperature gauge, supports mouse drag to adjust the value, and supports keyboard shortcuts.
This example runs as-is.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.beans.*;
public class TemperatureGaugeDemo {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("Temperature Gauge");
frame.setDefaultCloseOperation(WindowConstants.EXITONCLOSE);
TemperatureGauge gauge = new TemperatureGauge(-20, 120);
gauge.setValue(72);
JPanel root = new JPanel(new BorderLayout(12, 12));
root.setBorder(BorderFactory.createEmptyBorder(16, 16, 16, 16));
JLabel label = new JLabel("Drag the gauge or use \u2191/\u2193. Press R to reset.");
root.add(label, BorderLayout.NORTH);
root.add(gauge, BorderLayout.CENTER);
frame.setContentPane(root);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
static final class TemperatureGauge extends JComponent {
private final int min;
private final int max;
private int value;
private boolean dragging;
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
// Cached theme-derived colors (updated in updateUI)
private Color trackColor;
private Color fillColor;
private Color textColor;
private Color tickColor;
TemperatureGauge(int min, int max) {
if (min >= max) throw new IllegalArgumentException("min must be < max");
this.min = min;
this.max = max;
this.value = min;
setOpaque(true);
setBackground(Color.WHITE);
setToolTipText("Drag to change temperature");
setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createLineBorder(new Color(210, 210, 210)),
BorderFactory.createEmptyBorder(12, 12, 12, 12)
));
setFocusable(true);
// Accessibility: give screen readers meaningful text
if (getAccessibleContext() != null) {
getAccessibleContext().setAccessibleName("Temperature gauge");
getAccessibleContext().setAccessibleDescription("Adjustable temperature control");
}
MouseAdapter mouse = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
requestFocusInWindow();
dragging = true;
updateValueFromMouse(e);
}
@Override
public void mouseDragged(MouseEvent e) {
if (!dragging) return;
updateValueFromMouse(e);
}
@Override
public void mouseReleased(MouseEvent e) {
dragging = false;
}
};
addMouseListener(mouse);
addMouseMotionListener(mouse);
// Key bindings: arrow keys adjust, R resets
installKeyBindings();
// Focus repaint so we can draw a focus ring
addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
repaint();
}
@Override
public void focusLost(FocusEvent e) {
repaint();
}
});
updateUI();
}
@Override
public void updateUI() {
super.updateUI();
Color uiTrack = UIManager.getColor("ProgressBar.trackColor");
Color uiFill = UIManager.getColor("ProgressBar.foreground");
Color uiText = UIManager.getColor("Label.foreground");
trackColor = uiTrack != null ? uiTrack : new Color(235, 235, 235);
fillColor = uiFill != null ? uiFill : new Color(66, 133, 244);
textColor = uiText != null ? uiText : new Color(30, 30, 30);
tickColor = new Color(160, 160, 160);
repaint();
}
private void installKeyBindings() {
InputMap im = getInputMap(WHEN_FOCUSED);
ActionMap am = getActionMap();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "inc");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "inc");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "dec");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "dec");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_R, 0), "reset");
am.put("inc", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
setValue(getValue() + 1);
}
});
am.put("dec", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
setValue(getValue() – 1);
}
});
am.put("reset", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
setValue(min);
}
});
}
public int getValue() {
return value;
}
public void setValue(int newValue) {
int clamped = Math.max(min, Math.min(max, newValue));
if (clamped == value) return;
int old = value;
value = clamped;
// Notify observers (useful in real apps)
pcs.firePropertyChange("value", old, value);
// Redraw; for something like this, full repaint is fine
repaint();
// Tooltip is a small UX boost for precise values
setToolTipText("Temperature: " + value);
}
public void addValueChangeListener(PropertyChangeListener l) {
pcs.addPropertyChangeListener("value", l);
}
public void removeValueChangeListener(PropertyChangeListener l) {
pcs.removePropertyChangeListener("value", l);
}
private void updateValueFromMouse(MouseEvent e) {
Insets in = getInsets();
int x = in.left;
int y = in.top;
int w = getWidth() – in.left – in.right;
int h = getHeight() – in.top – in.bottom;
// Vertical slider logic: top = max, bottom = min
int trackTop = y + 18;
int trackBottom = y + h – 18;
int mouseY = e.getY();
int clampedY = Math.max(trackTop, Math.min(trackBottom, mouseY));
double t = 1.0 – (clampedY – trackTop) / (double) Math.max(1, (trackBottom – trackTop));
int newValue = (int) Math.round(min + t * (max – min));
setValue(newValue);
}
@Override
public Dimension getPreferredSize() {
// A reasonable default that still allows resizing
return new Dimension(260, 220);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
try {
g2.setRenderingHint(RenderingHints.KEYANTIALIASING, RenderingHints.VALUEANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEYTEXTANTIALIASING, RenderingHints.VALUETEXTANTIALIAS_ON);
Insets in = getInsets();
int x = in.left;
int y = in.top;
int w = getWidth() – in.left – in.right;
int h = getHeight() – in.top – in.bottom;
// Background for the content area (lets border stay clean)
g2.setColor(getBackground());
g2.fillRect(x, y, w, h);
// Layout
int trackW = Math.max(16, w / 6);
int trackX = x + (w – trackW) / 2;
int trackTop = y + 18;
int trackBottom = y + h – 18;
int trackH = Math.max(1, trackBottom – trackTop);
RoundRectangle2D track = new RoundRectangle2D.Double(trackX, trackTop, trackW, trackH, trackW, trackW);
g2.setColor(trackColor);
g2.fill(track);
// Fill
double fraction = (value – min) / (double) (max – min);
fraction = Math.max(0.0, Math.min(1.0, fraction));
int fillH = (int) Math.round(trackH * fraction);
int fillY = trackBottom – fillH;
RoundRectangle2D fill = new RoundRectangle2D.Double(trackX, fillY, trackW, fillH, trackW, trackW);
g2.setColor(fillColor);
g2.fill(fill);
// Ticks + labels
g2.setColor(tickColor);
int tickCount = 5;
for (int i = 0; i <= tickCount; i++) {
double tt = i / (double) tickCount;
int ty = (int) Math.round(trackTop + (1.0 – tt) * trackH);
int tickLen = 10;
g2.drawLine(trackX + trackW + 8, ty, trackX + trackW + 8 + tickLen, ty);
}
// Value label
String text = value + "\u00B0";
g2.setColor(textColor);
Font font = getFont().deriveFont(Font.BOLD, Math.max(14f, getFont().getSize2D() * 1.2f));
g2.setFont(font);
FontMetrics fm = g2.getFontMetrics();
int textW = fm.stringWidth(text);
int textX = x + (w – textW) / 2;
int textY = y + fm.getAscent();
g2.drawString(text, textX, textY);
// Focus ring (simple and theme-neutral)
if (isFocusOwner()) {
g2.setColor(new Color(66, 133, 244, 120));
g2.setStroke(new BasicStroke(2f));
int pad = 2;
g2.drawRect(x + pad, y + pad, w – pad 2 – 1, h – pad 2 – 1);
}
} finally {
g2.dispose();
}
}
}
}
What this example demonstrates (beyond “it paints”):
- It exposes a small API (
getValue,setValue, listener registration) so other code can treat it like a real widget. - It uses key bindings instead of a
KeyListener. - It’s focusable and draws a focus ring, which makes keyboard interaction discoverable.
- It respects
Insets, so the border isn’t an afterthought. - It uses
updateUI()to re-derive theme-dependent colors.
In a production app, I typically go one step further: separate the data model from the component so the same model can drive multiple views (more on that below).
When to use JComponent (and when not to)
I love custom JComponent work, but I don’t default to it. I ask a simple question:
Is the behavior mostly visual (custom rendering) or mostly structural (composition of existing widgets)?
Good reasons to build a custom JComponent
- You need custom painting (charts, gauges, timelines, overlays).
- You need unified interaction across platforms (drag behavior, hit testing, keyboard shortcuts).
- You want a reusable widget with a clean API.
- You need performance that’s hard to get from layering many stock components.
Reasons I avoid it
- The UI is just a layout problem. Compose existing components (
JPanel+JLabel+JButton) first. - You need rich text editing, accessibility-heavy text interaction, or complex selection behavior. Swing already has deep solutions (
JTextComponent, tables, trees). Recreating them is expensive. - Your component is really “an app inside the app.” At that point, a dedicated view framework (or even a separate process) may be a better trade-off.
There’s no shame in choosing composition over custom painting. In fact, it’s often the more maintainable choice.
Common JComponent pitfalls (and how I avoid them)
These mistakes are common because they feel intuitive—until they blow up.
1) Doing work in paintComponent()
I never fetch data, hit disk, parse JSON, or do heavy computation inside paint. Paint must be fast.
Fix: compute elsewhere, store results in fields, and call repaint() when the results change.
2) Calling getGraphics() to draw “right now”
getGraphics() can return a context that isn’t stable, can be clipped unexpectedly, and the drawing can vanish on the next repaint.
Fix: update state and call repaint().
3) Forgetting super.paintComponent(g) (opaque components)
This leads to trails and artifacts.
Fix: call it, unless you have a very deliberate reason not to—and even then, fully paint your background.
4) Not handling high-DPI / scaling gracefully
If you hardcode pixels everywhere (and never use font metrics or relative sizing), your component can look cramped or blurry on scaled displays.
Fix: base sizes on available width/height and font size. Use Graphics2D with antialiasing, and avoid scaling tiny bitmaps.
5) Listener leaks
If your custom component registers listeners on global objects (timers, models, event buses), it can prevent garbage collection.
Fix: unregister listeners in removeNotify() (and re-register in addNotify()), or scope listeners to the component’s lifetime.
6) Breaking focus and keyboard navigation
A component that reacts to keys but can’t gain focus is frustrating.
Fix: set setFocusable(true), request focus on click, and draw a focus indicator.
Deeper painting: clipping, transforms, and hit testing
Once you’ve built your first custom component, you quickly run into three practical questions:
- How do I keep painting inside bounds?
- How do I scale or rotate drawings cleanly?
- How do I map mouse coordinates to what I drew?
Clip awareness
Swing sets a clip on Graphics for repaint regions. If you paint outside it, you’re wasting time. I generally let Swing manage the clip, but if I intentionally draw only a small region, I’ll use repaint(x, y, w, h) to encourage tight clips.
Transforms
Transforms are powerful (zooming, rotating, scaling for animation), but they’re also an easy way to create “why is everything shifted?” bugs.
My habit is:
- Always use
Graphics2D g2 = (Graphics2D) g.create(). - Apply transforms to
g2. - Dispose
g2.
That isolates state.
Hit testing
If your component has multiple interactive subregions (like a mini timeline with draggable handles), you’ll want hit testing.
I like to keep a small set of rectangles or shapes as fields:
- During layout/paint preparation, compute the shapes.
- In mouse handlers, test
shape.contains(e.getPoint()).
This avoids duplicating geometry logic in two places.
The EDT, animation, and “smooth enough” updates
Custom components often become animated components: progress indicators, spinners, charts, live monitors.
The safest Swing animation tool is javax.swing.Timer because it fires on the EDT.
My approach:
- Store animation state (phase, offset, alpha) in fields.
- Use a
Timerto update those fields. - Call
repaint().
I intentionally avoid trying to hit a perfect FPS target. For typical business apps, “smooth enough” is usually in the range of ~30–60Hz depending on the complexity of painting and the user’s machine.
One anti-pattern: using Thread.sleep() on the EDT to drive animation. That will freeze the whole UI.
If animation work becomes heavy (large charts, expensive layout), I move computation off the EDT and repaint only when I have new data.
State, models, and property changes: making components reusable
The difference between a “cool demo component” and a “production widget” is almost always API design.
The bare minimum I want:
- getters/setters for state
- a clear event when state changes
- predictable repaint/revalidate behavior
Property change support
Swing plays nicely with JavaBeans conventions. Using PropertyChangeSupport makes it straightforward for other code to observe changes.
In the gauge example above, pcs.firePropertyChange("value", old, value) is what lets a parent panel update a label, write a preference, or record telemetry—without the component needing to know anything about it.
Repaint vs revalidate
I use this rule:
- If only pixels change:
repaint(). - If size/layout could change:
revalidate()(and usuallyrepaint()as well).
For example:
- Changing a color: repaint.
- Changing a label that affects preferred size: revalidate + repaint.
Threading rule for state changes
If a background worker updates component state, I marshal it to the EDT:
- Use
SwingUtilities.invokeLater(() -> setValue(x)).
This keeps paint and state coherent.
Practical scenarios you’ll actually run into
Here are a few common situations where JComponent becomes the right tool, plus the “gotchas” I expect.
Scenario 1: A status badge with custom shapes and theme-aware colors
You want a pill-shaped badge with “ONLINE”, “DEGRADED”, “OFFLINE”, with a subtle gradient and maybe an icon.
What matters:
- calculate padding using font metrics
- use
UIManagercolors when possible - keep a stable preferred size if the layout depends on it
Gotcha: if the text changes length (like “OFFLINE” vs “OK”), the component’s preferred size changes and the whole layout can shift. Sometimes that’s fine. Sometimes it’s annoying. In the annoying case, I fix the preferred width or define a min width.
Scenario 2: A sparkline chart that updates with live data
You want a tiny chart that updates every second.
What matters:
- don’t allocate arrays in paint
- precompute the polyline or
Path2Dwhen data changes - repaint only the region of the chart (or just repaint the component if it’s tiny)
Gotcha: if you redraw the whole dashboard every second because every sparkline repaints at once, you can create a stutter. Group updates or stagger timers.
Scenario 3: A custom control with keyboard shortcuts
You want consistent shortcuts across a UI (like Ctrl/Cmd + arrow to nudge something).
What matters:
- key bindings with the right input map condition (
WHENINFOCUSED_WINDOWis often what you want) - focus ring and focus traversal
Gotcha: if you register global shortcuts on many components, you can get conflicts. I often centralize app-level shortcuts on the root pane or a main panel and dispatch actions to the current selection.
Alternative approaches (so you don’t overbuild)
Sometimes the best JComponent is… no custom component at all.
1) Composition of stock components
If you can build the UI by composing labels, buttons, and panels, do that first. It’s easier to maintain and more accessible by default.
2) Icon or Border customization
If what you need is “a small graphic next to a label” or “a fancy outline,” implementing a custom Icon or Border can be enough. It keeps the logic narrowly scoped and avoids building a full component.
3) Pluggable look-and-feel (UI delegate)
If you’re building a reusable component library with multiple themes, a UI delegate approach can be appropriate. It’s more complex, but it aligns with how many Swing components are structured.
4) Switching UI toolkits
If the app is heavily graphics-driven, you may consider a toolkit designed for modern scene graphs. I don’t treat that as failure—just a trade-off decision. Swing is still very capable, but it has a different “sweet spot.”
Serialization: why I treat it as “sharp”
Yes, JComponent is Serializable. And yes, you can serialize component state. In practice, I rarely do it.
What goes wrong:
- UI trees are large and fragile across versions.
- You can accidentally serialize listeners, models, or references you didn’t intend.
- Look-and-feel differences can create weird restored state.
What I do instead:
- Serialize my own small model objects (plain data).
- Reconstruct UI components from that model.
If you must serialize, do it deliberately and test version-to-version compatibility.
A checklist I use before calling a custom JComponent “done”
This is the stuff that turns a component from “it works on my machine” into “it behaves like a real Swing widget.”
- Painting:
super.paintComponent(g)called when appropriate; no expensive work in paint - Layout: preferred size is reasonable; respects insets; behaves when resized
- Input: mouse interactions feel consistent; keyboard works via key bindings
- Focus: component can receive focus; focus indication is visible
- Threading: no blocking work on the EDT
- Look-and-feel: uses
UIManagerdefaults where sensible; handlesupdateUI() - API: clear getters/setters; fires property changes; minimal surface area
- Accessibility: at least an accessible name/description; keyboard access for core actions
Closing thought
JComponent is one of those classes that quietly rewards you. The first time you override paintComponent(), it feels like you’re drawing on a canvas. The tenth time, you realize you’re building reusable widgets with solid input behavior, clean sizing, and theme integration.
Once you internalize the painting pipeline, the layout contract, and the key binding system, custom Swing UI stops being a “hack” and becomes a normal, dependable tool.
If you want to go further, the next natural step is building a slightly more complex component (like a range slider with two handles), where hit testing, keyboard navigation, and accessibility all matter at once. That’s where JComponent really shines.



