I still reach for Swing when I need a fast, dependable desktop tool that launches instantly and runs everywhere a JVM does. The moment you need a user to pick from a set of items – files, people, tasks, build targets – JList is the simplest surface to start with. Its not flashy, but its consistent, and it scales from tiny lists to real datasets. Ive shipped internal tools where a single JList drove most of the workflow, and the reliability was the whole point.
Youre going to build practical JList examples that you can run immediately, then level them up with selection models, custom rendering, keyboard behavior, and performance-friendly models. Ill show where JList shines, when I avoid it, and the real mistakes I see in production code. Think of JList as a well-organized shelf: you can browse at a glance, you can grab one or many items, and you can replace the whole shelf without redesigning the room.
JList mental model: the object shelf
JList is a Swing component that displays a list of objects and lets a user select one or more items. It extends JComponent and relies on a ListModel to provide data. I like to describe it as a shelf of items; the model owns the items, and the list renders them. When you select an item, youre not changing the shelf, youre just placing a sticky note on it. That mental model keeps you from coupling selection to data storage.
The constructors matter because they imply how your data lives:
- JList() creates an empty list. Youll set a model later.
- JList(E[] l) creates a list backed by a fixed array.
- JList(ListModel d) creates a list backed by a model (best for dynamic data).
- JList(Vector l) creates a list backed by a vector.
If you need updates at runtime – search results, tasks, or logs – use a ListModel. If youre rendering a static list, an array or vector is fine.
A minimal runnable JList example (single selection)
I keep this example in my muscle memory because it shows the absolute minimum wiring. You get a list, you set a default selection, and you show it.
import java.awt.*;
import javax.swing.*;
public class SimpleJListDemo {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("JList basics");
frame.setDefaultCloseOperation(JFrame.EXITONCLOSE);
String[] weekdays = {
"Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday", "Sunday"
};
JList list = new JList(weekdays);
list.setSelectedIndex(2); // Wednesday
JPanel panel = new JPanel();
panel.add(new JLabel("Select a day"));
panel.add(list);
frame.add(panel);
frame.setSize(420, 220);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
Why the SwingUtilities.invokeLater call? Because all Swing UI updates must occur on the Event Dispatch Thread (EDT). If you ignore this, youll get weird intermittent rendering issues that feel like ghosts in your UI.
Selection handling that doesnt get in your way
In practice, you want to react to selections. JList uses a ListSelectionModel and fires ListSelectionEvents. I prefer ListSelectionListener over item listeners because its the idiomatic Swing way and gives you explicit control over "adjusting" events (dragging the mouse across multiple items triggers multiple events).
Heres a practical example: a birthday picker with three lists. It stays simple but demonstrates multi-list selection and updating a label only after the selection stabilizes.
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
public class BirthdayPicker implements ListSelectionListener {
private final JLabel resultLabel = new JLabel(" ");
private final JList monthList;
private final JList dayList;
private final JList yearList;
public BirthdayPicker() {
String[] months = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
};
String[] days = new String[31];
for (int i = 0; i < 31; i++) {
days[i] = String.valueOf(i + 1);
}
String[] years = new String[60];
for (int i = 0; i < 60; i++) {
years[i] = String.valueOf(2026 - i); // recent 60 years
}
monthList = new JList(months);
dayList = new JList(days);
yearList = new JList(years);
monthList.addListSelectionListener(this);
dayList.addListSelectionListener(this);
yearList.addListSelectionListener(this);
monthList.setVisibleRowCount(6);
dayList.setVisibleRowCount(6);
yearList.setVisibleRowCount(6);
}
@Override
public void valueChanged(ListSelectionEvent e) {
if (e.getValueIsAdjusting()) return; // avoid duplicate updates
String m = monthList.getSelectedValue();
String d = dayList.getSelectedValue();
String y = yearList.getSelectedValue();
if (m != null && d != null && y != null) {
resultLabel.setText("Birthday: " + m + " " + d + ", " + y);
}
}
private JComponent buildUI() {
JPanel lists = new JPanel(new GridLayout(1, 3, 10, 0));
lists.add(new JScrollPane(monthList));
lists.add(new JScrollPane(dayList));
lists.add(new JScrollPane(yearList));
JPanel root = new JPanel(new BorderLayout(10, 10));
root.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
root.add(new JLabel("Select your birthday"), BorderLayout.NORTH);
root.add(lists, BorderLayout.CENTER);
root.add(resultLabel, BorderLayout.SOUTH);
return root;
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("Birthday picker");
frame.setDefaultCloseOperation(JFrame.EXITONCLOSE);
frame.setContentPane(new BirthdayPicker().buildUI());
frame.setSize(520, 260);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
The key detail is getValueIsAdjusting() – it tells you whether the selection is still changing. I always gate on it when I update UI or trigger a query, or I end up with double or triple events.
ListModel: the modern way to manage data
JList can be backed by an array, but thats a dead end once data changes. In real tools, your list grows, shrinks, and filters. I recommend DefaultListModel for 80 percent of use cases; its mutable and makes updates obvious.
import javax.swing.*;
import java.awt.*;
public class DynamicListDemo {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("Dynamic list");
frame.setDefaultCloseOperation(JFrame.EXITONCLOSE);
DefaultListModel model = new DefaultListModel();
model.addElement("Build: core-service");
model.addElement("Build: analytics-service");
model.addElement("Deploy: staging");
JList list = new JList(model);
list.setSelectionMode(ListSelectionModel.MULTIPLEINTERVALSELECTION);
JButton add = new JButton("Add task");
add.addActionListener(e -> model.addElement("Run tests: integration"));
JButton remove = new JButton("Remove selected");
remove.addActionListener(e -> {
int[] selected = list.getSelectedIndices();
// remove from end to avoid shifting indices
for (int i = selected.length - 1; i >= 0; i--) {
model.removeElementAt(selected[i]);
}
});
JPanel buttons = new JPanel();
buttons.add(add);
buttons.add(remove);
frame.setLayout(new BorderLayout(10, 10));
frame.add(new JScrollPane(list), BorderLayout.CENTER);
frame.add(buttons, BorderLayout.SOUTH);
frame.setSize(420, 260);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
If you want a truly scalable list that streams data, create your own ListModel or use AbstractListModel. I do that for 10k+ items, or when the list is a view over a database or API results.
Custom rendering: when strings are not enough
The default renderer draws each item as text. Thats fine until you need status icons, colors, or multiple lines. The renderer is a lightweight component that paints each cell, usually a JLabel. You should never instantiate heavy UI inside it; just configure and return a single reusable label.
Heres a renderer that shows a status marker and bold title:
import javax.swing.*;
import java.awt.*;
class TaskItem {
final String name;
final String status; // "OK", "WARN", "FAIL"
TaskItem(String name, String status) {
this.name = name;
this.status = status;
}
@Override
public String toString() {
return name;
}
}
class TaskRenderer extends JLabel implements ListCellRenderer {
TaskRenderer() {
setOpaque(true);
setBorder(BorderFactory.createEmptyBorder(4, 6, 4, 6));
}
@Override
public Component getListCellRendererComponent(
JList list,
TaskItem value,
int index,
boolean isSelected,
boolean cellHasFocus) {
String marker;
switch (value.status) {
case "OK" -> marker = "o";
case "WARN" -> marker = "o";
default -> marker = "o";
}
String text = marker + " " + value.name + " (" + value.status + ")";
setText(text);
if (isSelected) {
setBackground(list.getSelectionBackground());
setForeground(list.getSelectionForeground());
} else {
setBackground(list.getBackground());
// subtle status-based color
setForeground("FAIL".equals(value.status) ? new Color(176, 0, 32) : list.getForeground());
}
return this;
}
}
Then wire it into a list:
import javax.swing.*;
import java.awt.*;
public class CustomRendererDemo {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
DefaultListModel model = new DefaultListModel();
model.addElement(new TaskItem("Deploy API", "OK"));
model.addElement(new TaskItem("Run migration", "WARN"));
model.addElement(new TaskItem("Restart cache", "FAIL"));
JList list = new JList(model);
list.setCellRenderer(new TaskRenderer());
JFrame frame = new JFrame("Custom renderer");
frame.setDefaultCloseOperation(JFrame.EXITONCLOSE);
frame.add(new JScrollPane(list));
frame.setSize(360, 220);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
You can also override getToolTipText(MouseEvent e) on the list to show details per item, which is a great ergonomic win in admin tooling.
Multi-selection: make it deliberate
I see many apps default to single selection even when multi-select would improve workflows. But multi-select has a cost: you must define how actions behave with multiple items. When you enable multi-selection, always wire the UI to show what will happen with more than one item.
list.setSelectionMode(ListSelectionModel.MULTIPLEINTERVALSELECTION);
Then read the selection safely:
java.util.List selected = list.getSelectedValuesList();
if (selected.isEmpty()) {
// disable action buttons
} else if (selected.size() == 1) {
// show detail panel
} else {
// show batch action panel
}
I use this rule: if a batch action is meaningful (delete, export, tag, queue), enable multi-selection and make the action explicit. If its ambiguous (edit details, open a single view), lock it to single selection.
Scrolling, orientation, and visible row count
JList doesnt scroll by itself. You always wrap it in a JScrollPane. This isnt just convenience; it keeps the list responsive and ensures keyboard navigation works as expected.
JScrollPane scrollPane = new JScrollPane(list);
Visible row count is how you signal size without hardcoding pixel heights:
list.setVisibleRowCount(8);
Orientation can be vertical (default), vertical wrap, or horizontal wrap. I only use horizontal wrap for tag-like lists or multi-column selectors.
list.setLayoutOrientation(JList.HORIZONTAL_WRAP);
list.setVisibleRowCount(-1); // let Swing calculate columns
Be careful: a wrapped list without a scroll pane will look broken. Thats a classic beginner mistake.
Selection colors and accessibility
Swing allows you to set selection colors. I only override defaults when I need to match a brand palette or when Im building a high-contrast tool for accessibility.
list.setSelectionBackground(new Color(24, 91, 179));
list.setSelectionForeground(Color.WHITE);
If you use a custom renderer, always respect the selected state as shown earlier. Otherwise, keyboard users cant tell whats selected.
Event handling without UI glitches
The usual trap: you update the list model from a background thread and the UI freezes or throws random exceptions. Swing is single-threaded. When you touch the model, do it on the EDT:
SwingUtilities.invokeLater(() -> model.addElement("New task"));
If youre loading data in the background, use SwingWorker or any executor for the I/O, then publish results on the EDT. In 2026, I still recommend SwingWorker for clarity because it fits the Swing threading model perfectly.
A simple pattern I use:
- Load data on a background thread.
- On completion, update the list model on the EDT.
- Avoid updating the model item-by-item if you can batch the changes.
That reduces UI jitter and makes selection stay stable.
Common mistakes I see (and how you avoid them)
1) Not using a scroll pane: the list clips and selection becomes awkward. Always wrap it.
2) Updating the model off the EDT: causes random UI behavior and makes debugging painful.
3) Heavy renderers: creating new components inside the renderer method slows everything down. Reuse a single renderer component.
4) Ignoring adjusting events: you end up with double event handling and duplicated side effects.
5) Using arrays for dynamic data: you replace the entire list to refresh it. Prefer DefaultListModel or a custom model.
A small analogy I use: the list is a window, not a warehouse. If your data lives in the list, youre locking yourself out of real filtering, sorting, and persistence. The list should be a view, not the source of truth.
Selection models: single, interval, and custom behavior
Most examples show setSelectionMode and stop there, but selection model choice changes your UI flow. There are three built-in modes:
- SINGLE_SELECTION: one item only.
- SINGLEINTERVALSELECTION: a single contiguous range.
- MULTIPLEINTERVALSELECTION: multiple ranges.
If you want a spreadsheet-like experience, MULTIPLEINTERVALSELECTION is what you want. If you want a tight, one-at-a-time control surface, SINGLE_SELECTION keeps your logic simple.
The bigger lesson is that the selection model can be swapped. In advanced tools, I sometimes plug in a custom ListSelectionModel to prevent specific items from being selectable. That is useful for separators or headers in a mixed list. Here is a simple guard that blocks selection for items that start with "[" to simulate section headers:
import javax.swing.*;
class GuardedSelectionModel extends DefaultListSelectionModel {
private final JList list;
GuardedSelectionModel(JList list) {
this.list = list;
}
@Override
public void setSelectionInterval(int index0, int index1) {
if (isSelectable(index0) && isSelectable(index1)) {
super.setSelectionInterval(index0, index1);
}
}
private boolean isSelectable(int index) {
String value = list.getModel().getElementAt(index);
return value != null && !value.startsWith("[");
}
}
I rarely need this, but when I do, it saves me from hacks inside the renderer and keeps selection logic explicit.
Filtering and search without losing your mind
Users expect to type and filter. JTable has built-in sorting and filtering, but JList does not, so you need a small helper model. I use a simple pattern: keep a master list, expose a filtered view, and rebuild on search.
import javax.swing.*;
import java.util.*;
class FilteredListModel extends AbstractListModel {
private final java.util.List allItems = new ArrayList();
private final java.util.List visibleItems = new ArrayList();
public void setItems(java.util.List items) {
allItems.clear();
allItems.addAll(items);
applyFilter("");
}
public void applyFilter(String query) {
visibleItems.clear();
String q = query == null ? "" : query.toLowerCase();
for (String item : allItems) {
if (item.toLowerCase().contains(q)) {
visibleItems.add(item);
}
}
fireContentsChanged(this, 0, getSize());
}
@Override
public int getSize() {
return visibleItems.size();
}
@Override
public String getElementAt(int index) {
return visibleItems.get(index);
}
}
Then wire it with a text field:
import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.util.*;
public class FilteredListDemo {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
FilteredListModel model = new FilteredListModel();
model.setItems(Arrays.asList(
"Deploy API", "Deploy Web", "Run migrations", "Roll back",
"Restart cache", "Smoke tests", "Build client"
));
JList list = new JList(model);
JTextField search = new JTextField();
search.getDocument().addDocumentListener(new DocumentListener() {
public void insertUpdate(DocumentEvent e) { model.applyFilter(search.getText()); }
public void removeUpdate(DocumentEvent e) { model.applyFilter(search.getText()); }
public void changedUpdate(DocumentEvent e) { model.applyFilter(search.getText()); }
});
JPanel root = new JPanel(new BorderLayout(8, 8));
root.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
root.add(new JLabel("Search tasks"), BorderLayout.NORTH);
root.add(search, BorderLayout.CENTER);
root.add(new JScrollPane(list), BorderLayout.SOUTH);
JFrame frame = new JFrame("Filtered JList");
frame.setDefaultCloseOperation(JFrame.EXITONCLOSE);
frame.setContentPane(root);
frame.setSize(360, 280);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
This is not as slick as a JTable filter, but it is predictable. Two tips: preserve selection if the selected item is still visible, and debounce input for very large datasets.
Keyboard navigation and custom actions
JList comes with decent keyboard defaults: arrows to move, space to select, and type-ahead to jump to items that start with the typed prefix. When I need more control, I add bindings with InputMap and ActionMap. Here is an example that opens the selected item with Enter and clears selection with Escape:
import javax.swing.*;
import java.awt.event.*;
public class ListKeyBindings {
static void wire(JList list) {
InputMap im = list.getInputMap(JComponent.WHEN_FOCUSED);
ActionMap am = list.getActionMap();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "open");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "clear");
am.put("open", new AbstractAction() {
public void actionPerformed(ActionEvent e) {
String value = list.getSelectedValue();
if (value != null) {
JOptionPane.showMessageDialog(list, "Open: " + value);
}
}
});
am.put("clear", new AbstractAction() {
public void actionPerformed(ActionEvent e) {
list.clearSelection();
}
});
}
}
That small layer makes JList feel like a first-class control surface, not just a passive display.
Mouse behavior: double-clicks and context menus
Most power users will double-click and right-click even if you never tell them to. I wire both, but I keep actions consistent. Double-click should open or preview. Right-click should show context actions for the current selection.
list.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseClicked(java.awt.event.MouseEvent e) {
if (e.getClickCount() == 2) {
int idx = list.locationToIndex(e.getPoint());
list.setSelectedIndex(idx);
String value = list.getSelectedValue();
if (value != null) {
JOptionPane.showMessageDialog(list, "Open: " + value);
}
}
}
});
For a context menu, I build a JPopupMenu once and show it on mouse pressed or released depending on the platform. The key idea is to select the row under the mouse before opening the menu, so actions apply to the expected item.
Drag and drop: lightweight reordering
JList supports drag and drop with a TransferHandler. I only add it when the list is a true ordering tool, like a playlist or build pipeline. For read-only lists, drag and drop is noise.
A minimal reorder handler looks like this:
list.setDragEnabled(true);
list.setDropMode(DropMode.INSERT);
list.setTransferHandler(new TransferHandler("selectedValue"));
This is the lightest possible approach. For production, I usually implement a custom TransferHandler that moves items inside the DefaultListModel so the underlying data changes, not just the display.
Large data: performance and perception
JList can handle large lists surprisingly well, but you need to respect how it works. It renders only visible rows, but it still queries the model for size and cell values on demand. That means two things:
- Your getSize and getElementAt must be fast.
- Your renderer must be lightweight and avoid allocations.
For very large datasets (tens of thousands of items), I usually:
- Use AbstractListModel with cached size.
- Lazy-load item strings or details.
- Avoid complex renderers; keep to one label, one icon at most.
- Batch updates (fireContentsChanged once per batch).
In practice, that keeps scrolling smooth. If you do heavy work in getElementAt, you will feel it immediately.
Custom models with AbstractListModel
When DefaultListModel stops being enough, I reach for AbstractListModel. It gives you control over where data comes from without forcing you to store everything in memory. Here is a simple model that wraps a java.util.List and exposes add and remove operations while keeping events explicit:
import javax.swing.*;
import java.util.*;
class SimpleListModel extends AbstractListModel {
private final java.util.List items = new ArrayList();
public void add(T item) {
int index = items.size();
items.add(item);
fireIntervalAdded(this, index, index);
}
public void removeAt(int index) {
items.remove(index);
fireIntervalRemoved(this, index, index);
}
public void setAll(java.util.List newItems) {
items.clear();
items.addAll(newItems);
fireContentsChanged(this, 0, getSize());
}
@Override
public int getSize() {
return items.size();
}
@Override
public T getElementAt(int index) {
return items.get(index);
}
}
That model is often enough to support filtering, sorting, and remote refreshes without leaking Swing details into your business logic.
Sorting a JList without a framework
JList does not sort for you. If you need sorting, sort the data before you put it into the model. I usually keep a master list, sort it with a Comparator, and call setAll:
List sorted = new ArrayList(items);
sorted.sort(String::compareToIgnoreCase);
model.setAll(sorted);
If you support multiple sort orders, keep the master list unsorted and derive sorted lists on demand. That keeps filtering and sorting decoupled.
Empty states and placeholders
An empty list can feel like a bug unless you tell the user what is happening. I handle this in two ways:
- Show an empty state label next to the list when size is zero.
- Provide a placeholder in the renderer when the model is empty.
I prefer the first approach because it keeps your model honest. Here is a small pattern:
JLabel empty = new JLabel("No tasks yet. Click Add to create one.");
empty.setHorizontalAlignment(SwingConstants.CENTER);
model.addListDataListener(new javax.swing.event.ListDataListener() {
public void intervalAdded(javax.swing.event.ListDataEvent e) { toggle(); }
public void intervalRemoved(javax.swing.event.ListDataEvent e) { toggle(); }
public void contentsChanged(javax.swing.event.ListDataEvent e) { toggle(); }
private void toggle() { empty.setVisible(model.getSize() == 0); }
});
Then stack the label behind the list in a CardLayout or layered panel. The UX improvement is real, especially in admin tools.
Persisting selection across updates
If you rebuild the model, selection resets. That frustrates users, especially in lists that refresh every few seconds. The easiest fix is to save the selected value before you update and restore it after:
String selected = list.getSelectedValue();
model.setAll(newItems);
if (selected != null) {
list.setSelectedValue(selected, true);
}
This assumes unique values. If your items can repeat, store a stable id inside the object and locate it after refresh. That is another reason I like list items to be objects, not just strings.
Testing and debugging JList code
UI code is notoriously hard to test, but you can still keep your list logic clean. My approach:
- Test the list model in isolation (pure Java, no Swing).
- Keep renderer logic small and deterministic.
- For UI flows, run manual scripts and verify keyboard and selection behavior.
If you use a custom model, it is easy to write unit tests that add items, remove items, and assert size. That catches most bugs before you even open a window.
When not to use JList
JList is not a hammer for every problem. I avoid it when:
- I need columnar data with headers (use JTable).
- I need a tree of nested data (use JTree).
- I need a compact drop-down control (use JComboBox).
- I need complex inline editing per row (JTable or custom panel).
If you start forcing a JList to act like a table, you will re-implement half of JTable and regret it. Pick the simplest control that matches the mental model.
Practical scenario: a lightweight deployment queue
Here is a more complete example that ties together selection, model updates, and actions. Its the kind of tool I actually build: a simple queue manager.
import javax.swing.*;
import java.awt.*;
public class DeployQueueDemo {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
DefaultListModel model = new DefaultListModel();
model.addElement("Deploy: auth-service");
model.addElement("Deploy: billing-service");
model.addElement("Restart: edge-cache");
JList list = new JList(model);
list.setSelectionMode(ListSelectionModel.MULTIPLEINTERVALSELECTION);
JButton promote = new JButton("Promote to top");
JButton delete = new JButton("Remove");
promote.addActionListener(e -> {
java.util.List selected = list.getSelectedValuesList();
if (selected.isEmpty()) return;
for (String s : selected) {
model.removeElement(s);
}
for (int i = selected.size() - 1; i >= 0; i--) {
model.add(0, selected.get(i));
}
list.setSelectedIndices(new int[] {0});
});
delete.addActionListener(e -> {
int[] idx = list.getSelectedIndices();
for (int i = idx.length - 1; i >= 0; i--) {
model.remove(idx[i]);
}
});
JPanel actions = new JPanel(new FlowLayout(FlowLayout.RIGHT));
actions.add(promote);
actions.add(delete);
JFrame frame = new JFrame("Deploy queue");
frame.setDefaultCloseOperation(JFrame.EXITONCLOSE);
frame.setLayout(new BorderLayout(8, 8));
frame.add(new JScrollPane(list), BorderLayout.CENTER);
frame.add(actions, BorderLayout.SOUTH);
frame.setSize(420, 260);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
This small demo reveals a real workflow issue: if you promote multiple items, ordering matters. In production, I usually keep the relative order of selected items and show a small toast explaining what happened. It sounds minor, but it prevents user confusion.
Edge cases that sneak into production
Here are the subtle issues that show up in real deployments:
- Null values in the model: the renderer must handle them or you get NullPointerException.
- Mutable list items: if you mutate objects without firing events, the list never repaints.
- Inconsistent equals: if you restore selection by value, inconsistent equals breaks it.
- Long strings: they can blow out row height or make selection look broken.
- Frequent refresh: selection and scroll position jump unless you preserve them.
I treat these as checklist items before shipping any list-heavy UI.
Practical renderer tips I use every time
A good renderer makes the list feel crisp and modern. A few guidelines I follow:
- Use a single component; do not create new components per row.
- Respect selection colors from the list, not from the renderer.
- Keep padding consistent with setBorder.
- Avoid heavy HTML in setText; it slows layout and paints.
- If you need icons, load them once and reuse them.
If the renderer feels sluggish, profile it by adding a timestamp and counting calls. You will be surprised how often it runs.
Alternatives and hybrids
Sometimes a list is just part of the experience. I often pair a JList on the left with a detail panel on the right. In that layout, JList is a navigation rail, not the destination. A typical pattern looks like:
- JList for navigation
- Details panel for editing
- Toolbar actions above the list
When people say Swing feels old, what they usually mean is the layout is dated. Modern layouts with JList still feel clean if spacing and typography are thoughtful.
Realistic performance expectations
In day-to-day tools, JList is fast enough for thousands of items. Beyond that, you will feel it, but you can still keep it smooth with the right model. I do not quote exact numbers because every machine differs, but the range is clear: for a few thousand items, default models are fine; for tens of thousands, custom models and light renderers matter; for hundreds of thousands, consider paging or a different UI pattern.
A quick checklist before you ship
- Data model is separate from selection.
- Model changes happen on the EDT.
- List is inside a JScrollPane.
- Renderer respects selection colors.
- Keyboard actions are defined for key workflows.
- Selection is preserved across refreshes when it should be.
Final take
JList is a small, reliable tool in the Swing toolbox. It does one thing well: present a list of items and let the user choose. If you treat it as a view, keep its model clean, and keep the renderer light, it will carry a surprising amount of workflow. I still use it because it is predictable, fast, and easy to reason about. In a world of complex UI stacks, that simplicity is worth a lot.


