diff --git a/napari/_vispy/_tests/test_vispy_scale_bar_visual.py b/napari/_vispy/_tests/test_vispy_scale_bar_visual.py
index e899f2f42b2..1e9391ea6b2 100644
--- a/napari/_vispy/_tests/test_vispy_scale_bar_visual.py
+++ b/napari/_vispy/_tests/test_vispy_scale_bar_visual.py
@@ -1,3 +1,7 @@
+from unittest.mock import MagicMock
+
+import pytest
+
 from napari._vispy.overlays.scale_bar import VispyScaleBarOverlay
 from napari.components.overlays import ScaleBarOverlay
 
@@ -9,3 +13,30 @@ def test_scale_bar_instantiation(make_napari_viewer):
     assert vispy_scale_bar.overlay.length is None
     model.length = 50
     assert vispy_scale_bar.overlay.length == 50
+
+
+def test_scale_bar_positioning(make_napari_viewer):
+    viewer = make_napari_viewer()
+    # set devicePixelRatio to 2 so testing works on CI and local
+    viewer.window._qt_window.devicePixelRatio = MagicMock(return_value=2)
+    model = ScaleBarOverlay()
+    scale_bar = VispyScaleBarOverlay(overlay=model, viewer=viewer)
+
+    assert model.position == 'bottom_right'
+    assert model.font_size == 10
+    assert scale_bar.y_offset == 20
+
+    model.position = 'top_right'
+    assert scale_bar.y_offset == pytest.approx(20.333, abs=0.1)
+
+    # increasing size while at top should increase y_offset to 7 + font_size*1.33
+    model.font_size = 30
+    assert scale_bar.y_offset == pytest.approx(47, abs=0.1)
+
+    # moving scale bar back to bottom should reset y_offset to 20
+    model.position = 'bottom_right'
+    assert scale_bar.y_offset == 20
+
+    # changing font_size at bottom should have no effect
+    model.font_size = 50
+    assert scale_bar.y_offset == 20
diff --git a/napari/_vispy/overlays/scale_bar.py b/napari/_vispy/overlays/scale_bar.py
index 94c9e3e0263..d1dc64dd993 100644
--- a/napari/_vispy/overlays/scale_bar.py
+++ b/napari/_vispy/overlays/scale_bar.py
@@ -135,7 +135,7 @@ def _on_zoom_change(self, *, force: bool = False):
         self.node.transform.scale = [scale, 1, 1, 1]
         self.node.text.text = f'{new_dim:g~#P}'
         self.x_size = scale  # needed to offset properly
-        self._on_position_change()
+        super()._on_position_change()
 
     def _on_data_change(self):
         """Change color and data of scale bar and box."""
@@ -173,7 +173,37 @@ def _on_box_change(self):
 
     def _on_text_change(self):
         """Update text information"""
-        self.node.text.font_size = self.overlay.font_size
+        # update the dpi scale factor to account for screen dpi
+        # because vispy scales pixel height of text by screen dpi
+        if self.node.text.transforms.dpi:
+            # use 96 as the napari reference dpi for historical reasons
+            dpi_scale_factor = 96 / self.node.text.transforms.dpi
+        else:
+            dpi_scale_factor = 1
+
+        self.node.text.font_size = self.overlay.font_size * dpi_scale_factor
+        # ensure we recalculate the y_offset from the text size when at top of canvas
+        if 'top' in self.overlay.position:
+            self._on_position_change()
+
+    def _on_position_change(self, event=None):
+        # prevent the text from being cut off by shifting down
+        if 'top' in self.overlay.position:
+            # convert font_size to logical pixels as vispy does
+            # in vispy/visuals/text/text.py
+            # 72 is the vispy reference dpi
+            # 96 dpi is used as the napari reference dpi
+            font_logical_pix = self.overlay.font_size * 96 / 72
+            # 7 is base value for the default 10 font size
+            self.y_offset = 7 + font_logical_pix
+        else:
+            self.y_offset = 20
+        super()._on_position_change()
+
+    def _on_visible_change(self):
+        # ensure that dpi is updated when the scale bar is visible
+        self._on_text_change()
+        return super()._on_visible_change()
 
     def reset(self):
         super().reset()