IntelliJ Platform Plugin SDK Help

Kotlin UI DSL Version 2

Kotlin UI DSL Version 2 allows creating UI forms with input components bound to state objects. The forms are built by using a declarative Kotlin syntax and follow the official IntelliJ Platform UI conventions described in the UI Guidelines. The library is written in Kotlin and makes it easy to develop user interfaces like dialogs and settings pages.

The Kotlin UI DSL is not intended to build general UIs, like tool windows controls that trigger some actions and do not contain any input components bound to state objects. For this purpose, use custom Swing components from the IntelliJ Platform or the standard ones.

The Kotlin UI DSL Version 2 functions are located in the com.intellij.ui.dsl.builder package.

UI DSL Examples

It is highly recommended taking a look at the UI DSL demo available via Tools | Internal Actions | UI | UI DSL Showcase (see Internal Actions if not available in your IDE instance).

It describes some UI DSL basics and contains explanations, tips, a list of all available components, and many examples with links to the source code.

UI DSL Basics

UI DSL Showcase Tab: Basics (Sources: DemoBasics)

See the following simple example of UI DSL:

panel { row("Enter value:") { textField() } }

Building content of any form starts from panel { which returns DialogPanel filled with components described inside the panel block. A panel consists of any number of rows marked with row tag created vertically from top to bottom.

Every row consists of cells where the last cell in a row occupies the remaining width. Inside one row, cells are added from left to right in the same order calls to factory methods or cell() appear in each row. Cells can contain one component or a sub-panel.

If there are unoccupied cells at the end of a row, they are merged into one cell with the last non-empty cell.


Panel is the start interface for building content. It can consist of several rows and different UI groups.


Adds row with the label if present.


UI DSL Showcase Tab: Gaps (Sources: DemoGaps)

Adds standard left indent:

row { label("Not indented row") } indent { row { label("Indented row") } }


UI DSL Showcase Tab: Groups (Sources: DemoGroups)

Adds horizontal line separator with an optional title.


Creates a sub-panel that occupies the whole width and uses its own grid inside.


Creates grouped rows range to perform operations on them like enabled, enabledIf etc. All rows use the parent grid.

UI DSL Showcase Tab: Groups (Sources: DemoGroups)

Adds a panel with an independent grid, optional title, and some vertical space above and below the group.

group("Title") { row("Row:") { textField() } }


Similar to method but uses the same grid as the parent.


UI DSL Showcase Tab: Groups (Sources: DemoGroups)

Adds collapsible panel with independent grid, title, and some vertical space above and below the group.

collapsibleGroup("Title") { row("Row:") { textField() } }


UI DSL Showcase Tab: Groups (Sources: DemoGroups)

Unions Row.radioButton in one group. Must also be used for Row.checkBox if these are grouped with some title.

var value = true buttonsGroup("Panel.buttonsGroup:") { row { radioButton("true", true) } row { radioButton("false", false) } }.bind({ value }, { value = it })


Registers callbacks that will be called from DialogPanel.apply()/reset()/isModified() methods.


Every row is represented by the Row interface. It contains all available factory methods for creating components (like button(), label(), textField(), etc.) and methods for row configuration.


UI DSL Showcase Tab: Row Layout (Sources: DemoRowLayout)

There are three possible layouts:

  • INDEPENDENT: all cells of the row (including label if present) independent of the parent grid. That means this row has its own grid.

  • LABEL_ALIGNED: label is aligned with the parent grid, other components independent of the parent grid.

  • PARENT_GRID: all components, including label (if present), are in the parent grid.

The default value is LABEL_ALIGNED when a label is provided for the row, INDEPENDENT otherwise.

row("Label:") { textField() } row("Long label:") { textField() }

Here both labels are aligned together because rows have LABEL_ALIGNED by default. If an independent layout is needed, then INDEPENDENT layout should be used:

row("Long label:") { textField() }.layout(RowLayout.INDEPENDENT)


The row becomes resizable and occupies all vertical free space. For several resizable rows, extra free space is divided between rows equally.


UI DSL Showcase Tab: Comments (Sources: DemoComments)

Adds comment after the row with appropriate color and font. Visibility and enabled state of the row affects row comment as well.


Adds component. Use it only for custom specific components, all standard components like label(), button(), checkbox() etc. are covered by dedicated Row factory methods.

For example, there is no method for password field so that the following code can be used:

val passwordField = JPasswordField() row { cell(passwordField) }


Adds component wrapped with JBScrollPane.


Adds additional gap above/below the current row. It is visible together with the row. By default, NONE is used. Between unrelated settings, SMALL can be used. MEDIUM is used between groups and usually set automatically by group() method and similar ones.


UI DSL Showcase Tab: Enabled/Visible (Sources: DemoAvailability)

Sets visibility/enabled state of the row, including row comment (see Row.rowComment) and all children recursively. The row is invisible/disabled if there is an invisible/disabled parent.


UI DSL Showcase Tab: Enabled/Visible (Sources: DemoAvailability)

Binds row visibility/enabled state to the provided predicate. Below is an example of a checkbox whose enabled state depends on another checkbox:

lateinit var checkBox: Cell<JBCheckBox> row { checkBox = checkBox("Check to enable option") } row { checkBox("Option 1") }.enabledIf(checkBox.selected)


UI DSL Showcase Tab: Groups (Sources: DemoGroups)

Creates a sub-panel inside the cell of the row. The panel contains its own set of rows and cells. For example, it is possible to create several columns by creating a row with several panels inside.


Every component in the UI DSL builder is wrapped into Cell class. Standard components should not be created directly but with factory methods from Row class like checkBox(), button() and others because of additional functionality, e.g. textField() is configured with columns width, radio buttons are placed into radio buttons groups.


JComponent that occupies the cell.


Sets horizontal/vertical alignment of content inside the cell.

row("Row:") { textField() .horizontalAlign(HorizontalAlign.FILL) }



Updates horizontal and/or vertical alignment of the component inside the cell. Default alignment is AlignX.LEFT and AlignY.CENTER.

To stretch the content on whole cell, use AlignX.FILL/AlignY.FILL/Align.FILL. For setting both horizontal and vertical alignment, use Align constants or overloaded plug operator like align(AlignX.LEFT + AlignY.TOP).

row("Row:") { textField() .align(AlignX.FILL) }


UI DSL Showcase Tab: Tips (Sources: DemoTips)

Marks column of the cell as resizable: the column occupies all extra horizontal space in panel and changes size together with the panel. It's possible to have several resizable columns, which means extra space is shared between them. There is no need to set resizable for cells in different rows but in the same column: it has no additional effect. Note that the size and placement of components in columns are managed by Cell.align (Cell.horizontalAlign/verticalAlign for pre-2022.3).

row("Row") { textField() .resizableColumn() link("Config...") {} }

UI DSL Showcase Tab: Gaps (Sources: DemoGaps)

Separates the next cell in the current row with rightGap. RightGap.SMALL gap is set after row label automatically by Panel.row() methods.

Below are some cases where RightGap.SMALL should be used:

row { val checkBox = checkBox("Use mail:") .gap(RightGap.SMALL) textField() } row("Width:") { textField() .gap(RightGap.SMALL) label("pixels") }


UI DSL Showcase Tab: Enabled/Visible (Sources: DemoAvailability)

Sets visibility/enabled state of the cell and all children recursively. The cell is invisible/disabled if there is an invisible/disabled parent.


UI DSL Showcase Tab: Enabled/Visible (Sources: DemoAvailability)

Binds cell visibility/enabled state to the provided predicate.

row { val mailCheckBox = checkBox("Use mail:") .gap(RightGap.SMALL) textField() .enabledIf(mailCheckBox.selected) }


UI DSL Showcase Tab: Comments (Sources: DemoComments)

Adds comment under the cell aligned by left edge with appropriate color and font size (macOS uses smaller font). Comment can contain HTML tags except <html>, which is added automatically. The comment occupies the available width before the following comment (if present) or the whole remaining width. Visibility and enabled state of the cell affect comment as well.

row("Label:") { textField() .comment("Comment for textField") }


UI DSL Showcase Tab: Components Labels (Sources: DemoComponentLabels)

Adds label at the specified position. LabelPosition.TOP labels occupy available width before the next top label (if present) or the whole remaining width. Visibility and enabled state of the cell affect the label as well.

row { textField() .label("Cell label on top:", LabelPosition.TOP) }


Registers callbacks that will be called for component from DialogPanel.apply()/reset()/isModified() methods.


Used as a reserved cell in the layout. It can be created by Row.placeholder() method and populated by content later via component property or reset to null.


It is possible to bind component values to properties with the following methods.


Binds component value changing to property. The property is updated when the value is changed and is not related to DialogPanel.apply(). The method is rarely used directly. There are many extensions for specific components described in Cell.bind.


UI DSL Showcase Tab: Binding (Sources: DemoBinding)

Binds component value that is provided by componentGet and componentSet methods to specified binding property. The property is applied only when DialogPanel.apply() is invoked. Methods DialogPanel.isModified() and DialogPanel.reset() are also supported automatically for bound properties.

The bind() method is rarely used directly. There are many extensions for specific components, see following example:

row { checkBox("Checkbox") .bindSelected(model::checkbox) } row("textField:") { textField() .bindText(model::textField) } row("intTextField(0..100):") { intTextField() .bindIntText(model::intTextField) } row("comboBox:") { comboBox(Color.values()) .bindItem(model::comboBoxColor) } row("slider:") { slider(0, 100, 10, 50) .bindValue(model::slider) } row("spinner:") { spinner(0..100) .bindIntValue(model::spinner) } buttonsGroup(title = "radioButton:") { for (value in Color.values()) { row { radioButton(, value) } } }.bind(model::radioButtonColor)

Version 1 and 2 Comparison

In UI DSL version 2, some crucial problems from version 1 have been fixed, so porting is highly desirable. See Migration from Version 1 on how to port existing UI DSL code from version 1 to version 2 API. Version 1 is deprecated and will be removed in future platform releases.

The following significant changes were made:

  • Reduced API, which allows conceiving API easier and faster. Example: there were 5 overloaded methods Cell.checkBox() in version 1, now only one method remains. Functionality for binding properties is extracted into Cell<T>.bindSelected() methods.

  • UI DSL became stricter, so the available API in every context is much smaller. Example: code like row { row { is forbidden now.

  • Structured API mostly based on interfaces, because it's easier to learn API by grouped methods. Only a small part of API is implemented as extensions.

  • KDoc is widely used.

  • MIG layout is fully removed from the new UI DSL and replaced by GridLayout. Because MIG layout is an external library, it's hard to fix bugs there (e.g., there are layout problems when components become invisible) and extend its functionality. Fixed focus ring cropping problems: when components are placed near the panel border focus ring could be cropped if panel insets do not specify enough space.

  • Implemented Placeholder that allows replacing components at runtime after content is shown.

Migration from Version 1

New API is very similar to the old one and covers almost all functionality now, so moving to the new version can be done quickly.

Version 1 is placed in com.intellij.ui.layout package.

Version 2 is placed in com.intellij.ui.dsl.builder package.

Migration Steps

  1. Having a screenshot or live version of the initial components layout can help

  2. Remove imports of old UI DSL starting with com.intellij.ui.layout

  3. Go to the place where the panel is created and import new UI DSL com.intellij.ui.dsl.builder suggested by IntelliJ IDEA

  4. Update non-compilable code, using the following table

    Version 1

    Version 2

    row { row {

    indent { row {

    row { cell(isFullWidth = true)

    row {

    fullRow {

    row {

    titledRow(…) {

    group(…) {



    cell used as sub-grid

    row { panel { … } }

    component(…) or its invocation via ()




    checkBox(text, bindOptions)


    radioButton(text, bindOptions)


    comboBox(…, bindOptions)


    textField(bindOptions, columns)


    scrollableTextArea(bindOptions, rows, columns)


    intTextField(bindOptions, columns, range, step)

    intTextField(range, step).bindIntText(bindOptions).columns(columns)

    textFieldWithBrowseButton(bindOptions, …)






    label(…, bold = true)



    For previous left cell use


    Please do not use custom gaps if possible


    Not needed, this gap is set by default









    Last modified: 16 May 2024