How to create a custom UI?
Designing an UI or Add-on is basically a combination of supplying Properties and the inheritance of built-in Type classes (Panel, Operator, Menu etc.).

Properties
Start by defining your Properties first. Properties are basically 'data types' and can be displayed in the UI for basic user interaction. Note that you can access the value of each property from almost everywhere. To populate a complete list in the console use python's dir() method on bpy.props :

Click to enlarge
Property Appearance

Define a BoolProperty for a 'Checkbox'
Define a FloatProperty or IntProperty to get a 'Slider'
Define a StringProperty for all different kinds of 'Character Input' or 'File Paths'
Define an EnumProperty to get a 'Dropdown Menu'
Define a FloatVectorProperty or IntVectorProperty for compound values eg. 'Color Pickers', 'Location Coordinates', 'Velocity Vectors', 'Matrices' etc. 
...

Property Definition
from bpy.props import (StringProperty,
                       BoolProperty,
                       IntProperty,
                       FloatProperty,
                       EnumProperty,
                       )

my_bool : BoolProperty(
    name=&quot;Enable or Disable&quot;,
    description=&quot;A bool property&quot;,
    default = False
    )

my_int : IntProperty(
    name = &quot;Set a value&quot;,
    description=&quot;A integer property&quot;,
    default = 23,
    min = 10,
    max = 100
    )
...

Note that as of Blender 2.8x, properties should be assigned to variables using a single colon : instead of the usual assignment operator = like in Blender 2.7x or older versions of Blender.

Types
The contents of bpy.types are class templates built for inheritance.
Panel
Panels are everywhere in Blender so it's the most basic element of the user interface. Where the panel is going to be used is defined by bl_space_type. Blenders interface is 'context sensitive' so you can define bl_context to get the Panel in one respective Mode (Object Mode, Edit Mode etc.).

class HelloWorldPanel(bpy.types.Panel):
    bl_idname = &quot;OBJECT_PT_hello_world&quot;
    bl_label = &quot;Hello World&quot;
    bl_space_type = 'PROPERTIES'
    bl_region_type = 'WINDOW'
    bl_context = &quot;object&quot;

    def draw(self, context):
        self.layout.label(text=&quot;Hello World&quot;)

bpy.utils.register_class(HelloWorldPanel)

Sub Panels

As of Blender 2.8x we can have sub panels by assigning a certain panel (parent) to bl_parent_id:
import bpy

class HelloWorldPanel:
    bl_space_type = &quot;VIEW_3D&quot;
    bl_region_type = &quot;UI&quot;
    bl_category = &quot;Tools&quot;
    bl_options = {&quot;DEFAULT_CLOSED&quot;}


class HELLO_PT_World1(HelloWorldPanel, bpy.types.Panel):
    bl_idname = &quot;HELLO_PT_World1&quot;
    bl_label = &quot;Panel 1&quot;

    def draw(self, context):
        layout = self.layout
        layout.label(text=&quot;This is the main panel.&quot;)


class HELLO_PT_World2(HelloWorldPanel, bpy.types.Panel):
    bl_parent_id = &quot;HELLO_PT_World1&quot;
    bl_label = &quot;Panel 2&quot;

    def draw(self, context):
        layout = self.layout
        layout.label(text=&quot;First Sub Panel of Panel 1.&quot;)


class HELLO_PT_World3(HelloWorldPanel, bpy.types.Panel):
    bl_parent_id = &quot;HELLO_PT_World1&quot;
    bl_label = &quot;Panel 3&quot;

    def draw(self, context):
        layout = self.layout
        layout.label(text=&quot;Second Sub Panel of Panel 1.&quot;)


classes = (
    HELLO_PT_World1,
    HELLO_PT_World2, 
    HELLO_PT_World3
)

def register():
    for cls in classes:
        bpy.utils.register_class(cls)

def unregister():
    for cls in classes:
        bpy.utils.unregister_class(cls)


if __name__ == &quot;__main__&quot;:
    register()

See also: Text Editor &gt; Templates &gt; Python &gt; UI Panel.
Operator
The Operator is the most important bit to understand. You can display 'operators' as Buttons and once registered and you can call it from everywhere via bpy.ops.IDNAME(). That's also the way blender is designed, all real Buttons are 'operators' under the hood, mostly written in C but then exposed to python. See also: Text Editor &gt; Templates &gt; Python &gt; Operator....

class HelloWorldMinimal(bpy.types.Operator):
    bl_idname = &quot;wm.hello_world&quot;
    bl_label = &quot;Minimal Operator&quot;

    def execute(self, context):
        # Report &quot;Hello World&quot; to the Info Area
        self.report({'INFO'}, &quot;Hello World&quot;)

        return {'FINISHED'}

bpy.utils.register_class(HelloWorldMinimal)

# test call the operator
bpy.ops.wm.hello_world()

The Operator class template comes with predefined methods, in fact poll, invoke, execute, draw, modal, and cancel which can be used along custom properties for all different kinds of operations and also to provide user interaction. More complete example of an operator:
class HelloWorld(bpy.types.Operator):
    bl_idname = &quot;wm.hello_world&quot;
    bl_label = &quot;Minimal Operator&quot;
    bl_options = {'REGISTER'}
    
    # Operator user properties, should be assigned using a single colon :
    # instead of using an equal sign = in Blender 2.8
    report_flag: bpy.props.BoolProperty(
        name = &quot;Report&quot;,
        default = True)

    @classmethod # Will never run when poll returns false
    def poll(cls, context):
        return context.object
    
    def invoke(self, context, event): # Used for user interaction
        wm = context.window_manager
        return wm.invoke_props_dialog(self)
    
    def draw(self, context): # Draw options (typically displayed in the tool-bar)
        row = self.layout
        row.prop(self, &quot;report_flag&quot;, text=&quot;Report Hello World&quot;)
    
    def execute(self, context): # Runs by default 
        if self.report_flag:
            self.report({'INFO'}, &quot;Hello World&quot;)
        else:
            print (&quot;Hello World&quot;)
        return {'FINISHED'}
        
bpy.utils.register_class(HelloWorld)

# For interaction, pass 'INVOKE_DEFAULT' when calling 
# the operator, this way invoke runs before execute method 
bpy.ops.wm.hello_world('INVOKE_DEFAULT')

Further reading: How to call a confirmation dialog box? (for tremendously dangerous operators).
Menu
For a custom menu define/inherit a Menu Class. Add your operators and properties to the draw() function properly (see also: Text Editor &gt; Templates &gt; Python &gt; UI Menu templates).

class SimpleCustomMenu(bpy.types.Menu):
    bl_label = &quot;Simple Custom Menu&quot;
    bl_idname = &quot;OBJECT_MT_simple_custom_menu&quot;

    def draw(self, context):
        layout = self.layout

        layout.operator(&quot;wm.open_mainfile&quot;)
        layout.operator(&quot;wm.save_as_mainfile&quot;) 

# The menu can also be called from scripts
bpy.ops.wm.call_menu(name=SimpleCustomMenu.bl_idname)

You can also draw a button to call the menu without declaring any extra operator by layout.operator(&quot;wm.call_menu&quot;).name=&quot;bl_idname&quot;
Submenu
To get a submenu, call the second one within the parent menu via layout.menu(bl_idname).

class MyCustomMenu(bpy.types.Menu):
    bl_label = &quot;First Menu&quot;
    bl_idname = &quot;OBJECT_MT_custom_menu&quot;

    def draw(self, context):
        layout = self.layout
        layout.label(text=&quot;Hello First Menu!&quot;, icon='WORLD_DATA')
        
         # call the second custom menu
        layout.menu(&quot;OBJECT_MT_sub_menu&quot;, icon=&quot;COLLAPSEMENU&quot;)
        
class MyCustomSubMenu(bpy.types.Menu):
    bl_label = &quot;Sub Menu&quot;
    bl_idname = &quot;OBJECT_MT_sub_menu&quot;

    def draw(self, context):
        layout = self.layout
        layout.label(text=&quot;Hello Second Menu!&quot;, icon='WORLD_DATA')
        
        # call another predefined menu
        layout.operator(&quot;wm.call_menu&quot;, text=&quot;Unwrap&quot;).name = &quot;VIEW3D_MT_uv_map&quot; 


# draw a button within the panel to call the first menu
class OBJECT_PT_my_panel(bpy.types.Panel):
    ...
    def draw(self, context):
        layout.operator(&quot;wm.call_menu&quot;, text=&quot;Call My Menu&quot;).name = &quot;OBJECT_MT_custom_menu&quot;
        ...


All in One
When creating an Add-on, usually a lot of properties are requiered. For a better organisation you can create a 'settings class' by using a PropertyGroup.Note that as of Blender 2.8x, properties should be assigned using a single colon : instead of the usual assignment operator =.
2.7x
class MySettings(PropertyGroup):

    my_bool = BoolProperty()
    my_int = IntProperty()
    my_float = FloatProperty()
    ...

2.8x
class MySettings(PropertyGroup):

    my_bool: BoolProperty()
    my_int: IntProperty()
    my_float: FloatProperty()
    ...

Also, as of Blender 2.8x module/class registration has changed to prevent name conflicts. bpy.utils.register_module(__name__) isn't available anymore so you basically have to register/unregister each class separately or within a loop (best practice):
classes = (
    WM_OT_HelloWorld,
    OBJECT_PT_CustomPanel,
)

def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)

def unregister():
    from bpy.utils import unregister_class
    for cls in reversed(classes):
        unregister_class(cls) 

Note that in addition to the old naming conventions you also have to add a seperator like _OT_, _MT_ or _PT_ to the name of your class based on the inherited class type (Operator, Menu, Panel).

Add-On Template
The following Add-on appends a custom panel to the Tool Shelf of the 3D View and prints the current 'user values' of all custom properties to the console:

Blender 2.7x
    bl_info = {
    &quot;name&quot;: &quot;Add-on Template&quot;,
    &quot;description&quot;: &quot;&quot;,
    &quot;author&quot;: &quot;&quot;,
    &quot;version&quot;: (0, 0, 2),
    &quot;blender&quot;: (2, 70, 0),
    &quot;location&quot;: &quot;3D View &gt; Tools&quot;,
    &quot;warning&quot;: &quot;&quot;, # used for warning icon and text in addons panel
    &quot;wiki_url&quot;: &quot;&quot;,
    &quot;tracker_url&quot;: &quot;&quot;,
    &quot;category&quot;: &quot;Development&quot;
}

import bpy

from bpy.props import (StringProperty,
                       BoolProperty,
                       IntProperty,
                       FloatProperty,
                       EnumProperty,
                       PointerProperty,
                       )
from bpy.types import (Panel,
                       Operator,
                       PropertyGroup,
                       )


# ------------------------------------------------------------------------
#    Scene Properties
# ------------------------------------------------------------------------

class MySettings(PropertyGroup):

    my_bool = BoolProperty(
        name=&quot;Enable or Disable&quot;,
        description=&quot;A bool property&quot;,
        default = False
        )

    my_int = IntProperty(
        name = &quot;Int Value&quot;,
        description=&quot;A integer property&quot;,
        default = 23,
        min = 10,
        max = 100
        )

    my_float = FloatProperty(
        name = &quot;Float Value&quot;,
        description = &quot;A float property&quot;,
        default = 23.7,
        min = 0.01,
        max = 30.0
        )

    my_string = StringProperty(
        name=&quot;User Input&quot;,
        description=&quot;:&quot;,
        default=&quot;&quot;,
        maxlen=1024,
        )
        
    my_enum = EnumProperty(
        name=&quot;Dropdown:&quot;,
        description=&quot;Apply Data to attribute.&quot;,
        items=[ ('OP1', &quot;Option 1&quot;, &quot;&quot;),
                ('OP2', &quot;Option 2&quot;, &quot;&quot;),
                ('OP3', &quot;Option 3&quot;, &quot;&quot;),
               ]
        )
        
# ------------------------------------------------------------------------
#    Operators
# ------------------------------------------------------------------------

class WM_OT_HelloWorld(bpy.types.Operator):
    bl_idname = &quot;wm.hello_world&quot;
    bl_label = &quot;Print Values Operator&quot;

    def execute(self, context):
        scene = context.scene
        mytool = scene.my_tool
        
        # print the values to the console
        print(&quot;Hello World&quot;)
        print(&quot;bool state:&quot;, mytool.my_bool)
        print(&quot;int value:&quot;, mytool.my_int)
        print(&quot;float value:&quot;, mytool.my_float)
        print(&quot;string value:&quot;, mytool.my_string)
        print(&quot;enum state:&quot;, mytool.my_enum)
        
        return {'FINISHED'}

# ------------------------------------------------------------------------
#    Menus
# ------------------------------------------------------------------------

class OBJECT_MT_CustomMenu(bpy.types.Menu):
    bl_idname = &quot;object.custom_menu&quot;
    bl_label = &quot;Select&quot;

    def draw(self, context):
        layout = self.layout
        
        # Built-in example operators
        layout.operator(&quot;object.select_all&quot;, text=&quot;Select/Deselect All&quot;).action = 'TOGGLE'
        layout.operator(&quot;object.select_all&quot;, text=&quot;Inverse&quot;).action = 'INVERT'
        layout.operator(&quot;object.select_random&quot;, text=&quot;Random&quot;)
            
# ------------------------------------------------------------------------
#    Panel in Object Mode
# ------------------------------------------------------------------------

class OBJECT_PT_CustomPanel(Panel):
    bl_idname = &quot;object.custom_panel&quot;
    bl_label = &quot;My Panel&quot;
    bl_space_type = &quot;VIEW_3D&quot;   
    bl_region_type = &quot;TOOLS&quot;    
    bl_category = &quot;Tools&quot;
    bl_context = &quot;objectmode&quot;   

    @classmethod
    def poll(self,context):
        return context.object is not None

    def draw(self, context):
        layout = self.layout
        scene = context.scene
        mytool = scene.my_tool

        layout.prop(mytool, &quot;my_bool&quot;)
        layout.prop(mytool, &quot;my_enum&quot;, text=&quot;&quot;) 
        layout.prop(mytool, &quot;my_int&quot;)
        layout.prop(mytool, &quot;my_float&quot;)
        layout.prop(mytool, &quot;my_string&quot;)
        layout.operator(&quot;wm.hello_world&quot;)
        layout.menu(OBJECT_MT_CustomMenu.bl_idname, text=&quot;Presets&quot;, icon=&quot;SCENE&quot;)
        layout.separator()

# ------------------------------------------------------------------------
#    Registration
# ------------------------------------------------------------------------

def register():
    bpy.utils.register_module(__name__)
    bpy.types.Scene.my_tool = PointerProperty(type=MySettings)

def unregister():
    bpy.utils.unregister_module(__name__)
    del bpy.types.Scene.my_tool

if __name__ == &quot;__main__&quot;:
    register()

Gist: https://gist.github.com/p2or/a00bdde9f2751940717a404cf977dd01
Blender 2.8x
bl_info = {
    &quot;name&quot;: &quot;Add-on Template&quot;,
    &quot;description&quot;: &quot;&quot;,
    &quot;author&quot;: &quot;p2or&quot;,
    &quot;version&quot;: (0, 0, 3),
    &quot;blender&quot;: (2, 80, 0),
    &quot;location&quot;: &quot;3D View &gt; Tools&quot;,
    &quot;warning&quot;: &quot;&quot;, # used for warning icon and text in addons panel
    &quot;wiki_url&quot;: &quot;&quot;,
    &quot;tracker_url&quot;: &quot;&quot;,
    &quot;category&quot;: &quot;Development&quot;
}


import bpy

from bpy.props import (StringProperty,
                       BoolProperty,
                       IntProperty,
                       FloatProperty,
                       FloatVectorProperty,
                       EnumProperty,
                       PointerProperty,
                       )
from bpy.types import (Panel,
                       Menu,
                       Operator,
                       PropertyGroup,
                       )


# ------------------------------------------------------------------------
#    Scene Properties
# ------------------------------------------------------------------------

class MyProperties(PropertyGroup):

    my_bool: BoolProperty(
        name=&quot;Enable or Disable&quot;,
        description=&quot;A bool property&quot;,
        default = False
        )

    my_int: IntProperty(
        name = &quot;Int Value&quot;,
        description=&quot;A integer property&quot;,
        default = 23,
        min = 10,
        max = 100
        )

    my_float: FloatProperty(
        name = &quot;Float Value&quot;,
        description = &quot;A float property&quot;,
        default = 23.7,
        min = 0.01,
        max = 30.0
        )

    my_float_vector: FloatVectorProperty(
        name = &quot;Float Vector Value&quot;,
        description=&quot;Something&quot;,
        default=(0.0, 0.0, 0.0), 
        min= 0.0, # float
        max = 0.1
    ) 

    my_string: StringProperty(
        name=&quot;User Input&quot;,
        description=&quot;:&quot;,
        default=&quot;&quot;,
        maxlen=1024,
        )

    my_path: StringProperty(
        name = &quot;Directory&quot;,
        description=&quot;Choose a directory:&quot;,
        default=&quot;&quot;,
        maxlen=1024,
        subtype='DIR_PATH'
        )
        
    my_enum: EnumProperty(
        name=&quot;Dropdown:&quot;,
        description=&quot;Apply Data to attribute.&quot;,
        items=[ ('OP1', &quot;Option 1&quot;, &quot;&quot;),
                ('OP2', &quot;Option 2&quot;, &quot;&quot;),
                ('OP3', &quot;Option 3&quot;, &quot;&quot;),
               ]
        )

# ------------------------------------------------------------------------
#    Operators
# ------------------------------------------------------------------------

class WM_OT_HelloWorld(Operator):
    bl_label = &quot;Print Values Operator&quot;
    bl_idname = &quot;wm.hello_world&quot;

    def execute(self, context):
        scene = context.scene
        mytool = scene.my_tool

        # print the values to the console
        print(&quot;Hello World&quot;)
        print(&quot;bool state:&quot;, mytool.my_bool)
        print(&quot;int value:&quot;, mytool.my_int)
        print(&quot;float value:&quot;, mytool.my_float)
        print(&quot;string value:&quot;, mytool.my_string)
        print(&quot;enum state:&quot;, mytool.my_enum)

        return {'FINISHED'}

# ------------------------------------------------------------------------
#    Menus
# ------------------------------------------------------------------------

class OBJECT_MT_CustomMenu(bpy.types.Menu):
    bl_label = &quot;Select&quot;
    bl_idname = &quot;OBJECT_MT_custom_menu&quot;

    def draw(self, context):
        layout = self.layout

        # Built-in operators
        layout.operator(&quot;object.select_all&quot;, text=&quot;Select/Deselect All&quot;).action = 'TOGGLE'
        layout.operator(&quot;object.select_all&quot;, text=&quot;Inverse&quot;).action = 'INVERT'
        layout.operator(&quot;object.select_random&quot;, text=&quot;Random&quot;)

# ------------------------------------------------------------------------
#    Panel in Object Mode
# ------------------------------------------------------------------------

class OBJECT_PT_CustomPanel(Panel):
    bl_label = &quot;My Panel&quot;
    bl_idname = &quot;OBJECT_PT_custom_panel&quot;
    bl_space_type = &quot;VIEW_3D&quot;   
    bl_region_type = &quot;UI&quot;
    bl_category = &quot;Tools&quot;
    bl_context = &quot;objectmode&quot;   


    @classmethod
    def poll(self,context):
        return context.object is not None

    def draw(self, context):
        layout = self.layout
        scene = context.scene
        mytool = scene.my_tool

        layout.prop(mytool, &quot;my_bool&quot;)
        layout.prop(mytool, &quot;my_enum&quot;, text=&quot;&quot;) 
        layout.prop(mytool, &quot;my_int&quot;)
        layout.prop(mytool, &quot;my_float&quot;)
        layout.prop(mytool, &quot;my_float_vector&quot;, text=&quot;&quot;)
        layout.prop(mytool, &quot;my_string&quot;)
        layout.prop(mytool, &quot;my_path&quot;)
        layout.operator(&quot;wm.hello_world&quot;)
        layout.menu(OBJECT_MT_CustomMenu.bl_idname, text=&quot;Presets&quot;, icon=&quot;SCENE&quot;)
        layout.separator()

# ------------------------------------------------------------------------
#    Registration
# ------------------------------------------------------------------------

classes = (
    MyProperties,
    WM_OT_HelloWorld,
    OBJECT_MT_CustomMenu,
    OBJECT_PT_CustomPanel
)

def register():
    from bpy.utils import register_class
    for cls in classes:
        register_class(cls)

    bpy.types.Scene.my_tool = PointerProperty(type=MyProperties)

def unregister():
    from bpy.utils import unregister_class
    for cls in reversed(classes):
        unregister_class(cls)
    del bpy.types.Scene.my_tool


if __name__ == &quot;__main__&quot;:
    register()

Gist: https://gist.github.com/p2or/2947b1aa89141caae182526a8fc2bc5a

More complex examples

What ui would work for choosing from a long, long list?
script custom previews in a menu
Create an interface which is similar to the material list box