diff --git a/NAMESPACE b/NAMESPACE
index 50bd3625..2bf18bc8 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -37,6 +37,7 @@ export(type_segments)
 export(type_spineplot)
 export(type_spline)
 export(type_summary)
+export(type_text)
 export(type_vline)
 importFrom(grDevices,adjustcolor)
 importFrom(grDevices,as.raster)
diff --git a/NEWS.md b/NEWS.md
index 1f596e61..3528edcb 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -60,6 +60,7 @@ New plot types:
     (#252 @vincentarelbundock, @zeileis, and @grantmcdermott)
     - `type_rug()` (shortcut: `"rug"`) adds a rug to an existing plot. (#276
     @grantmcdermott)
+    - `type_text()` (shortcut: `"text"`) adds text annotations. (@vincentarelbundock)
   - Models:
     - `type_glm()` (shortcut: `"glm"`) (@vincentarelbundock)
     - `type_lm()` (shortcut: `"lm"`) (@vincentarelbundock)
diff --git a/R/by_aesthetics.R b/R/by_aesthetics.R
index 7a93a9bb..805f1862 100755
--- a/R/by_aesthetics.R
+++ b/R/by_aesthetics.R
@@ -123,7 +123,9 @@ by_col = function(ngrps = 1L, col = NULL, palette = NULL, gradient = NULL, order
 
 by_pch = function(ngrps, type, pch = NULL) {
   no_pch = FALSE
-  if (!type %in% c("p", "b", "o", "pointrange", "errorbar", "boxplot", "qq")) {
+  if (identical(type, "text")) {
+    pch <- rep(15, ngrps)
+  } else if (!type %in% c("p", "b", "o", "pointrange", "errorbar", "boxplot", "qq")) {
     no_pch = TRUE
     pch = NULL
 
diff --git a/R/sanitize.R b/R/sanitize.R
index 8202b5bd..8a862437 100644
--- a/R/sanitize.R
+++ b/R/sanitize.R
@@ -17,7 +17,7 @@ sanitize_type = function(type, x, y, dots) {
     "abline", "area", "boxplot", "errorbar", "function", "glm", "hist",
     "histogram", "hline", "j", "jitter", "lines", "lm", "loess", "pointrange",
     "points", "polygon", "polypath", "qq", "rect", "ribbon", "ridge", "rug",
-    "segments", "spineplot", "spline", "vline"
+    "segments", "spineplot", "spline", "text", "vline"
   )
   assert_choice(type, types, null.ok = TRUE)
 
@@ -60,6 +60,7 @@ sanitize_type = function(type, x, y, dots) {
     "segments"   = type_segments,
     "spineplot"  = type_spineplot,
     "spline"     = type_spline,
+    "text"       = type_text,
     "vline"      = type_vline,
     type           # default case
   )
diff --git a/R/tinyplot.R b/R/tinyplot.R
index fede03eb..629eae74 100644
--- a/R/tinyplot.R
+++ b/R/tinyplot.R
@@ -100,6 +100,7 @@
 #'       - `"rect"` / [`type_rect()`]: Draws rectangles; requires `xmin`, `xmax`, `ymin`, and `ymax`.
 #'       - `"ribbon"` / [`type_ribbon()`]: Creates a filled area between `ymin` and `ymax`.
 #'       - `"segments"` / [`type_segments()`]: Draws line segments between pairs of points.
+#'       - `"text"` / [`type_text()`]: Add text annotations.
 #'     - Visualizations:
 #'       - `"boxplot"` / [`type_boxplot()`]: Creates a box-and-whisker plot.
 #'       - `"density"`: Plots the density estimate of a variable.
@@ -118,7 +119,7 @@
 #'       - [`type_abline()`]: line(s) with intercept and slope.
 #'       - [`type_hline()`]: horizontal line(s).
 #'       - [`type_vline()`]: vertical line(s).
-#'       - [`type_function()`]: arbitrary function. 
+#'       - [`type_function()`]: arbitrary function.
 #'       - [`type_summary()`]: summarize `y` by unique values of `x`.
 #' @param xmin,xmax,ymin,ymax minimum and maximum coordinates of relevant area
 #'   or interval plot types. Only used when the `type` argument is one of
@@ -500,11 +501,12 @@
 #'
 #' # It's possible to customize the look of your plots by setting graphical
 #' # parameters (e.g., via `(t)par`)... But a more convenient way is to just use
-#' # built-in themes (see `?tinytheme`). 
+#' # built-in themes (see `?tinytheme`).
 #'
 #' tinytheme("clean2")
 #' tinyplot(
-#'   Temp ~ Day | Month, data = aq,
+#'   Temp ~ Day | Month,
+#'   data = aq,
 #'   type = "b",
 #'   alpha = 0.5,
 #'   main = "Daily temperatures by month",
@@ -1170,6 +1172,7 @@ tinyplot.default = function(
       ixmax = idata[[ii]]$xmax
       iymin = idata[[ii]]$ymin
       iymax = idata[[ii]]$ymax
+      ilabels = idata[[ii]][["labels"]]
 
       if (isTRUE(by_continuous)) {
         icol = idata[[ii]]$col
@@ -1230,6 +1233,7 @@ tinyplot.default = function(
           iy = iy,
           iymax = iymax,
           iymin = iymin,
+          ilabels = ilabels,
           iz = iz,
           cex = cex,
           dots = dots,
diff --git a/R/type_text.R b/R/type_text.R
new file mode 100644
index 00000000..1a109215
--- /dev/null
+++ b/R/type_text.R
@@ -0,0 +1,49 @@
+#' Text annotations plot type
+#'
+#' @description Type function for adding text annotations to a plot. This function allows
+#' you to draw text at specified (x,y) coordinates. If no label is provided, the y-values
+#' will be used as the text labels.
+#'
+#' @param labels Character vector of the same length as the number of x,y coordinates.
+#' @param font Font to be used, following [graphics::par()]
+#' @inheritParams graphics::text
+#' @examples
+#'
+#' tinyplot(mpg ~ hp | factor(cyl),
+#'   data = mtcars,
+#'   type = type_text(
+#'     labels = row.names(mtcars),
+#'     font = 2,
+#'     adj = 0))
+#'
+#' @export
+type_text = function(labels, adj = NULL, pos = NULL, offset = 0.5, vfont = NULL, font = NULL) {
+  out = list(
+    draw = draw_text(adj = adj, pos = pos, offset = offset, vfont = vfont, font = font),
+    data = data_text(labels = labels),
+    name = "text"
+  )
+  class(out) = "tinyplot_type"
+  return(out)
+}
+
+data_text = function(labels) {
+  fun = function(datapoints, ...) {
+    assert_character(labels, name = "labels")
+    if (length(labels) != 1 && length(labels) != nrow(datapoints)) {
+      msg <- sprintf("`labels` must be of length 1 or %s.", nrow(datapoints))
+      stop(msg, call. = FALSE)
+    }
+    datapoints$labels = labels
+    out = list(datapoints = datapoints)
+    return(out)
+  }
+  return(fun)
+}
+
+draw_text = function(adj = NULL, pos = NULL, offset = 0.5, vfont = NULL, font = NULL) {
+  fun = function(ix, iy, ilabels, icol, cex, ...) {
+    text(x = ix, y = iy, labels = ilabels, col = icol, adj = adj, pos = pos, offset = offset, vfont = vfont, font = font, cex = cex)
+  }
+}
+
diff --git a/altdoc/quarto_website.yml b/altdoc/quarto_website.yml
index 290bc87c..0bab4ae3 100644
--- a/altdoc/quarto_website.yml
+++ b/altdoc/quarto_website.yml
@@ -59,6 +59,8 @@ website:
               file: man/type_rect.qmd
             - text: type_ribbon
               file: man/type_ribbon.qmd
+            - text: type_text
+              file: man/type_text.qmd
             - text: type_segments
               file: man/type_segments.qmd
           - section: Visualizations
diff --git a/inst/tinytest/_tinysnapshot/text_facets.svg b/inst/tinytest/_tinysnapshot/text_facets.svg
new file mode 100644
index 00000000..746d6527
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/text_facets.svg
@@ -0,0 +1,160 @@
+<?xml version='1.0' encoding='UTF-8' ?>
+<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' width='504.00pt' height='504.00pt' viewBox='0 0 504.00 504.00'>
+<defs>
+  <style type='text/css'><![CDATA[
+    .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle {
+      fill: none;
+      stroke: #000000;
+      stroke-linecap: round;
+      stroke-linejoin: round;
+      stroke-miterlimit: 10.00;
+    }
+    .svglite text {
+      white-space: pre;
+    }
+  ]]></style>
+</defs>
+<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/>
+<defs>
+  <clipPath id='cpMC4wMHw1MDQuMDB8MC4wMHw1MDQuMDA='>
+    <rect x='0.00' y='0.00' width='504.00' height='504.00' />
+  </clipPath>
+</defs>
+<g clip-path='url(#cpMC4wMHw1MDQuMDB8MC4wMHw1MDQuMDA=)'>
+<polygon points='464.78,240.30 470.18,240.30 470.18,234.90 464.78,234.90 ' style='stroke-width: 0.75; stroke: none; fill: #000000;' />
+<polygon points='464.78,254.70 470.18,254.70 470.18,249.30 464.78,249.30 ' style='stroke-width: 0.75; stroke: none; fill: #DF536B;' />
+<polygon points='464.78,269.10 470.18,269.10 470.18,263.70 464.78,263.70 ' style='stroke-width: 0.75; stroke: none; fill: #61D04F;' />
+<text x='473.52' y='223.20' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='52.68px' lengthAdjust='spacingAndGlyphs'>factor(cyl)</text>
+<text x='478.28' y='241.73' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text>
+<text x='478.28' y='256.13' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>6</text>
+<text x='478.28' y='270.53' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>8</text>
+</g>
+<defs>
+  <clipPath id='cpMC4wMHw0MzAuMDh8MC4wMHw1MDQuMDA='>
+    <rect x='0.00' y='0.00' width='430.08' height='504.00' />
+  </clipPath>
+</defs>
+<g clip-path='url(#cpMC4wMHw0MzAuMDh8MC4wMHw1MDQuMDA=)'>
+<text x='244.56' y='485.28' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>hp</text>
+<text transform='translate(12.96,244.80) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='23.35px' lengthAdjust='spacingAndGlyphs'>mpg</text>
+</g>
+<defs>
+  <clipPath id='cpNTkuMDR8MjI4LjcyfDU5LjA0fDQzMC41Ng=='>
+    <rect x='59.04' y='59.04' width='169.68' height='371.52' />
+  </clipPath>
+</defs>
+<g clip-path='url(#cpNTkuMDR8MjI4LjcyfDU5LjA0fDQzMC41Ng==)'>
+</g>
+<g clip-path='url(#cpMC4wMHw1MDQuMDB8MC4wMHw1MDQuMDA=)'>
+<line x1='64.21' y1='430.56' x2='203.00' y2='430.56' style='stroke-width: 0.75;' />
+<line x1='64.21' y1='430.56' x2='64.21' y2='437.76' style='stroke-width: 0.75;' />
+<line x1='91.97' y1='430.56' x2='91.97' y2='437.76' style='stroke-width: 0.75;' />
+<line x1='119.73' y1='430.56' x2='119.73' y2='437.76' style='stroke-width: 0.75;' />
+<line x1='147.49' y1='430.56' x2='147.49' y2='437.76' style='stroke-width: 0.75;' />
+<line x1='175.25' y1='430.56' x2='175.25' y2='437.76' style='stroke-width: 0.75;' />
+<line x1='203.00' y1='430.56' x2='203.00' y2='437.76' style='stroke-width: 0.75;' />
+<text x='64.21' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>50</text>
+<text x='91.97' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>100</text>
+<text x='147.49' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>200</text>
+<text x='203.00' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>300</text>
+<line x1='59.04' y1='422.66' x2='59.04' y2='129.89' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='422.66' x2='51.84' y2='422.66' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='349.46' x2='51.84' y2='349.46' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='276.27' x2='51.84' y2='276.27' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='203.08' x2='51.84' y2='203.08' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='129.89' x2='51.84' y2='129.89' style='stroke-width: 0.75;' />
+<text transform='translate(41.76,422.66) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>10</text>
+<text transform='translate(41.76,349.46) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>15</text>
+<text transform='translate(41.76,276.27) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>20</text>
+<text transform='translate(41.76,203.08) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>25</text>
+<text transform='translate(41.76,129.89) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>30</text>
+<rect x='59.04' y='43.20' width='169.68' height='15.84' style='stroke-width: 0.75; stroke: none;' />
+<text x='143.88' y='53.28' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>0</text>
+<polygon points='59.04,430.56 228.72,430.56 228.72,59.04 59.04,59.04 ' style='stroke-width: 0.75;' />
+</g>
+<defs>
+  <clipPath id='cpMjYwLjQwfDQzMC4wOHw1OS4wNHw0MzAuNTY='>
+    <rect x='260.40' y='59.04' width='169.68' height='371.52' />
+  </clipPath>
+</defs>
+<g clip-path='url(#cpMjYwLjQwfDQzMC4wOHw1OS4wNHw0MzAuNTY=)'>
+</g>
+<g clip-path='url(#cpMC4wMHw1MDQuMDB8MC4wMHw1MDQuMDA=)'>
+<line x1='265.57' y1='430.56' x2='404.36' y2='430.56' style='stroke-width: 0.75;' />
+<line x1='265.57' y1='430.56' x2='265.57' y2='437.76' style='stroke-width: 0.75;' />
+<line x1='293.33' y1='430.56' x2='293.33' y2='437.76' style='stroke-width: 0.75;' />
+<line x1='321.09' y1='430.56' x2='321.09' y2='437.76' style='stroke-width: 0.75;' />
+<line x1='348.85' y1='430.56' x2='348.85' y2='437.76' style='stroke-width: 0.75;' />
+<line x1='376.60' y1='430.56' x2='376.60' y2='437.76' style='stroke-width: 0.75;' />
+<line x1='404.36' y1='430.56' x2='404.36' y2='437.76' style='stroke-width: 0.75;' />
+<text x='265.57' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>50</text>
+<text x='293.33' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>100</text>
+<text x='348.85' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>200</text>
+<text x='404.36' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='20.02px' lengthAdjust='spacingAndGlyphs'>300</text>
+<line x1='260.40' y1='422.66' x2='260.40' y2='129.89' style='stroke-width: 0.75;' />
+<line x1='260.40' y1='422.66' x2='253.20' y2='422.66' style='stroke-width: 0.75;' />
+<line x1='260.40' y1='349.46' x2='253.20' y2='349.46' style='stroke-width: 0.75;' />
+<line x1='260.40' y1='276.27' x2='253.20' y2='276.27' style='stroke-width: 0.75;' />
+<line x1='260.40' y1='203.08' x2='253.20' y2='203.08' style='stroke-width: 0.75;' />
+<line x1='260.40' y1='129.89' x2='253.20' y2='129.89' style='stroke-width: 0.75;' />
+<text transform='translate(243.12,422.66) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>10</text>
+<text transform='translate(243.12,349.46) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>15</text>
+<text transform='translate(243.12,276.27) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>20</text>
+<text transform='translate(243.12,203.08) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>25</text>
+<text transform='translate(243.12,129.89) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='13.35px' lengthAdjust='spacingAndGlyphs'>30</text>
+<rect x='260.40' y='43.20' width='169.68' height='15.84' style='stroke-width: 0.75; stroke: none;' />
+<text x='345.24' y='53.28' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>1</text>
+<polygon points='260.40,430.56 430.08,430.56 430.08,59.04 260.40,59.04 ' style='stroke-width: 0.75;' />
+</g>
+<g clip-path='url(#cpNTkuMDR8MjI4LjcyfDU5LjA0fDQzMC41Ng==)'>
+<text x='70.88' y='216.00' style='font-size: 12.00px; font-weight: bold; font-family: "Liberation Sans";' textLength='60.05px' lengthAdjust='spacingAndGlyphs'>Merc 240D</text>
+<text x='89.20' y='239.41' style='font-size: 12.00px; font-weight: bold; font-family: "Liberation Sans";' textLength='51.38px' lengthAdjust='spacingAndGlyphs'>Merc 230</text>
+<text x='90.31' y='257.26' style='font-size: 12.00px; font-weight: bold; font-family: "Liberation Sans";' textLength='83.78px' lengthAdjust='spacingAndGlyphs'>Toyota Corona</text>
+</g>
+<g clip-path='url(#cpMjYwLjQwfDQzMC4wOHw1OS4wNHw0MzAuNTY=)'>
+<text x='289.44' y='239.42' style='font-size: 12.00px; font-weight: bold; font-family: "Liberation Sans";' textLength='64.03px' lengthAdjust='spacingAndGlyphs'>Datsun 710</text>
+<text x='274.46' y='99.05' style='font-size: 12.00px; font-weight: bold; font-family: "Liberation Sans";' textLength='44.69px' lengthAdjust='spacingAndGlyphs'>Fiat 128</text>
+<text x='266.68' y='128.33' style='font-size: 12.00px; font-weight: bold; font-family: "Liberation Sans";' textLength='69.35px' lengthAdjust='spacingAndGlyphs'>Honda Civic</text>
+<text x='273.90' y='75.90' style='font-size: 12.00px; font-weight: bold; font-family: "Liberation Sans";' textLength='83.12px' lengthAdjust='spacingAndGlyphs'>Toyota Corolla</text>
+<text x='274.46' y='173.70' style='font-size: 12.00px; font-weight: bold; font-family: "Liberation Sans";' textLength='50.02px' lengthAdjust='spacingAndGlyphs'>Fiat X1-9</text>
+<text x='288.33' y='192.73' style='font-size: 12.00px; font-weight: bold; font-family: "Liberation Sans";' textLength='81.39px' lengthAdjust='spacingAndGlyphs'>Porsche 914-2</text>
+<text x='300.55' y='126.92' style='font-size: 12.00px; font-weight: bold; font-family: "Liberation Sans";' textLength='77.34px' lengthAdjust='spacingAndGlyphs'>Lotus Europa</text>
+<text x='298.33' y='260.07' style='font-size: 12.00px; font-weight: bold; font-family: "Liberation Sans";' textLength='63.14px' lengthAdjust='spacingAndGlyphs'>Volvo 142E</text>
+</g>
+<g clip-path='url(#cpNTkuMDR8MjI4LjcyfDU5LjA0fDQzMC41Ng==)'>
+<text x='97.52' y='260.07' style='font-size: 12.00px; font-weight: bold;fill: #DF536B; font-family: "Liberation Sans";' textLength='82.04px' lengthAdjust='spacingAndGlyphs'>Hornet 4 Drive</text>
+<text x='94.75' y='308.38' style='font-size: 12.00px; font-weight: bold;fill: #DF536B; font-family: "Liberation Sans";' textLength='38.69px' lengthAdjust='spacingAndGlyphs'>Valiant</text>
+<text x='104.74' y='292.12' style='font-size: 12.00px; font-weight: bold;fill: #DF536B; font-family: "Liberation Sans";' textLength='51.38px' lengthAdjust='spacingAndGlyphs'>Merc 280</text>
+<text x='104.74' y='312.61' style='font-size: 12.00px; font-weight: bold;fill: #DF536B; font-family: "Liberation Sans";' textLength='60.05px' lengthAdjust='spacingAndGlyphs'>Merc 280C</text>
+</g>
+<g clip-path='url(#cpMjYwLjQwfDQzMC4wOHw1OS4wNHw0MzAuNTY=)'>
+<text x='298.88' y='265.93' style='font-size: 12.00px; font-weight: bold;fill: #DF536B; font-family: "Liberation Sans";' textLength='63.37px' lengthAdjust='spacingAndGlyphs'>Mazda RX4</text>
+<text x='298.88' y='264.71' style='font-size: 12.00px; font-weight: bold;fill: #DF536B; font-family: "Liberation Sans";' textLength='91.58px' lengthAdjust='spacingAndGlyphs'>Mazda RX4 Wag</text>
+<text x='334.97' y='284.96' style='font-size: 12.00px; font-weight: bold;fill: #DF536B; font-family: "Liberation Sans";' textLength='68.03px' lengthAdjust='spacingAndGlyphs'>Ferrari Dino</text>
+</g>
+<g clip-path='url(#cpNTkuMDR8MjI4LjcyfDU5LjA0fDQzMC41Ng==)'>
+<text x='133.61' y='298.41' style='font-size: 12.00px; font-weight: bold;fill: #61D04F; font-family: "Liberation Sans";' textLength='106.00px' lengthAdjust='spacingAndGlyphs'>Hornet Sportabout</text>
+<text x='172.47' y='363.83' style='font-size: 12.00px; font-weight: bold;fill: #61D04F; font-family: "Liberation Sans";' textLength='61.37px' lengthAdjust='spacingAndGlyphs'>Duster 360</text>
+<text x='136.38' y='333.10' style='font-size: 12.00px; font-weight: bold;fill: #61D04F; font-family: "Liberation Sans";' textLength='67.39px' lengthAdjust='spacingAndGlyphs'>Merc 450SE</text>
+<text x='136.38' y='319.93' style='font-size: 12.00px; font-weight: bold;fill: #61D04F; font-family: "Liberation Sans";' textLength='66.72px' lengthAdjust='spacingAndGlyphs'>Merc 450SL</text>
+<text x='136.38' y='350.67' style='font-size: 12.00px; font-weight: bold;fill: #61D04F; font-family: "Liberation Sans";' textLength='75.38px' lengthAdjust='spacingAndGlyphs'>Merc 450SLC</text>
+<text x='150.26' y='421.09' style='font-size: 12.00px; font-weight: bold;fill: #61D04F; font-family: "Liberation Sans";' textLength='108.69px' lengthAdjust='spacingAndGlyphs'>Cadillac Fleetwood</text>
+<text x='155.82' y='421.09' style='font-size: 12.00px; font-weight: bold;fill: #61D04F; font-family: "Liberation Sans";' textLength='111.99px' lengthAdjust='spacingAndGlyphs'>Lincoln Continental</text>
+<text x='164.14' y='356.96' style='font-size: 12.00px; font-weight: bold;fill: #61D04F; font-family: "Liberation Sans";' textLength='98.06px' lengthAdjust='spacingAndGlyphs'>Chrysler Imperial</text>
+<text x='119.73' y='345.22' style='font-size: 12.00px; font-weight: bold;fill: #61D04F; font-family: "Liberation Sans";' textLength='102.69px' lengthAdjust='spacingAndGlyphs'>Dodge Challenger</text>
+<text x='119.73' y='350.83' style='font-size: 12.00px; font-weight: bold;fill: #61D04F; font-family: "Liberation Sans";' textLength='71.37px' lengthAdjust='spacingAndGlyphs'>AMC Javelin</text>
+<text x='172.47' y='378.48' style='font-size: 12.00px; font-weight: bold;fill: #61D04F; font-family: "Liberation Sans";' textLength='68.70px' lengthAdjust='spacingAndGlyphs'>Camaro Z28</text>
+<text x='133.61' y='292.27' style='font-size: 12.00px; font-weight: bold;fill: #61D04F; font-family: "Liberation Sans";' textLength='91.35px' lengthAdjust='spacingAndGlyphs'>Pontiac Firebird</text>
+</g>
+<g clip-path='url(#cpMjYwLjQwfDQzMC4wOHw1OS4wNHw0MzAuNTY=)'>
+<text x='384.38' y='342.04' style='font-size: 12.00px; font-weight: bold;fill: #61D04F; font-family: "Liberation Sans";' textLength='84.69px' lengthAdjust='spacingAndGlyphs'>Ford Pantera L</text>
+<text x='423.79' y='353.76' style='font-size: 12.00px; font-weight: bold;fill: #61D04F; font-family: "Liberation Sans";' textLength='79.38px' lengthAdjust='spacingAndGlyphs'>Maserati Bora</text>
+</g>
+<defs>
+  <clipPath id='cpMTAyLjI0fDQxNS42OHw4Ny44NHwzNzIuOTY='>
+    <rect x='102.24' y='87.84' width='313.44' height='285.12' />
+  </clipPath>
+</defs>
+<g clip-path='url(#cpMTAyLjI0fDQxNS42OHw4Ny44NHwzNzIuOTY=)'>
+</g>
+</svg>
diff --git a/inst/tinytest/_tinysnapshot/text_missing_label.svg b/inst/tinytest/_tinysnapshot/text_missing_label.svg
new file mode 100644
index 00000000..77f20625
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/text_missing_label.svg
@@ -0,0 +1,59 @@
+<?xml version='1.0' encoding='UTF-8' ?>
+<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' width='504.00pt' height='504.00pt' viewBox='0 0 504.00 504.00'>
+<defs>
+  <style type='text/css'><![CDATA[
+    .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle {
+      fill: none;
+      stroke: #000000;
+      stroke-linecap: round;
+      stroke-linejoin: round;
+      stroke-miterlimit: 10.00;
+    }
+    .svglite text {
+      white-space: pre;
+    }
+  ]]></style>
+</defs>
+<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/>
+<defs>
+  <clipPath id='cpMC4wMHw1MDQuMDB8MC4wMHw1MDQuMDA='>
+    <rect x='0.00' y='0.00' width='504.00' height='504.00' />
+  </clipPath>
+</defs>
+<g clip-path='url(#cpMC4wMHw1MDQuMDB8MC4wMHw1MDQuMDA=)'>
+<text x='266.40' y='485.28' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.00px' lengthAdjust='spacingAndGlyphs'>x</text>
+<text transform='translate(12.96,244.80) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.00px' lengthAdjust='spacingAndGlyphs'>y</text>
+<line x1='74.40' y1='430.56' x2='458.40' y2='430.56' style='stroke-width: 0.75;' />
+<line x1='74.40' y1='430.56' x2='74.40' y2='437.76' style='stroke-width: 0.75;' />
+<line x1='170.40' y1='430.56' x2='170.40' y2='437.76' style='stroke-width: 0.75;' />
+<line x1='266.40' y1='430.56' x2='266.40' y2='437.76' style='stroke-width: 0.75;' />
+<line x1='362.40' y1='430.56' x2='362.40' y2='437.76' style='stroke-width: 0.75;' />
+<line x1='458.40' y1='430.56' x2='458.40' y2='437.76' style='stroke-width: 0.75;' />
+<text x='74.40' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>1.0</text>
+<text x='170.40' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>1.5</text>
+<text x='266.40' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>2.0</text>
+<text x='362.40' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>2.5</text>
+<text x='458.40' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>3.0</text>
+<line x1='59.04' y1='416.80' x2='59.04' y2='72.80' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='416.80' x2='51.84' y2='416.80' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='330.80' x2='51.84' y2='330.80' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='244.80' x2='51.84' y2='244.80' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='158.80' x2='51.84' y2='158.80' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='72.80' x2='51.84' y2='72.80' style='stroke-width: 0.75;' />
+<text transform='translate(41.76,416.80) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>1.0</text>
+<text transform='translate(41.76,330.80) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>1.5</text>
+<text transform='translate(41.76,244.80) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>2.0</text>
+<text transform='translate(41.76,158.80) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>2.5</text>
+<text transform='translate(41.76,72.80) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>3.0</text>
+<polygon points='59.04,430.56 473.76,430.56 473.76,59.04 59.04,59.04 ' style='stroke-width: 0.75;' />
+</g>
+<defs>
+  <clipPath id='cpNTkuMDR8NDczLjc2fDU5LjA0fDQzMC41Ng=='>
+    <rect x='59.04' y='59.04' width='414.72' height='371.52' />
+  </clipPath>
+</defs>
+<g clip-path='url(#cpNTkuMDR8NDczLjc2fDU5LjA0fDQzMC41Ng==)'>
+<text x='74.40' y='419.97' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>a</text>
+<text x='458.40' y='75.97' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.00px' lengthAdjust='spacingAndGlyphs'>c</text>
+</g>
+</svg>
diff --git a/inst/tinytest/_tinysnapshot/text_single_character.svg b/inst/tinytest/_tinysnapshot/text_single_character.svg
new file mode 100644
index 00000000..2308d6b0
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/text_single_character.svg
@@ -0,0 +1,60 @@
+<?xml version='1.0' encoding='UTF-8' ?>
+<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' class='svglite' width='504.00pt' height='504.00pt' viewBox='0 0 504.00 504.00'>
+<defs>
+  <style type='text/css'><![CDATA[
+    .svglite line, .svglite polyline, .svglite polygon, .svglite path, .svglite rect, .svglite circle {
+      fill: none;
+      stroke: #000000;
+      stroke-linecap: round;
+      stroke-linejoin: round;
+      stroke-miterlimit: 10.00;
+    }
+    .svglite text {
+      white-space: pre;
+    }
+  ]]></style>
+</defs>
+<rect width='100%' height='100%' style='stroke: none; fill: #FFFFFF;'/>
+<defs>
+  <clipPath id='cpMC4wMHw1MDQuMDB8MC4wMHw1MDQuMDA='>
+    <rect x='0.00' y='0.00' width='504.00' height='504.00' />
+  </clipPath>
+</defs>
+<g clip-path='url(#cpMC4wMHw1MDQuMDB8MC4wMHw1MDQuMDA=)'>
+<text x='266.40' y='485.28' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.00px' lengthAdjust='spacingAndGlyphs'>x</text>
+<text transform='translate(12.96,244.80) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.00px' lengthAdjust='spacingAndGlyphs'>y</text>
+<line x1='74.40' y1='430.56' x2='458.40' y2='430.56' style='stroke-width: 0.75;' />
+<line x1='74.40' y1='430.56' x2='74.40' y2='437.76' style='stroke-width: 0.75;' />
+<line x1='170.40' y1='430.56' x2='170.40' y2='437.76' style='stroke-width: 0.75;' />
+<line x1='266.40' y1='430.56' x2='266.40' y2='437.76' style='stroke-width: 0.75;' />
+<line x1='362.40' y1='430.56' x2='362.40' y2='437.76' style='stroke-width: 0.75;' />
+<line x1='458.40' y1='430.56' x2='458.40' y2='437.76' style='stroke-width: 0.75;' />
+<text x='74.40' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>1.0</text>
+<text x='170.40' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>1.5</text>
+<text x='266.40' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>2.0</text>
+<text x='362.40' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>2.5</text>
+<text x='458.40' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>3.0</text>
+<line x1='59.04' y1='416.80' x2='59.04' y2='72.80' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='416.80' x2='51.84' y2='416.80' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='330.80' x2='51.84' y2='330.80' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='244.80' x2='51.84' y2='244.80' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='158.80' x2='51.84' y2='158.80' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='72.80' x2='51.84' y2='72.80' style='stroke-width: 0.75;' />
+<text transform='translate(41.76,416.80) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>1.0</text>
+<text transform='translate(41.76,330.80) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>1.5</text>
+<text transform='translate(41.76,244.80) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>2.0</text>
+<text transform='translate(41.76,158.80) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>2.5</text>
+<text transform='translate(41.76,72.80) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='16.68px' lengthAdjust='spacingAndGlyphs'>3.0</text>
+<polygon points='59.04,430.56 473.76,430.56 473.76,59.04 59.04,59.04 ' style='stroke-width: 0.75;' />
+</g>
+<defs>
+  <clipPath id='cpNTkuMDR8NDczLjc2fDU5LjA0fDQzMC41Ng=='>
+    <rect x='59.04' y='59.04' width='414.72' height='371.52' />
+  </clipPath>
+</defs>
+<g clip-path='url(#cpNTkuMDR8NDczLjc2fDU5LjA0fDQzMC41Ng==)'>
+<text x='74.40' y='419.97' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>a</text>
+<text x='266.40' y='247.97' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>a</text>
+<text x='458.40' y='75.97' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>a</text>
+</g>
+</svg>
diff --git a/inst/tinytest/test-text.R b/inst/tinytest/test-text.R
new file mode 100644
index 00000000..e49da13a
--- /dev/null
+++ b/inst/tinytest/test-text.R
@@ -0,0 +1,31 @@
+source("helpers.R")
+using("tinysnapshot")
+
+f = function() {
+  tinyplot(mpg ~ hp | factor(cyl),
+    data = mtcars,
+    facet = ~am,
+    type = type_text(
+      labels = row.names(mtcars),
+      font = 2,
+      adj = 0))
+}
+expect_snapshot_plot(f, label = "text_facets")
+
+
+f = function() {
+  x = c(1, 2, 3)
+  y = c(1, 2, 3)
+  z = c("a", NA, "c")
+  tinyplot(x, y, labels = z, type = "text")
+}
+expect_snapshot_plot(f, label = "text_missing_label")
+
+
+f = function() {
+  x = c(1, 2, 3)
+  y = c(1, 2, 3)
+  z = "a"
+  tinyplot(x, y, labels = z, type = "text")
+}
+expect_snapshot_plot(f, label = "text_single_character")
diff --git a/man/tinyplot.Rd b/man/tinyplot.Rd
index 0941375f..71bc75c1 100644
--- a/man/tinyplot.Rd
+++ b/man/tinyplot.Rd
@@ -224,6 +224,7 @@ type of plot desired.
 \item \code{"rect"} / \code{\link[=type_rect]{type_rect()}}: Draws rectangles; requires \code{xmin}, \code{xmax}, \code{ymin}, and \code{ymax}.
 \item \code{"ribbon"} / \code{\link[=type_ribbon]{type_ribbon()}}: Creates a filled area between \code{ymin} and \code{ymax}.
 \item \code{"segments"} / \code{\link[=type_segments]{type_segments()}}: Draws line segments between pairs of points.
+\item \code{"text"} / \code{\link[=type_text]{type_text()}}: Add text annotations.
 }
 \item Visualizations:
 \itemize{
@@ -677,11 +678,12 @@ tinyplot(
 
 # It's possible to customize the look of your plots by setting graphical
 # parameters (e.g., via `(t)par`)... But a more convenient way is to just use
-# built-in themes (see `?tinytheme`). 
+# built-in themes (see `?tinytheme`).
 
 tinytheme("clean2")
 tinyplot(
-  Temp ~ Day | Month, data = aq,
+  Temp ~ Day | Month,
+  data = aq,
   type = "b",
   alpha = 0.5,
   main = "Daily temperatures by month",
diff --git a/man/type_text.Rd b/man/type_text.Rd
new file mode 100644
index 00000000..8e4291e3
--- /dev/null
+++ b/man/type_text.Rd
@@ -0,0 +1,57 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/type_text.R
+\name{type_text}
+\alias{type_text}
+\title{Text annotations plot type}
+\usage{
+type_text(
+  labels,
+  adj = NULL,
+  pos = NULL,
+  offset = 0.5,
+  vfont = NULL,
+  font = NULL
+)
+}
+\arguments{
+\item{labels}{Character vector of the same length as the number of x,y coordinates.}
+
+\item{adj}{one or two values in \eqn{[0, 1]} which specify the x
+    (and optionally y) adjustment (\sQuote{justification}) of the
+    labels, with 0 for left/bottom, 1 for right/top, and 0.5 for
+    centered.  On most devices values outside \eqn{[0, 1]} will also
+    work.  See below.}
+
+\item{pos}{a position specifier for the text.  If specified this
+    overrides any \code{adj} value given.  Values of \code{1},
+    \code{2}, \code{3} and \code{4}, respectively indicate
+    positions below, to the left of, above and to the right of
+    the specified \code{(x,y)} coordinates.}
+
+\item{offset}{when \code{pos} is specified, this value controls the
+    distance (\sQuote{offset}) of the text label from the specified
+    coordinate in fractions of a character width.}
+
+\item{vfont}{\code{NULL} for the current font family, or a character
+    vector of length 2 for \code{\link[grDevices]{Hershey}} vector
+    fonts.  The first element of
+    the vector selects a typeface and the second element selects a
+    style.  Ignored if \code{labels} is an expression.}
+
+\item{font}{Font to be used, following \code{\link[graphics:par]{graphics::par()}}}
+}
+\description{
+Type function for adding text annotations to a plot. This function allows
+you to draw text at specified (x,y) coordinates. If no label is provided, the y-values
+will be used as the text labels.
+}
+\examples{
+
+tinyplot(mpg ~ hp | factor(cyl),
+  data = mtcars,
+  type = type_text(
+    labels = row.names(mtcars),
+    font = 2,
+    adj = 0))
+
+}
diff --git a/vignettes/types.qmd b/vignettes/types.qmd
index 78533423..c82d62cd 100644
--- a/vignettes/types.qmd
+++ b/vignettes/types.qmd
@@ -64,6 +64,7 @@ Shapes:
 - `"rect"` / `type_rect()`: Draws rectangles; requires `xmin`, `xmax`, `ymin`, and `ymax`.
 - `"ribbon"` / `type_ribbon()`: Creates a filled area between `ymin` and `ymax`.
 - `"segments"` / `type_segments()`: Draws line segments between pairs of points.
+- `"text"` / `type_text()`: Add text annotations to a plot.
 
 Visualizations:
 
