Skip to content

Styling

Every UI element inherits a set of visual properties from the Element base class. This page covers the styling options available across all element types.

Colour

The Colour class provides predefined colour constants and methods for custom colours.

Predefined Colours

ConstantValue
Colour.blue"blue"
Colour.yellow"yellow"
Colour.red"red"
Colour.green"green"
Colour.magenta"magenta"
Colour.limegreen"limegreen"
Colour.tomato"tomato"
Colour.orange"orange"
Colour.purple"purple"
Colour.grey"grey"

Custom Colours

Use from_hex() for hex colour codes or from_string() for any HTML colour name.

from pydoover.ui import Colour

# Hex colour
custom = Colour.from_hex("#FF5733")

# HTML colour name
brown = Colour.from_string("brown")

The Doover portal accepts any valid HTML colour name or hex string, so these methods are primarily for documentation clarity in your code.

Using Colours

Colours can be applied to elements, ranges, thresholds, series, and confirm dialogs.

from pydoover.ui import NumericVariable, Range, Threshold, Colour

temperature = NumericVariable(
    "Temperature",
    value=22.5,
    colour=Colour.red,
    ranges=[
        Range(label="Normal", min_val=15, max_val=30, colour=Colour.green),
        Range(label="Warning", min_val=30, max_val=40, colour=Colour.yellow),
        Range(label="Critical", min_val=40, max_val=60, colour=Colour.red),
    ],
    thresholds=[
        Threshold(label="Max Safe", value=40, colour=Colour.tomato),
    ],
)

Icons

The icon parameter accepts an icon identifier string. Icons appear alongside the element's display name in the portal.

from pydoover.ui import NumericVariable, Button, Submodule

temp = NumericVariable("Temperature", value=22.5, icon="thermometer")
restart = Button("Restart", icon="refresh")
settings = Submodule("Settings", children=[], icon="cog")

Icons are also available on UI subclass definitions:

from pydoover.ui import UI, Colour

class MyUI(UI, icon="gauge", colour=Colour.blue):
    pass

Widget

The Widget class controls how NumericVariable values are visualised beyond the standard number display. Gauge widgets give operators at-a-glance visual feedback for readings like engine RPM, tank levels, or pressures — pairing a gauge with Range colour bands lets operators instantly see whether a value is in a safe operating zone.

WidgetValueDescription
Widget.linear"linearGauge"Horizontal bar gauge
Widget.radial"radialGauge"Circular dial gauge
from pydoover.ui import NumericVariable, Widget, Range, Colour

cpu_load = NumericVariable(
    "CPU Load",
    value=45,
    units="%",
    form=Widget.radial,
    ranges=[
        Range(min_val=0, max_val=70, colour=Colour.green),
        Range(min_val=70, max_val=90, colour=Colour.yellow),
        Range(min_val=90, max_val=100, colour=Colour.red),
    ],
)
Information Circle
Example — Radial and Linear Gauges

An engine controller uses Widget.radial for the main RPM gauge and Widget.linear for secondary readings like engine load. Range colour bands give operators at-a-glance status — blue for idle, green for normal operation, yellow for approaching limits, and red for critical:

from pydoover import ui

rpm_gauge = ui.NumericVariable(
    "Engine RPM",
    value=MyTags.engine_rpm,
    precision=0,
    form=ui.Widget.radial,
    ranges=[
        ui.Range(label="Idle", min_val=0, max_val=800, colour=ui.Colour.blue),
        ui.Range(label="Run", min_val=800, max_val=2200, colour=ui.Colour.green),
        ui.Range(label="High", min_val=2200, max_val=4000, colour=ui.Colour.yellow),
    ],
)

engine_load = ui.NumericVariable(
    "Engine Load",
    value=MyTags.engine_load_pct,
    units="%",
    precision=0,
    form=ui.Widget.linear,
    ranges=[
        ui.Range(label="Idle", min_val=0, max_val=15, colour=ui.Colour.blue),
        ui.Range(label="Working", min_val=15, max_val=85, colour=ui.Colour.green),
        ui.Range(label="High", min_val=85, max_val=100, colour=ui.Colour.yellow),
        ui.Range(label="Overload", min_val=100, max_val=125, colour=ui.Colour.red),
    ],
)

Use Widget.radial for primary readings operators check first (RPM, speed), and Widget.linear for supporting metrics (load, temperature, pressure). Ranges should cover the full expected value span with no gaps.

Conditional Display

hidden

The hidden parameter controls whether an element is visible. It accepts a boolean, a tag reference, a Tag descriptor, or a config variable reference string for dynamic visibility.

from pydoover.ui import NumericVariable, Submodule, UI, tag_ref
from pydoover.tags import Tag, Tags

class MyTags(Tags):
    show_advanced = Tag("boolean", default=False)

# Always hidden
debug_info = NumericVariable("Debug", value=0, hidden=True)

# Dynamically hidden based on a tag reference
advanced_panel = Submodule(
    "Advanced",
    children=[],
    hidden=tag_ref("hide_advanced", tag_type="boolean", default_value=True),
)

# Dynamically hidden using a Tag descriptor directly
advanced_panel = Submodule(
    "Advanced",
    children=[],
    hidden=MyTags.show_advanced,
)

# Dynamically hidden using a config variable reference
class OptionalUI(UI, hidden="$config.app().hide_ui"):
    pass

The config variable reference form ("$config.app().field_name") reads from a config.Boolean element in the application's configuration schema, allowing operators to toggle UI visibility through the deployment config rather than a runtime tag.

is_available

Controls whether the element appears as available (enabled) or unavailable (greyed out) in the portal.

from pydoover.ui import Button

# Conditionally available
manual_btn = Button("Manual Override", is_available=False)

conditions

A dictionary of conditions that must be met for the element to be displayed. The portal evaluates these conditions against the current state.

from pydoover.ui import FloatInput

# Only show when mode is "manual"
manual_setpoint = FloatInput(
    "Manual Setpoint",
    conditions={"mode": "manual"},
)

Position

The position parameter controls the ordering of elements within their parent container. Lower values appear first. If not set, elements are automatically assigned incrementing positions in the order they are defined.

from pydoover.ui import NumericVariable

# Explicit positioning
first = NumericVariable("First", value=0, position=1)
second = NumericVariable("Second", value=0, position=2)
third = NumericVariable("Third", value=0, position=3)

For the UI subclass itself, position controls ordering among multiple applications on a device page:

class PrimaryUI(UI, position=1):
    pass

class SecondaryUI(UI, position=50):
    pass

RangeView

RangeView controls how ranges and thresholds are displayed on plots. It provides three modes:

ModeValueDescription
RangeView.line"line"Show thresholds as horizontal lines
RangeView.zone"zone"Shade the area covered by each range
RangeView.off"off"Hide ranges and thresholds
from pydoover.ui import NumericVariable, RangeView, Range, Threshold, Colour

# Use zones for ranges
tank_level = NumericVariable(
    "Tank Level",
    value=75,
    units="%",
    ranges=[
        Range(label="Low", min_val=0, max_val=20, colour=Colour.red),
        Range(label="Normal", min_val=20, max_val=80, colour=Colour.green),
        Range(label="High", min_val=80, max_val=100, colour=Colour.yellow),
    ],
    default_range_view=RangeView.zone,
)

# Use lines for thresholds
pressure = NumericVariable(
    "Pressure",
    value=50,
    thresholds=[
        Threshold(label="Max", value=100, colour=Colour.red),
    ],
    default_range_view=RangeView.line,
)

When default_range_view is not set, the portal defaults to "line" when thresholds are defined, "zone" when ranges are defined, or "off" when neither is present.

show_on_graph

Each Range has a show_on_graph parameter (default True) that controls whether the range band appears on the element's data graph. Set it to False to use ranges only for gauge colouring without cluttering the graph view.

from pydoover.ui import NumericVariable, Range, Colour

rssi = NumericVariable(
    "RSSI",
    value=-75,
    units="dBm",
    ranges=[
        Range("Weak", -130, -100, Colour.red, show_on_graph=True),
        Range("Fair", -100, -80, Colour.yellow, show_on_graph=True),
        Range("Good", -80, -50, Colour.green, show_on_graph=True),
    ],
)

ConfirmDialog

ConfirmDialog customises the confirmation dialog shown before an interaction executes. Pass it to requires_confirm on any interactive element.

from pydoover.ui import Button, ConfirmDialog, Colour

emergency_stop = Button(
    "Emergency Stop",
    requires_confirm=ConfirmDialog(
        title="Emergency Stop",
        subtitle="All processes will be halted immediately",
        warning_reason="This action cannot be undone remotely",
        colour=Colour.red,
        help_text="Press confirm to stop all running equipment",
        icon="alert-octagon",
    ),
)
ParameterDescription
titleDialog title (defaults to "Confirm {display_name} change")
subtitleText shown below the title
warning_reasonWarning message explaining why confirmation is needed
colourDialog accent colour
help_textAdditional explanatory text
iconIcon shown in the dialog header

For a simple default confirmation dialog, just pass True:

delete_btn = Button("Delete Data", requires_confirm=True)
Information Circle
Example — Safety-Critical Controls

An engine controller uses ConfirmDialog on safety-critical controls to warn operators about the consequences of remote commands. The warning_reason highlights the risk, and help_text explains what will happen:

from pydoover import ui

stop_btn = ui.Button(
    "Stop Pump",
    requires_confirm=ui.ConfirmDialog(
        title="Stop the pump?",
        warning_reason=(
            "This will shut the engine down. Anyone relying on "
            "flow downstream will lose it."
        ),
        help_text=(
            "Sends a remote STOP command to the panel. "
            "The engine will enter Cooldown for ~30 seconds "
            "and then stop."
        ),
    ),
)

rpm_slider = ui.Slider(
    "Target RPM",
    min_val=600,
    max_val=2400,
    step_size=10,
    dual_slider=False,
    inverted=False,
    requires_confirm=ui.ConfirmDialog(
        title="Change target RPM?",
        help_text=(
            "Sets the engine speed setpoint while the pump is "
            "running. The value is clamped to the configured "
            "Target RPM Min / Max."
        ),
    ),
)

ConfirmDialog works on any interactive element — Button, Switch, Slider, Select, etc. Use warning_reason for destructive or irreversible actions and help_text for additional context.

ApplicationVariant

ApplicationVariant controls how applications are displayed on the device page. This is set on the Application container or through the UI framework's internal wiring.

VariantValueDescription
ApplicationVariant.submodule"submodule"Embeds the app inside a collapsible submodule (default)
ApplicationVariant.stacked"stacked"Places elements directly on the page without submodule wrapping

Next Steps