diff --git a/kayak_font/Cargo.toml b/kayak_font/Cargo.toml
index 305c83cd2cdf1328bf60af9168b28cb874cf50f2..03ee9c3e382bae688eb059301d93fd155d79bbb1 100644
--- a/kayak_font/Cargo.toml
+++ b/kayak_font/Cargo.toml
@@ -10,7 +10,7 @@ bevy_renderer = ["bevy"]
 [dependencies]
 anyhow = { version = "1.0" }
 nanoserde = "0.1.30"
-unicode-segmentation = "1.9"
+unicode-segmentation = "1.10.0"
 
 # Provides UAX #14 line break segmentation
 xi-unicode = "0.3"
diff --git a/kayak_font/src/font.rs b/kayak_font/src/font.rs
index 43be3b888bcf831abd5646b83f468dca03cfe83f..b1143fc66fe921344789fdbdd02bb7f5a256ebcf 100644
--- a/kayak_font/src/font.rs
+++ b/kayak_font/src/font.rs
@@ -92,6 +92,11 @@ impl KayakFont {
         width
     }
 
+    /// Splits up the provided &str into grapheme clusters.
+    pub fn get_graphemes<'a>(&'a self, content: &'a str) -> Vec<&'a str> {
+        UnicodeSegmentation::graphemes(content, true).collect::<Vec<_>>()
+    }
+
     /// Measures the given text content and calculates an appropriate layout
     /// given a set of properties.
     ///
diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs
index a11e2cb6997d9d3555b66fa58f599ef61b93fe8d..2ab6866681c67f1dc026ec82f3323b9f1a6251a2 100644
--- a/src/widgets/mod.rs
+++ b/src/widgets/mod.rs
@@ -86,7 +86,8 @@ pub struct KayakWidgets;
 
 impl Plugin for KayakWidgets {
     fn build(&self, app: &mut bevy::prelude::App) {
-        app.add_startup_system_to_stage(StartupStage::PostStartup, add_widget_systems);
+        app.add_startup_system_to_stage(StartupStage::PostStartup, add_widget_systems)
+            .add_system(text_box::cursor_animation_system);
     }
 }
 
diff --git a/src/widgets/text_box.rs b/src/widgets/text_box.rs
index 5c92aa98dbd07d6234af538bb4fd09f1e749ca44..8bf53c00891b88dd5b77c2fae9ec3a0719086b3f 100644
--- a/src/widgets/text_box.rs
+++ b/src/widgets/text_box.rs
@@ -1,5 +1,8 @@
-use bevy::prelude::{Bundle, Color, Commands, Component, Entity, In, Query};
-use kayak_ui_macros::rsx;
+use std::time::Instant;
+
+use bevy::prelude::*;
+use kayak_font::{KayakFont, TextProperties};
+use kayak_ui_macros::{constructor, rsx};
 
 use crate::{
     context::WidgetName,
@@ -8,6 +11,7 @@ use crate::{
     on_event::OnEvent,
     on_layout::OnLayout,
     prelude::{KChildren, KayakWidgetContext, OnChange},
+    render::font::FontMapping,
     styles::{Edge, KStyle, RenderCommand, StyleProp, Units},
     widget::Widget,
     widget_state::WidgetState,
@@ -15,7 +19,7 @@ use crate::{
         text::{TextProps, TextWidgetBundle},
         BackgroundBundle, ClipBundle,
     },
-    Focusable,
+    Focusable, DEFAULT_FONT,
 };
 
 /// Props used by the [`TextBox`] widget
@@ -32,9 +36,27 @@ pub struct TextBoxProps {
     pub value: String,
 }
 
-#[derive(Component, Default, Clone, PartialEq)]
+#[derive(Component, Clone, PartialEq)]
 pub struct TextBoxState {
     pub focused: bool,
+    pub graphemes: Vec<String>,
+    pub cursor_x: f32,
+    pub cursor_position: usize,
+    pub cursor_visible: bool,
+    pub cursor_last_update: Instant,
+}
+
+impl Default for TextBoxState {
+    fn default() -> Self {
+        Self {
+            focused: Default::default(),
+            graphemes: Default::default(),
+            cursor_x: 0.0,
+            cursor_position: Default::default(),
+            cursor_visible: Default::default(),
+            cursor_last_update: Instant::now(),
+        }
+    }
 }
 
 pub struct TextBoxValue(pub String);
@@ -80,7 +102,9 @@ pub fn text_box_render(
         let state_entity = widget_context.use_state::<TextBoxState>(
             &mut commands,
             entity,
-            TextBoxState::default(),
+            TextBoxState {
+                ..TextBoxState::default()
+            },
         );
 
         if let Ok(state) = state_query.get(state_entity) {
@@ -119,6 +143,8 @@ pub fn text_box_render(
             let current_value = text_box.value.clone();
             let cloned_on_change = on_change.clone();
 
+            let style_font = styles.font.clone();
+
             *on_event = OnEvent::new(
                 move |In((event_dispatcher_context, _, mut event, _entity)): In<(
                     EventDispatcherContext,
@@ -126,8 +152,38 @@ pub fn text_box_render(
                     Event,
                     Entity,
                 )>,
+                      font_assets: Res<Assets<KayakFont>>,
+                      font_mapping: Res<FontMapping>,
                       mut state_query: Query<&mut TextBoxState>| {
                     match event.event_type {
+                        EventType::KeyDown(key_event) => {
+                            if key_event.key() == KeyCode::Right {
+                                if let Ok(mut state) = state_query.get_mut(state_entity) {
+                                    if state.cursor_position < state.graphemes.len() {
+                                        state.cursor_position += 1;
+                                    }
+                                    set_new_cursor_position(
+                                        &mut state,
+                                        &font_assets,
+                                        &font_mapping,
+                                        &style_font,
+                                    );
+                                }
+                            }
+                            if key_event.key() == KeyCode::Left {
+                                if let Ok(mut state) = state_query.get_mut(state_entity) {
+                                    if state.cursor_position > 0 {
+                                        state.cursor_position -= 1;
+                                    }
+                                    set_new_cursor_position(
+                                        &mut state,
+                                        &font_assets,
+                                        &font_mapping,
+                                        &style_font,
+                                    );
+                                }
+                            }
+                        }
                         EventType::CharInput { c } => {
                             let mut current_value = current_value.clone();
                             let cloned_on_change = cloned_on_change.clone();
@@ -140,17 +196,61 @@ pub fn text_box_render(
                             }
                             if is_backspace(c) {
                                 if !current_value.is_empty() {
-                                    current_value.truncate(current_value.len() - 1);
+                                    if let Ok(mut state) = state_query.get_mut(state_entity) {
+                                        // TODO: This doesn't respect graphemes!
+                                        current_value.remove(state.cursor_position - 1);
+                                        state.cursor_position -= 1;
+                                    }
                                 }
                             } else if !c.is_control() {
-                                current_value.push(c);
+                                if let Ok(mut state) = state_query.get_mut(state_entity) {
+                                    // TODO: This doesn't respect graphemes!
+                                    current_value.insert(state.cursor_position, c);
+                                    state.cursor_position += 1;
+                                }
+                            }
+
+                            if let Ok(mut state) = state_query.get_mut(state_entity) {
+                                // Update graphemes
+                                set_graphemes(
+                                    &mut state,
+                                    &font_assets,
+                                    &font_mapping,
+                                    &style_font,
+                                    &current_value,
+                                );
+
+                                set_new_cursor_position(
+                                    &mut state,
+                                    &font_assets,
+                                    &font_mapping,
+                                    &style_font,
+                                );
                             }
+
                             cloned_on_change.set_value(current_value);
                             event.add_system(cloned_on_change);
                         }
                         EventType::Focus => {
                             if let Ok(mut state) = state_query.get_mut(state_entity) {
                                 state.focused = true;
+                                // Update graphemes
+                                set_graphemes(
+                                    &mut state,
+                                    &font_assets,
+                                    &font_mapping,
+                                    &style_font,
+                                    &current_value,
+                                );
+
+                                state.cursor_position = state.graphemes.len();
+
+                                set_new_cursor_position(
+                                    &mut state,
+                                    &font_assets,
+                                    &font_mapping,
+                                    &style_font,
+                                );
                             }
                         }
                         EventType::Blur => {
@@ -164,6 +264,16 @@ pub fn text_box_render(
                 },
             );
 
+            let cursor_styles = KStyle {
+                background_color: Color::rgba(0.933, 0.745, 0.745, 1.0).into(),
+                position_type: crate::styles::KPositionType::SelfDirected.into(),
+                top: Units::Pixels(5.0).into(),
+                left: Units::Pixels(state.cursor_x).into(),
+                width: Units::Pixels(2.0).into(),
+                height: Units::Pixels(26.0 - 10.0).into(),
+                ..Default::default()
+            };
+
             let parent_id = Some(entity);
             rsx! {
                 <BackgroundBundle styles={background_styles}>
@@ -188,6 +298,13 @@ pub fn text_box_render(
                                 ..Default::default()
                             }}
                         />
+                        {
+                            if state.focused && state.cursor_visible {
+                                constructor! {
+                                    <BackgroundBundle styles={cursor_styles} />
+                                }
+                            }
+                        }
                     </ClipBundle>
                 </BackgroundBundle>
             }
@@ -203,3 +320,71 @@ pub fn text_box_render(
 fn is_backspace(c: char) -> bool {
     c == '\u{8}' || c == '\u{7f}'
 }
+
+fn set_graphemes(
+    state: &mut TextBoxState,
+    font_assets: &Res<Assets<KayakFont>>,
+    font_mapping: &FontMapping,
+    style_font: &StyleProp<String>,
+    current_value: &String,
+) {
+    let font_handle = match style_font {
+        StyleProp::Value(font) => font_mapping.get_handle(font.clone()).unwrap(),
+        _ => font_mapping.get_handle(DEFAULT_FONT.into()).unwrap(),
+    };
+
+    if let Some(font) = font_assets.get(&font_handle) {
+        state.graphemes = font
+            .get_graphemes(&current_value)
+            .iter()
+            .map(|s| s.to_string())
+            .collect::<Vec<_>>();
+    }
+}
+
+fn set_new_cursor_position(
+    state: &mut TextBoxState,
+    font_assets: &Res<Assets<KayakFont>>,
+    font_mapping: &FontMapping,
+    style_font: &StyleProp<String>,
+) {
+    let font_handle = match style_font {
+        StyleProp::Value(font) => font_mapping.get_handle(font.clone()).unwrap(),
+        _ => font_mapping.get_handle(DEFAULT_FONT.into()).unwrap(),
+    };
+
+    if let Some(font) = font_assets.get(&font_handle) {
+        let string_to_cursor = state.graphemes[0..state.cursor_position].join("");
+        let measurement = font.measure(
+            &string_to_cursor,
+            TextProperties {
+                font_size: 14.0,
+                line_height: 18.0,
+                max_size: (10000.0, 18.0),
+                alignment: kayak_font::Alignment::Start,
+                tab_size: 4,
+            },
+        );
+
+        state.cursor_x = measurement.size().0;
+    }
+}
+
+pub fn cursor_animation_system(
+    mut state_query: ParamSet<(Query<(Entity, &TextBoxState)>, Query<&mut TextBoxState>)>,
+) {
+    let mut should_update = Vec::new();
+
+    for (entity, state) in state_query.p0().iter() {
+        if state.cursor_last_update.elapsed().as_secs_f32() > 0.5 && state.focused {
+            should_update.push(entity);
+        }
+    }
+
+    for state_entity in should_update.drain(..) {
+        if let Ok(mut state) = state_query.p1().get_mut(state_entity) {
+            state.cursor_last_update = Instant::now();
+            state.cursor_visible = !state.cursor_visible;
+        }
+    }
+}