diff --git a/Marlin/src/feature/joystick.cpp b/Marlin/src/feature/joystick.cpp
index efadeeff30..45507339c5 100644
--- a/Marlin/src/feature/joystick.cpp
+++ b/Marlin/src/feature/joystick.cpp
@@ -36,6 +36,10 @@
 
 Joystick joystick;
 
+#if ENABLED(EXTENSIBLE_UI)
+  #include "../lcd/extensible_ui/ui_api.h"
+#endif
+
 #if HAS_JOY_ADC_X
   temp_info_t Joystick::x; // = { 0 }
 #endif
@@ -65,35 +69,39 @@ Joystick joystick;
   }
 #endif
 
-void Joystick::calculate(float norm_jog[XYZ]) {
-  // Do nothing if enable pin (active-low) is not LOW
-  #if HAS_JOY_ADC_EN
-    if (READ(JOY_EN_PIN)) return;
-  #endif
+#if HAS_JOY_ADC_X || HAS_JOY_ADC_Y || HAS_JOY_ADC_Z
 
-  auto _normalize_joy = [](float &adc, const int16_t raw, const int16_t (&joy_limits)[4]) {
-    if (WITHIN(raw, joy_limits[0], joy_limits[3])) {
-      // within limits, check deadzone
-      if (raw > joy_limits[2])
-        adc = (raw - joy_limits[2]) / float(joy_limits[3] - joy_limits[2]);
-      else if (raw < joy_limits[1])
-        adc = (raw - joy_limits[1]) / float(joy_limits[1] - joy_limits[0]);  // negative value
-    }
-  };
+  void Joystick::calculate(float (&norm_jog)[XYZ]) {
+    // Do nothing if enable pin (active-low) is not LOW
+    #if HAS_JOY_ADC_EN
+      if (READ(JOY_EN_PIN)) return;
+    #endif
 
-  #if HAS_JOY_ADC_X
-    static constexpr int16_t joy_x_limits[4] = JOY_X_LIMITS;
-    _normalize_joy(norm_jog[X_AXIS], x.raw, joy_x_limits);
-  #endif
-  #if HAS_JOY_ADC_Y
-    static constexpr int16_t joy_y_limits[4] = JOY_Y_LIMITS;
-    _normalize_joy(norm_jog[Y_AXIS], y.raw, joy_y_limits);
-  #endif
-  #if HAS_JOY_ADC_Z
-    static constexpr int16_t joy_z_limits[4] = JOY_Z_LIMITS;
-    _normalize_joy(norm_jog[Z_AXIS], z.raw, joy_z_limits);
-  #endif
-}
+    auto _normalize_joy = [](float &adc, const int16_t raw, const int16_t (&joy_limits)[4]) {
+      if (WITHIN(raw, joy_limits[0], joy_limits[3])) {
+        // within limits, check deadzone
+        if (raw > joy_limits[2])
+          adc = (raw - joy_limits[2]) / float(joy_limits[3] - joy_limits[2]);
+        else if (raw < joy_limits[1])
+          adc = (raw - joy_limits[1]) / float(joy_limits[1] - joy_limits[0]);  // negative value
+      }
+    };
+
+    #if HAS_JOY_ADC_X
+      static constexpr int16_t joy_x_limits[4] = JOY_X_LIMITS;
+      _normalize_joy(norm_jog[X_AXIS], x.raw, joy_x_limits);
+    #endif
+    #if HAS_JOY_ADC_Y
+      static constexpr int16_t joy_y_limits[4] = JOY_Y_LIMITS;
+      _normalize_joy(norm_jog[Y_AXIS], y.raw, joy_y_limits);
+    #endif
+    #if HAS_JOY_ADC_Z
+      static constexpr int16_t joy_z_limits[4] = JOY_Z_LIMITS;
+      _normalize_joy(norm_jog[Z_AXIS], z.raw, joy_z_limits);
+    #endif
+  }
+
+#endif
 
 #if ENABLED(POLL_JOG)
 
@@ -122,11 +130,19 @@ void Joystick::calculate(float norm_jog[XYZ]) {
     float norm_jog[XYZ] = { 0 };
 
     // Use ADC values and defined limits. The active zone is normalized: -1..0 (dead) 0..1
-    joystick.calculate(norm_jog);
+    #if HAS_JOY_ADC_X || HAS_JOY_ADC_Y || HAS_JOY_ADC_Z
+      joystick.calculate(norm_jog);
+    #endif
 
     // Other non-joystick poll-based jogging could be implemented here
     // with "jogging" encapsulated as a more general class.
 
+    #if ENABLED(EXTENSIBLE_UI)
+      norm_jog[X_AXIS] = ExtUI::norm_jog[X_AXIS];
+      norm_jog[Y_AXIS] = ExtUI::norm_jog[Y_AXIS];
+      norm_jog[Z_AXIS] = ExtUI::norm_jog[Z_AXIS];
+    #endif
+
     // Jogging value maps continuously (quadratic relationship) to feedrate
     float move_dist[XYZ] = { 0 }, hypot2 = 0;
     LOOP_XYZ(i) if (norm_jog[i]) {
diff --git a/Marlin/src/feature/joystick.h b/Marlin/src/feature/joystick.h
index 57dd5deeb5..e96120517e 100644
--- a/Marlin/src/feature/joystick.h
+++ b/Marlin/src/feature/joystick.h
@@ -46,7 +46,7 @@ class Joystick {
     #if ENABLED(JOYSTICK_DEBUG)
       static void report();
     #endif
-    static void calculate(float norm_jog[XYZ]);
+    static void calculate(float (&norm_jog)[XYZ]);
     static void inject_jog_moves();
 };
 
diff --git a/Marlin/src/lcd/extensible_ui/lib/lulzbot/ftdi_eve_lib/extended/event_loop.cpp b/Marlin/src/lcd/extensible_ui/lib/lulzbot/ftdi_eve_lib/extended/event_loop.cpp
index a9194fdaba..3fe868ec7e 100644
--- a/Marlin/src/lcd/extensible_ui/lib/lulzbot/ftdi_eve_lib/extended/event_loop.cpp
+++ b/Marlin/src/lcd/extensible_ui/lib/lulzbot/ftdi_eve_lib/extended/event_loop.cpp
@@ -156,9 +156,11 @@ namespace FTDI {
         if (!UIData::flags.bits.touch_debouncing) {
           if (tag == pressed_tag) {
             // The user is holding down a button.
-            if (touch_timer.elapsed(1000 / TOUCH_REPEATS_PER_SECOND) && current_screen.onTouchHeld(tag)) {
-              current_screen.onRefresh();
-              if (UIData::flags.bits.touch_repeat_sound) sound.play(repeat_sound);
+            if (touch_timer.elapsed(1000 / TOUCH_REPEATS_PER_SECOND)) {
+              if (current_screen.onTouchHeld(tag)) {
+                current_screen.onRefresh();
+                if (UIData::flags.bits.touch_repeat_sound) sound.play(repeat_sound);
+              }
               touch_timer.start();
             }
           }
diff --git a/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/bio_confirm_home_e.cpp b/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/bio_confirm_home_e.cpp
index c43a80e1c3..ec6b6045e7 100644
--- a/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/bio_confirm_home_e.cpp
+++ b/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/bio_confirm_home_e.cpp
@@ -36,13 +36,7 @@ void BioConfirmHomeE::onRedraw(draw_mode_t) {
 bool BioConfirmHomeE::onTouchEnd(uint8_t tag) {
   switch (tag) {
     case 1:
-      SpinnerDialogBox::enqueueAndWait_P(F(
-        "G112\n"                            /* Home extruder */
-        LULZBOT_AXIS_LEVELING_COMMANDS      /* Level X axis */
-        "G0 X115 Z50 F6000\n"               /* Goto loading position */
-        "M400\n"                            /* Wait for moves to finish */
-        "M18 X Y"                           /* Unlock motors */
-      ));
+      SpinnerDialogBox::enqueueAndWait_P(F(LULZBOT_HOME_E_COMMANDS));
       current_screen.forget();
       break;
     case 2:
diff --git a/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/bio_confirm_home_xyz.cpp b/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/bio_confirm_home_xyz.cpp
index 091a2baaf3..71fc02bd14 100644
--- a/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/bio_confirm_home_xyz.cpp
+++ b/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/bio_confirm_home_xyz.cpp
@@ -36,10 +36,7 @@ void BioConfirmHomeXYZ::onRedraw(draw_mode_t) {
 bool BioConfirmHomeXYZ::onTouchEnd(uint8_t tag) {
   switch (tag) {
     case 1:
-      SpinnerDialogBox::enqueueAndWait_P(F(
-        "G28 X Y Z\n"             /* Home all axis */
-        "G0 X115 Z50 F6000"       /* Move to park position */
-      ));
+      SpinnerDialogBox::enqueueAndWait_P(F(LULZBOT_HOME_XYZ_COMMANDS));
       current_screen.forget();
       break;
     case 2:
diff --git a/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/bio_printing_dialog_box.cpp b/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/bio_printing_dialog_box.cpp
index 93e3335574..c24eb53a74 100644
--- a/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/bio_printing_dialog_box.cpp
+++ b/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/bio_printing_dialog_box.cpp
@@ -141,6 +141,7 @@ void BioPrintingDialogBox::setStatusMessage(const char* message) {
 }
 
 void BioPrintingDialogBox::onIdle() {
+  reset_menu_timeout();
   if (refresh_timer.elapsed(STATUS_UPDATE_INTERVAL)) {
     onRefresh();
     refresh_timer.start();
diff --git a/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/bio_status_screen.cpp b/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/bio_status_screen.cpp
index f414fef47b..3f8b3d611a 100644
--- a/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/bio_status_screen.cpp
+++ b/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/bio_status_screen.cpp
@@ -37,6 +37,9 @@
 #define POLY(A) PolyUI::poly_reader_t(A, sizeof(A)/sizeof(A[0]))
 
 const uint8_t shadow_depth = 5;
+const float   max_speed = 0.30;
+const float   min_speed = 0.05;
+const uint8_t num_speeds = 10;
 
 using namespace FTDI;
 using namespace Theme;
@@ -248,7 +251,7 @@ void StatusScreen::onRedraw(draw_mode_t what) {
 }
 
 bool StatusScreen::onTouchStart(uint8_t) {
-  increment = fine_motion ? 0.25 : 1;
+  increment = min_speed;
   return true;
 }
 
@@ -263,6 +266,11 @@ bool StatusScreen::onTouchEnd(uint8_t tag) {
         jog_xy = true;
         injectCommands_P(PSTR("M17"));
       }
+      jog(0,  0,  0);
+      break;
+    case 5:
+    case 6:
+      jog(0,  0,  0);
       break;
     case 9:  GOTO_SCREEN(FilesScreen); break;
     case 10: GOTO_SCREEN(MainMenu); break;
@@ -280,25 +288,31 @@ bool StatusScreen::onTouchEnd(uint8_t tag) {
 
 bool StatusScreen::onTouchHeld(uint8_t tag) {
   if (tag >= 1 && tag <= 4 && !jog_xy) return false;
-  if (ExtUI::isMoving()) return false; // Don't allow moves to accumulate
-  #define UI_INCREMENT_AXIS(axis) MoveAxisScreen::setManualFeedrate(axis, increment); UI_INCREMENT(AxisPosition_mm, axis);
-  #define UI_DECREMENT_AXIS(axis) MoveAxisScreen::setManualFeedrate(axis, increment); UI_DECREMENT(AxisPosition_mm, axis);
+  const float s = fine_motion ? min_speed : increment;
   switch (tag) {
-    case 1: UI_DECREMENT_AXIS(X);  break;
-    case 2: UI_INCREMENT_AXIS(X);  break;
-    case 4: UI_DECREMENT_AXIS(Y);  break; // NOTE: Y directions inverted because bed rather than needle moves
-    case 3: UI_INCREMENT_AXIS(Y);  break;
-    case 5: UI_DECREMENT_AXIS(Z);  break;
-    case 6: UI_INCREMENT_AXIS(Z);  break;
-    case 7: UI_DECREMENT_AXIS(E0); break;
-    case 8: UI_INCREMENT_AXIS(E0); break;
-    default: return false;
+    case 1: jog(-s,  0,  0); break;
+    case 2: jog( s,  0,  0); break;
+    case 4: jog( 0, -s,  0); break; // NOTE: Y directions inverted because bed rather than needle moves
+    case 3: jog( 0,  s,  0); break;
+    case 5: jog( 0,  0, -s); break;
+    case 6: jog( 0,  0,  s); break;
+    case 7:
+      if (ExtUI::isMoving()) return false;
+      MoveAxisScreen::setManualFeedrate(E0, 1);
+      UI_INCREMENT(AxisPosition_mm, E0);
+      current_screen.onRefresh();
+      break;
+    case 8:
+      if (ExtUI::isMoving()) return false;
+      MoveAxisScreen::setManualFeedrate(E0, 1);
+      UI_DECREMENT(AxisPosition_mm, E0);
+      current_screen.onRefresh();
+      break;
+    default:
+      return false;
   }
-  #undef UI_DECREMENT_AXIS
-  #undef UI_INCREMENT_AXIS
-  if (increment < 10 && !fine_motion)
-    increment += 0.5;
-  current_screen.onRefresh();
+  if (increment < max_speed)
+    increment += (max_speed - min_speed) / num_speeds;
   return false;
 }
 
diff --git a/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/main_menu.cpp b/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/main_menu.cpp
index 827e3b5a15..ae6dd58c24 100644
--- a/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/main_menu.cpp
+++ b/Marlin/src/lcd/extensible_ui/lib/lulzbot/screens/main_menu.cpp
@@ -69,7 +69,7 @@ void MainMenu::onRedraw(draw_mode_t what) {
     #else
       #define GRID_ROWS 5
       #define GRID_COLS 2
-        .tag(2).button( BTN_POS(1,1), BTN_SIZE(1,1), GET_TEXT(AUTO_HOME))
+        .tag(2).button( BTN_POS(1,1), BTN_SIZE(1,1), GET_TEXTF(AUTO_HOME))
         #if ENABLED(NOZZLE_CLEAN_FEATURE)
          .enabled(1)
         #else
diff --git a/Marlin/src/lcd/extensible_ui/ui_api.cpp b/Marlin/src/lcd/extensible_ui/ui_api.cpp
index 851e8179ba..28b2ec006a 100644
--- a/Marlin/src/lcd/extensible_ui/ui_api.cpp
+++ b/Marlin/src/lcd/extensible_ui/ui_api.cpp
@@ -102,12 +102,16 @@
   #include "../../feature/host_actions.h"
 #endif
 
-static struct {
-  uint8_t printer_killed  : 1;
-  uint8_t manual_motion : 1;
-} flags;
-
 namespace ExtUI {
+  static struct {
+    uint8_t printer_killed  : 1;
+    uint8_t manual_motion   : 1;
+  } flags;
+
+  #if ENABLED(JOYSTICK)
+    float norm_jog[XYZ];
+  #endif
+
   #ifdef __SAM3X8E__
     /**
      * Implement a special millis() to allow time measurement
@@ -193,6 +197,14 @@ namespace ExtUI {
     #endif
   }
 
+  void jog(float dx, float dy, float dz) {
+    #if ENABLED(JOYSTICK)
+      norm_jog[X] = dx;
+      norm_jog[Y] = dy;
+      norm_jog[Z] = dz;
+    #endif
+  }
+
   bool isHeaterIdle(const extruder_t extruder) {
     return false
       #if HOTENDS && HEATER_IDLE_HANDLER
@@ -1037,9 +1049,10 @@ void MarlinUI::update() {
 }
 
 void MarlinUI::kill_screen(PGM_P const msg) {
+  using namespace ExtUI;
   if (!flags.printer_killed) {
     flags.printer_killed = true;
-    ExtUI::onPrinterKilled(msg);
+    onPrinterKilled(msg);
   }
 }
 
diff --git a/Marlin/src/lcd/extensible_ui/ui_api.h b/Marlin/src/lcd/extensible_ui/ui_api.h
index 29cfea61fe..6d040e2db9 100644
--- a/Marlin/src/lcd/extensible_ui/ui_api.h
+++ b/Marlin/src/lcd/extensible_ui/ui_api.h
@@ -45,6 +45,11 @@
 #include "../../inc/MarlinConfig.h"
 
 namespace ExtUI {
+
+  #if ENABLED(JOYSTICK)
+    extern float norm_jog[];
+  #endif
+
   // The ExtUI implementation can store up to this many bytes
   // in the EEPROM when the methods onStoreSettings and
   // onLoadSettings are called.
@@ -79,6 +84,8 @@ namespace ExtUI {
   void enableHeater(const heater_t);
   void enableHeater(const extruder_t);
 
+  void jog(float dx, float dy, float dz);
+
   /**
    * Getters and setters
    * Should be used by the EXTENSIBLE_UI to query or change Marlin's state.