Create Your Own Components
Instead of using sprite-based components, you might want to create components that are based on shapes provided by the UI Module. Some advantages to this approach are better control of the colors, as well as ending up with components that are easily resizable.
You can use the basic building blocks provided in the Kontakt Controls Package to create your own components. Below is an example of how you could go about creating a Knob. You can similarly implement a Slider, Switch, and ToggleButton based on the same approach.
Here's how to use the same building blocks that make up the Knob offered in the package to make your own Knob using the Arc component from the UI Module.
To start off, let's make an instrument with the Knob available out-of-the-box, for comparison purposes.
import * from ui
import * from kontakt_controls
component Main {
ZStack {
// Background
Rectangle(color: Color(0xFF2A2A2A))
// Controls
HStack(spacing: 50) {
Knob(
control_id: "my_knob",
label: "My Knob",
)
}
}
}
export var main: Component = Main()
on init
declare ui_knob $my_knob (0, 1000, 1)
make_persistent($my_knob)
read_persistent_var($my_knob)
expose_controls
end on
And this is the starting state of the instrument:

To start building the custom Knob component you can reuse the same base that the current Knob uses. Namely the Draggable
modifier, which already implements the drag gesture behavior needed for a knob. So what's left to implement is the connection to the KSP Control and the appearance of the knob.
Knob Appearance
Create a new file which contains the custom knob component, next to the main.kscript
file. Let's call it my_knob.kscript
, and add to it code that implements the knob appearance, with some hard-coded values. Note that the component is exported, so that you can use it in your instrument:
import * from ui
export component MyKnob {
Arc(
color: Color(0xFFC780FF),
angle: Angle(degrees: 180),
start_angle: Angle(degrees:90),
) with {
Frame (width: 60, height: 60)
Overlay {
Text("180", color: Color(0xFFC780FF))
}
}
}
To test it out, you can add it to the instrument along the existing knob:
import { MyKnob } from my_knob
component Main {
ZStack {
// Background
Rectangle(color: Color(0xFF2A2A2A))
// Controls
HStack(spacing: 50) {
MyKnob()
Knob(
control_id: "my_knob",
label: "My Knob",
)
}
}
}

KSP Control Connection
The next step is connect the Knob to the KSP Control, so that the data can come from KSP instead of being hard-coded.
The id of the control will be passed as a property to the component, and a ksp_control
computed state provides access to all properties of the KSPKnob. You can use these properties to set values for the angle of the knob, and for the displayed text.
import * from ui
import { KSPKnob } from kontakt
export component MyKnob {
@property control_id: String
ksp_control: KSPKnob { return KSPKnob(id: self.control_id) }
Arc(
color: Color(0xFFC780FF),
angle: Angle(degrees: self.ksp_control.normalized_value*360),
start_angle: Angle(degrees: 90),
) with {
Frame (width: 60, height: 60)
Overlay {
Text("\{self.ksp_control.value}", color: Color(0xFFC780FF))
}
}
}
Now pass the same control id used by the Knob from the Kontakt Controls Package to the new component:
component Main {
ZStack {
// Background
Rectangle(color: Color(0xFF2A2A2A))
// Controls
HStack(spacing: 50) {
MyKnob(
control_id: "my_knob"
)
Knob(
control_id: "my_knob",
label: "My Knob",
)
}
}
}
Interactive Behavior
All there is left to do is to add the interactive behavior of the knob. This comes from applying to the Knob the existing Draggable
modifier from the package. Draggable
is suitable for both knobs and sliders implementations, and contains the handling of the drag gesture that controls the value updates of the component, as well as the reset trigger implementation(control/command + tap).
For the complete functioning of the modifier, you also need to implement two callbacks:
on_editing_changed
called when the user starts or ends interacting with the control,- and
on_reset
, called when the user triggers a reset (control/command + tap).
import * from ui
import { KSPKnob } from kontakt
import { EdgeInsets, Draggable, DraggableAxis } from kontakt_controls
export component MyKnob {
@property control_id: String
@property disabled: Bool = false
ksp_control: KSPKnob { return KSPKnob(id: self.control_id) }
noop: Float {
get { return 0 }
set(new_value) {}
}
axis: DraggableAxis {
return DraggableAxis(
step_count: self.ksp_control.max - self.ksp_control.min,
fine_step_count: 2,
sensitivity: 0.5,
)
}
Arc(
color: Color(0xFFC780FF),
angle: Angle(degrees: self.ksp_control.normalized_value*360),
start_angle: Angle(degrees: 90),
) with {
Frame (width: 60, height: 60)
Overlay {
Text("\{self.ksp_control.value}", color: Color(0xFFC780FF))
}
Draggable(
x_value: self.$noop,
y_value: self.ksp_control.$normalized_value,
y_axis: self.axis,
disabled: self.disabled,
on_editing_changed: fun (editing: Bool) {
self.ksp_control.automate = editing
},
on_reset: fun () {
self.ksp_control.value = self.ksp_control.default_value
},
inset: EdgeInsets(0)
)
}
}
Notice that most of the property values you set for Draggable
come from the KSP Control. In addition, the code above:
- passes a binding to a 0 value for the horizontal axis, since the knob only updates when dragging on the vertical axis, not the horizontal one
- sets a vertical axis for interacting with the knob
- passes a
disabled
property from above - and sets an inset value
You can further refine and customize this knob based on your design needs, but with a minimal implementation you already got a rough equivalent of the knob from the package, using vector instead or raster graphics. The two knobs in the instrument are now in sync, both connected to the same KSP control.
