diff --git a/generator/fixtures/component_name.py b/generator/fixtures/component_name.py
index 97f3f6c2a..6f7ec299c 100644
--- a/generator/fixtures/component_name.py
+++ b/generator/fixtures/component_name.py
@@ -10,7 +10,7 @@
   insert_composite_component,
   register_event_mapper,
 )
-from mesop.events import MesopEvent, ClickEvent
+from mesop.events import MesopEvent, ClickEvent, InputEvent
 
 # INSERT_EVENTS
 
diff --git a/generator/generate_ng_ts.py b/generator/generate_ng_ts.py
index b32bc5d6b..9acc0785a 100644
--- a/generator/generate_ng_ts.py
+++ b/generator/generate_ng_ts.py
@@ -84,6 +84,17 @@ def generate_ts_event_method(prop: pb.OutputProp) -> str:
 
 def generate_ts_native_event_method(native_event: str) -> str:
   native_event_name = upper_camel_case(native_event)
+  if native_event == "input":
+    return f"""
+  on{native_event_name}(event: Event): void {{
+    const userEvent = new UserEvent();
+    userEvent.setHandlerId(this.config().getOn{native_event_name}HandlerId())
+    userEvent.setString((event.target as HTMLInputElement).value);
+    userEvent.setKey(this.key);
+    this.channel.dispatch(userEvent);
+  }}
+  """
+
   return f"""
   on{native_event_name}(event: Event): void {{
     const userEvent = new UserEvent();
diff --git a/generator/output_data/input.binarypb b/generator/output_data/input.binarypb
index 79b458744..75ef4abcf 100644
--- a/generator/output_data/input.binarypb
+++ b/generator/output_data/input.binarypb
@@ -1,6 +1,6 @@
 
-–
-input"MatInput*input:matInputRerrorStateMatcher2)
+
+input"MatInput*input:matInputJinputRerrorStateMatcher2)
 @angular/material/inputMatInputModule22
 @angular/material/form-fieldMatFormFieldModuleX
 disabledboolean
diff --git a/generator/output_data/input.json b/generator/output_data/input.json
index 78861dc55..614f10e43 100644
--- a/generator/output_data/input.json
+++ b/generator/output_data/input.json
@@ -6,7 +6,7 @@
     "elementName": "input",
     "tsFilename": "",
     "directiveNamesList": ["matInput"],
-    "nativeEventsList": [],
+    "nativeEventsList": ["input"],
     "skipPropertyNamesList": ["errorStateMatcher"],
     "ngModulesList": [
       {
diff --git a/generator/spec_generator.ts b/generator/spec_generator.ts
index 22ca8cced..a3c1c42e1 100644
--- a/generator/spec_generator.ts
+++ b/generator/spec_generator.ts
@@ -35,6 +35,7 @@ const inputSpecInput = (() => {
   i.setName('input');
   i.setElementName('input');
   i.addDirectiveNames('matInput');
+  i.addNativeEvents('input');
   i.setIsFormField(true);
   i.addSkipPropertyNames('errorStateMatcher');
   return i;
diff --git a/mesop/__init__.py b/mesop/__init__.py
index 1a4060e45..37cf913e6 100644
--- a/mesop/__init__.py
+++ b/mesop/__init__.py
@@ -24,6 +24,9 @@
 from mesop.events import (
   ClickEvent as ClickEvent,
 )
+from mesop.events import (
+  InputEvent as InputEvent,
+)
 from mesop.features import page as page
 from mesop.key import Key as Key
 
diff --git a/mesop/components/input/e2e/input_app.py b/mesop/components/input/e2e/input_app.py
index c91ea8c0a..0115da153 100644
--- a/mesop/components/input/e2e/input_app.py
+++ b/mesop/components/input/e2e/input_app.py
@@ -1,7 +1,30 @@
 import mesop as me
 
 
+@me.stateclass
+class State:
+  input: str = ""
+  count: int = 0
+  checked: bool = False
+
+
+@me.on(me.InputEvent)
+def on_input(e: me.InputEvent):
+  state = me.state(State)
+  state.input = e.value
+
+
+@me.on(me.CheckboxChangeEvent)
+def on_change(e: me.CheckboxChangeEvent):
+  state = me.state(State)
+  state.checked = e.checked
+
+
 @me.page(path="/components/input/e2e/input_app")
 def app():
   me.text(text="Hello, world!")
-  me.input(label="Basic input")
+  with me.checkbox(on_change=on_change):
+    me.text(text="check")
+  s = me.state(State)
+  me.input(label="Basic input", on_input=on_input, key=str(s.checked))
+  me.text(text=s.input)
diff --git a/mesop/components/input/input.ng.html b/mesop/components/input/input.ng.html
index 1a599c7d2..2fa466a47 100644
--- a/mesop/components/input/input.ng.html
+++ b/mesop/components/input/input.ng.html
@@ -18,6 +18,7 @@
     [aria-describedby]="config().getUserAriaDescribedBy()"
     [value]="config().getValue()"
     [readonly]="config().getReadonly()"
+    (input)="onInput($event)"
   />
 </mat-form-field>
 }
diff --git a/mesop/components/input/input.proto b/mesop/components/input/input.proto
index 92e4ef7c8..6ed578dd4 100644
--- a/mesop/components/input/input.proto
+++ b/mesop/components/input/input.proto
@@ -20,5 +20,6 @@ message InputType {
   string subscript_sizing = 14;
   string hint_label = 15;
   string label = 16;
-  int32 variant_index = 17;
+  string on_input_handler_id = 17;
+  int32 variant_index = 18;
 }
diff --git a/mesop/components/input/input.py b/mesop/components/input/input.py
index f36e1813b..00696cf7e 100644
--- a/mesop/components/input/input.py
+++ b/mesop/components/input/input.py
@@ -1,11 +1,13 @@
-from typing import Literal
+from typing import Any, Callable, Literal
 
 from pydantic import validate_arguments
 
 import mesop.components.input.input_pb2 as input_pb
 from mesop.component_helpers import (
+  handler_type,
   insert_component,
 )
+from mesop.events import InputEvent
 
 
 @validate_arguments
@@ -28,6 +30,7 @@ def input(
   subscript_sizing: Literal["fixed", "dynamic"] = "fixed",
   hint_label: str = "",
   label: str = "",
+  on_input: Callable[[InputEvent], Any] | None = None,
   variant: Literal["matInput"] = "matInput",
 ):
   """
@@ -53,6 +56,7 @@ def input(
       subscript_sizing=subscript_sizing,
       hint_label=hint_label,
       label=label,
+      on_input_handler_id=handler_type(on_input) if on_input else "",
       variant_index=_get_variant_index(variant),
     ),
   )
diff --git a/mesop/components/input/input.ts b/mesop/components/input/input.ts
index c7df861d1..37d4ad1ca 100644
--- a/mesop/components/input/input.ts
+++ b/mesop/components/input/input.ts
@@ -46,4 +46,12 @@ export class InputComponent {
   getSubscriptSizing(): 'fixed' | 'dynamic' {
     return this.config().getSubscriptSizing() as 'fixed' | 'dynamic';
   }
+
+  onInput(event: Event): void {
+    const userEvent = new UserEvent();
+    userEvent.setHandlerId(this.config().getOnInputHandlerId());
+    userEvent.setString((event.target as HTMLInputElement).value);
+    userEvent.setKey(this.key);
+    this.channel.dispatch(userEvent);
+  }
 }
diff --git a/mesop/event_handler/event_handler.py b/mesop/event_handler/event_handler.py
index c53f43019..e5cda59da 100644
--- a/mesop/event_handler/event_handler.py
+++ b/mesop/event_handler/event_handler.py
@@ -49,3 +49,11 @@ def wrapper(action: E):
     key=key,
   ),
 )
+
+runtime().register_event_mapper(
+  events.InputEvent,
+  lambda userEvent, key: events.InputEvent(
+    value=userEvent.string,
+    key=key,
+  ),
+)
diff --git a/mesop/events/__init__.py b/mesop/events/__init__.py
index f4585815b..6fcb54ead 100644
--- a/mesop/events/__init__.py
+++ b/mesop/events/__init__.py
@@ -4,6 +4,9 @@
 from .events import (
   ClickEvent as ClickEvent,
 )
+from .events import (
+  InputEvent as InputEvent,
+)
 from .events import (
   MesopEvent as MesopEvent,
 )
diff --git a/mesop/events/events.py b/mesop/events/events.py
index 6a0cbf6d5..0b740dd68 100644
--- a/mesop/events/events.py
+++ b/mesop/events/events.py
@@ -13,6 +13,11 @@ class ClickEvent(MesopEvent):
   pass
 
 
+@dataclass
+class InputEvent(MesopEvent):
+  value: str
+
+
 @dataclass
 class ChangeEvent(MesopEvent):
   value: str