diff --git a/basicctrls.go b/basicctrls.go
index 8e4e926..82caada 100644
--- a/basicctrls.go
+++ b/basicctrls.go
@@ -146,7 +146,6 @@ func NewTextbox() Textbox {
 // - TODO set increment? (work on windows)
 // - TODO set page step?
 // - TODO wrapping
-// - TODO set/get integer value
 // - TODO negative values
 // - TODO ensuring values entered in text box stay within bounds
 type Spinbox interface {
@@ -156,6 +155,10 @@ type Spinbox interface {
 	// For SetValue, if the new value is outside the current range of the Spinbox, it is set to the nearest extremity.
 	Value() int
 	SetValue(value int)
+
+	// OnChanged sets the event handler for when the Spinbox's value is changed.
+	// Under what conditions this event is raised when the user types into the Spinbox's edit field is platform-defined.
+	OnChanged(func())
 }
 
 // NewSpinbox creates a new Spinbox with the given minimum and maximum.
diff --git a/spinbox_darwin.go b/spinbox_darwin.go
index 0d0d858..65274bd 100644
--- a/spinbox_darwin.go
+++ b/spinbox_darwin.go
@@ -21,12 +21,14 @@ import "C"
 // - proper spacing between edit and spinner: Interface Builder isn't clear; NSDatePicker doesn't spill the beans
 
 type spinbox struct {
-	id	C.id
+	id			C.id
+	changed		*event
 }
 
 func newSpinbox(min int, max int) Spinbox {
 	s := new(spinbox)
 	s.id = C.newSpinbox(unsafe.Pointer(s), C.intmax_t(min), C.intmax_t(max))
+	s.changed = newEvent()
 	return s
 }
 
@@ -38,6 +40,16 @@ func (s *spinbox) SetValue(value int) {
 	C.spinboxSetValue(s.id, C.intmax_t(value))
 }
 
+func (s *spinbox) OnChanged(e func()) {
+	s.changed.set(e)
+}
+
+//export spinboxChanged
+func spinboxChanged(data unsafe.Pointer) {
+	s := (*spinbox)(data)
+	s.changed.fire()
+}
+
 func (s *spinbox) textfield() C.id {
 	return C.spinboxTextField(s.id)
 }
diff --git a/spinbox_darwin.m b/spinbox_darwin.m
index 9693669..fddfbfd 100644
--- a/spinbox_darwin.m
+++ b/spinbox_darwin.m
@@ -84,11 +84,13 @@
 - (IBAction)stepperClicked:(id)sender
 {
 	[self setValue:[self->stepper integerValue]];
+	spinboxChanged(self->gospinbox);
 }
 
 - (void)controlTextDidChange:(NSNotification *)note
 {
 	[self setValue:[self->textfield integerValue]];
+	spinboxChanged(self->gospinbox);
 }
 
 @end
diff --git a/spinbox_unix.go b/spinbox_unix.go
index b6b2ec3..ca84b28 100644
--- a/spinbox_unix.go
+++ b/spinbox_unix.go
@@ -9,6 +9,7 @@ import (
 )
 
 // #include "gtk_unix.h"
+// extern void spinboxChanged(GtkSpinButton *, gpointer);
 import "C"
 
 // TODO preferred width may be too wide
@@ -16,6 +17,7 @@ import "C"
 type spinbox struct {
 	*controlSingleWidget
 	spinbutton	*C.GtkSpinButton
+	changed		*event
 }
 
 func newSpinbox(min int, max int) Spinbox {
@@ -24,9 +26,18 @@ func newSpinbox(min int, max int) Spinbox {
 	s := &spinbox{
 		controlSingleWidget:	newControlSingleWidget(widget),
 		spinbutton:			(*C.GtkSpinButton)(unsafe.Pointer(widget)),
+		changed:				newEvent(),
 	}
 	C.gtk_spin_button_set_digits(s.spinbutton, 0)				// integers
 	C.gtk_spin_button_set_numeric(s.spinbutton, C.TRUE)		// digits only
+	// this isn't specifically documented as the signal to connect to until 3.14
+	// it has existed as far back as 3.4, though, if not earlier
+	// there's also ::change-value which is for keyboard changing
+	g_signal_connect(
+		C.gpointer(unsafe.Pointer(s.spinbutton)),
+		"value-changed",
+		C.GCallback(C.spinboxChanged),
+		C.gpointer(unsafe.Pointer(s)))
 	return s
 }
 
@@ -46,3 +57,13 @@ func (s *spinbox) SetValue(value int) {
 	}
 	C.gtk_spin_button_set_value(s.spinbutton, C.gdouble(value))
 }
+
+func (s *spinbox) OnChanged(e func()) {
+	s.changed.set(e)
+}
+
+//export spinboxChanged
+func spinboxChanged(swid *C.GtkSpinButton, data C.gpointer) {
+	s := (*spinbox)(unsafe.Pointer(data))
+	s.changed.fire()
+}
diff --git a/zz_test.go b/zz_test.go
index afe3c04..bf11488 100644
--- a/zz_test.go
+++ b/zz_test.go
@@ -150,15 +150,11 @@ func (tw *testwin) addfe() {
 	tw.festack.SetStretchy(4)
 	tw.festack.SetStretchy(6)
 	sb := NewSpinbox(0, 100)
-	cbutton := NewButton("Set to Invalid Low")
-	cbutton.OnClicked(func() {
-		sb.SetValue(-500)
+	sl := NewLabel("")
+	sb.OnChanged(func() {
+		sl.SetText(fmt.Sprintf("%d", sb.Value()))
 	})
-	dbutton := NewButton("Set to Invalid High")
-	dbutton.OnClicked(func() {
-		sb.SetValue(500)
-	})
-	tw.festack2 = newVerticalStack(sb, cbutton, dbutton, Space(), NewTextbox())
+	tw.festack2 = newVerticalStack(sb, sl, Space(), Space(), NewTextbox())
 	tw.festack2.SetStretchy(3)
 	tw.festack2.SetStretchy(4)
 	tw.festack = newHorizontalStack(tw.festack, tw.festack2)