diff --git a/NEWS.md b/NEWS.md
index 3ee126e7..7e1cf144 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -10,13 +10,17 @@ where the formatting is also better._
 
 - `type_text()` gains `xpd` and `srt` arguments for controlling text clipping
   rotation, respectively. (#428 @grantmcdermott)
+- Add `xlevels` (in addition to `ylevels`) in `type_spineplot()` for spine plots
+  with categorical `x` variable. (#431 @zeileis)
 
 ### Bug fixes
 
 - Safer handling of pre-plot hooks. Resolves an issue affecting how `tinyplot`
   behaves inside loops, particularly for themed plots where only the final plot
   was being drawn in Quarto/RMarkdown contexts. Special thanks to @hadley and @cderv
-  for helping us to debug. (@vincentarelbundock #425)
+  for helping us to debug. (#425 @vincentarelbundock)
+- The `xlevels` argument of `type_barplot()` could not handle numeric indexes correctly.
+  (#431 @zeileis)
 
 ## 0.4.1
 
diff --git a/R/type_barplot.R b/R/type_barplot.R
index 944fc4ab..64c3ac47 100644
--- a/R/type_barplot.R
+++ b/R/type_barplot.R
@@ -19,8 +19,9 @@
 #'   or the mid-way in the third category, respectively.
 #' @param FUN a function to compute the summary statistic for `y` within each
 #'   group of `x` in case of using a two-sided formula `y ~ x` (default: mean).
-#' @param xlevels a character or numeric vector specifying in which order the
-#'   levels of the `x` variable should be plotted.
+#' @param xlevels a character or numeric vector specifying the ordering of the
+#'   levels of the `x` variable (if character) or the corresponding indexes
+#'   (if numeric) for the plot.
 #' @param xaxlabels a character vector with the axis labels for the `x` variable,
 #'   defaulting to the levels of `x`.
 #' @param drop.zeros logical. Should bars with zero height be dropped? If set
@@ -34,6 +35,10 @@
 #' tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE)
 #' tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE, fill = 0.2)
 #' 
+#' # Reorder x variable categories either by their character levels or numeric indexes
+#' tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = c("8", "6", "4"))
+#' tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = 3:1)
+#' 
 #' # Note: Above we used automatic argument passing for `beside`. But this
 #' # wouldn't work for `width`, since it would conflict with the top-level
 #' # `tinyplot(..., width = <width>)` argument. It's safer to pass these args
@@ -88,7 +93,11 @@ data_barplot = function(width = 5/6, beside = FALSE, center = FALSE, FUN = NULL,
           if (is.null(FUN)) FUN = function(x, ...) mean(x, ..., na.rm = TRUE)
         }
         if (!is.factor(datapoints$x)) datapoints$x = factor(datapoints$x)
-        if (!is.null(xlevels)) datapoints$x = factor(datapoints$x, levels = if(is.numeric(xlevels)) levels(x)[xlevels] else xlevels)
+        if (!is.null(xlevels)) {
+          xlevels = if(is.numeric(xlevels)) levels(datapoints$x)[xlevels] else xlevels
+          if (any(is.na(xlevels)) || !all(xlevels %in% levels(datapoints$x))) warning("not all 'xlevels' correspond to levels of 'x'")
+          datapoints$x = factor(datapoints$x, levels = xlevels)
+        }
         if (!is.null(xaxlabels)) levels(datapoints$x) <- xaxlabels
         datapoints = aggregate(datapoints[, "y", drop = FALSE], datapoints[, c("x", "by", "facet")], FUN = FUN, drop = FALSE)
         datapoints$y[is.na(datapoints$y)] = 0 #FIXME: always?#
diff --git a/R/type_spineplot.R b/R/type_spineplot.R
index 08cb8b37..13dcd537 100644
--- a/R/type_spineplot.R
+++ b/R/type_spineplot.R
@@ -4,6 +4,9 @@
 #'   are modified versions of histograms or mosaic plots, and particularly
 #'   useful for visualizing factor variables. Note that [`tinyplot`] defaults
 #'   to `type_spineplot()` if `y` is a factor variable.
+#' @param xlevels,ylevels a character or numeric vector specifying the ordering of the
+#'   levels of the `x` and `y` variables (if character) or the corresponding indexes
+#'   (if numeric) for the plot.
 #' @inheritParams graphics::spineplot
 #' @examples
 #' # "spineplot" type convenience string
@@ -47,7 +50,13 @@
 #'   type = type_spineplot(weights = ttnc$Freq),
 #'   palette = "Dark 2", facet.args = list(nrow = 1), axes = "t"
 #' )
-#' 
+#'
+#' # Reorder x and y variable categories either by their character levels or numeric indexes
+#' tinyplot(
+#'   Survived ~ Sex, facet = ~ Class, data = ttnc,
+#'   type = type_spineplot(weights = ttnc$Freq, xlevels = c("Female", "Male"), ylevels = 2:1)
+#' )
+#'
 #' # Note: It's possible to use "by" on its own (without faceting), but the
 #' # overlaid result isn't great. We will likely overhaul this behaviour in a
 #' # future version of tinyplot...
@@ -56,10 +65,10 @@
 #' )
 #' 
 #' @export
-type_spineplot = function(breaks = NULL, tol.ylab = 0.05, off = NULL, ylevels = NULL, col = NULL, xaxlabels = NULL, yaxlabels = NULL, weights = NULL) {
+type_spineplot = function(breaks = NULL, tol.ylab = 0.05, off = NULL, xlevels = NULL, ylevels = NULL, col = NULL, xaxlabels = NULL, yaxlabels = NULL, weights = NULL) {
   col = col
   out = list(
-    data = data_spineplot(off = off, breaks = breaks, ylevels = ylevels, xaxlabels = xaxlabels, yaxlabels = yaxlabels, weights = weights),
+    data = data_spineplot(off = off, breaks = breaks, xlevels = xlevels, ylevels = ylevels, xaxlabels = xaxlabels, yaxlabels = yaxlabels, weights = weights),
     draw = draw_spineplot(tol.ylab = tol.ylab, off = off, col = col, xaxlabels = xaxlabels, yaxlabels = yaxlabels),
     name = "spineplot"
   )
@@ -68,7 +77,7 @@ type_spineplot = function(breaks = NULL, tol.ylab = 0.05, off = NULL, ylevels =
 }
 
 #' @importFrom grDevices nclass.Sturges
-data_spineplot = function(off = NULL, breaks = NULL, ylevels = ylevels, xaxlabels = NULL, yaxlabels = NULL, weights = NULL) {
+data_spineplot = function(off = NULL, breaks = NULL, xlevels = xlevels, ylevels = ylevels, xaxlabels = NULL, yaxlabels = NULL, weights = NULL) {
     fun = function(
       datapoints,
       by = NULL, col = NULL, bg = NULL, palette = NULL,
@@ -114,7 +123,6 @@ data_spineplot = function(off = NULL, breaks = NULL, ylevels = ylevels, xaxlabel
 
         ## process y variable
         if (!is.factor(datapoints$y)) datapoints$y = factor(datapoints$y)
-        # if (!is.null(ylevels)) datapoints$y = factor(datapoints$y, levels = if(is.numeric(ylevels)) levels(datapoints$y)[ylevels] else ylevels)
         if (is.null(ylim)) ylim = c(0, 1)
 
         ## adjust facet margins
@@ -125,12 +133,20 @@ data_spineplot = function(off = NULL, breaks = NULL, ylevels = ylevels, xaxlabel
         x_by = identical(datapoints$x, datapoints$by)
         y_by = identical(datapoints$y, datapoints$by)
         
+        x.categorical = is.factor(datapoints$x)
+        if (!is.null(xlevels) && x.categorical) {
+          xlevels = if(is.numeric(xlevels)) levels(datapoints$x)[xlevels] else xlevels
+          if (any(is.na(xlevels)) || !all(xlevels %in% levels(datapoints$x))) warning("not all 'xlevels' correspond to levels of 'x'")
+          datapoints$x = factor(datapoints$x, levels = xlevels)
+          if (x_by) datapoints$by = datapoints$x
+        }
         if (!is.null(ylevels)) {
-          datapoints$y = factor(datapoints$y, levels = if(is.numeric(ylevels)) levels(datapoints$y)[ylevels] else ylevels)
+          ylevels = if(is.numeric(ylevels)) levels(datapoints$y)[ylevels] else ylevels
+          if (any(is.na(ylevels)) || !all(ylevels %in% levels(datapoints$y))) warning("not all 'ylevels' correspond to levels of 'y'")
+          datapoints$y = factor(datapoints$y, levels = ylevels)
           if (y_by) datapoints$by = datapoints$y
         }
         
-        x.categorical = is.factor(datapoints$x)
         x = datapoints$x
         y = datapoints$y
         
diff --git a/inst/tinytest/_tinysnapshot/barplot_xlevels_issue430.svg b/inst/tinytest/_tinysnapshot/barplot_xlevels_issue430.svg
new file mode 100644
index 00000000..26076ab1
--- /dev/null
+++ b/inst/tinytest/_tinysnapshot/barplot_xlevels_issue430.svg
@@ -0,0 +1,63 @@
+<?xml version='1.0' encoding='UTF-8' ?>
+<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='504.00pt' height='504.00pt' viewBox='0 0 504.00 504.00'>
+<g class='svglite'>
+<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;
+    }
+    .svglite g.glyphgroup path {
+      fill: inherit;
+      stroke: none;
+    }
+  ]]></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='14.67px' lengthAdjust='spacingAndGlyphs'>cyl</text>
+<text transform='translate(12.96,244.80) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='32.02px' lengthAdjust='spacingAndGlyphs'>Count</text>
+<text x='130.87' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>8</text>
+<text x='266.40' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>6</text>
+<text x='401.93' y='456.48' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text>
+<line x1='59.04' y1='430.56' x2='59.04' y2='66.32' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='430.56' x2='51.84' y2='430.56' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='378.53' x2='51.84' y2='378.53' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='326.49' x2='51.84' y2='326.49' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='274.46' x2='51.84' y2='274.46' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='222.43' x2='51.84' y2='222.43' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='170.39' x2='51.84' y2='170.39' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='118.36' x2='51.84' y2='118.36' style='stroke-width: 0.75;' />
+<line x1='59.04' y1='66.32' x2='51.84' y2='66.32' style='stroke-width: 0.75;' />
+<text transform='translate(41.76,430.56) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>0</text>
+<text transform='translate(41.76,378.53) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>2</text>
+<text transform='translate(41.76,326.49) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>4</text>
+<text transform='translate(41.76,274.46) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>6</text>
+<text transform='translate(41.76,222.43) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='6.67px' lengthAdjust='spacingAndGlyphs'>8</text>
+<text transform='translate(41.76,170.39) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='13.34px' lengthAdjust='spacingAndGlyphs'>10</text>
+<text transform='translate(41.76,118.36) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='13.34px' lengthAdjust='spacingAndGlyphs'>12</text>
+<text transform='translate(41.76,66.32) rotate(-90)' text-anchor='middle' style='font-size: 12.00px; font-family: "Liberation Sans";' textLength='13.34px' lengthAdjust='spacingAndGlyphs'>14</text>
+</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==)'>
+<rect x='74.40' y='66.32' width='112.94' height='364.24' style='stroke-width: 0.75; fill: #BEBEBE;' />
+<rect x='209.93' y='248.44' width='112.94' height='182.12' style='stroke-width: 0.75; fill: #BEBEBE;' />
+<rect x='345.46' y='144.38' width='112.94' height='286.18' style='stroke-width: 0.75; fill: #BEBEBE;' />
+</g>
+</g>
+</svg>
diff --git a/inst/tinytest/test-type_barplot.R b/inst/tinytest/test-type_barplot.R
index f75580af..9fce4274 100644
--- a/inst/tinytest/test-type_barplot.R
+++ b/inst/tinytest/test-type_barplot.R
@@ -39,4 +39,9 @@ f = function() {
   tinyplot(Freq ~ Sex | Survived, facet = ~ Class, data = as.data.frame(Titanic),
            type = "barplot", flip = TRUE, fill = 0.6, beside = TRUE)
 }
-expect_snapshot_plot(f, label = "barplot_flip_fancy")
\ No newline at end of file
+expect_snapshot_plot(f, label = "barplot_flip_fancy")
+
+f = function() {
+  tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = 3:1)
+}
+expect_snapshot_plot(f, label = "barplot_xlevels_issue430")
diff --git a/man/type_barplot.Rd b/man/type_barplot.Rd
index 35972f0b..35266a78 100644
--- a/man/type_barplot.Rd
+++ b/man/type_barplot.Rd
@@ -32,8 +32,9 @@ or the mid-way in the third category, respectively.}
 \item{FUN}{a function to compute the summary statistic for \code{y} within each
 group of \code{x} in case of using a two-sided formula \code{y ~ x} (default: mean).}
 
-\item{xlevels}{a character or numeric vector specifying in which order the
-levels of the \code{x} variable should be plotted.}
+\item{xlevels}{a character or numeric vector specifying the ordering of the
+levels of the \code{x} variable (if character) or the corresponding indexes
+(if numeric) for the plot.}
 
 \item{xaxlabels}{a character vector with the axis labels for the \code{x} variable,
 defaulting to the levels of \code{x}.}
@@ -56,6 +57,10 @@ tinyplot(~ cyl | vs, data = mtcars, type = "barplot")
 tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE)
 tinyplot(~ cyl | vs, data = mtcars, type = "barplot", beside = TRUE, fill = 0.2)
 
+# Reorder x variable categories either by their character levels or numeric indexes
+tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = c("8", "6", "4"))
+tinyplot(~ cyl, data = mtcars, type = "barplot", xlevels = 3:1)
+
 # Note: Above we used automatic argument passing for `beside`. But this
 # wouldn't work for `width`, since it would conflict with the top-level
 # `tinyplot(..., width = <width>)` argument. It's safer to pass these args
diff --git a/man/type_spineplot.Rd b/man/type_spineplot.Rd
index c9a27743..ce0a3903 100644
--- a/man/type_spineplot.Rd
+++ b/man/type_spineplot.Rd
@@ -8,6 +8,7 @@ type_spineplot(
   breaks = NULL,
   tol.ylab = 0.05,
   off = NULL,
+  xlevels = NULL,
   ylevels = NULL,
   col = NULL,
   xaxlabels = NULL,
@@ -27,8 +28,9 @@ type_spineplot(
 \item{off}{vertical offset between the bars (in per cent). It is fixed to
     \code{0} for spinograms and defaults to \code{2} for spine plots.}
 
-\item{ylevels}{a character or numeric vector specifying in which order
-    the levels of the dependent variable should be plotted.}
+\item{xlevels, ylevels}{a character or numeric vector specifying the ordering of the
+levels of the \code{x} and \code{y} variables (if character) or the corresponding indexes
+(if numeric) for the plot.}
 
 \item{col}{a vector of fill colors of the same length as \code{levels(y)}.
     The default is to call \code{\link{gray.colors}}.}
@@ -92,6 +94,12 @@ tinyplot(
   palette = "Dark 2", facet.args = list(nrow = 1), axes = "t"
 )
 
+# Reorder x and y variable categories either by their character levels or numeric indexes
+tinyplot(
+  Survived ~ Sex, facet = ~ Class, data = ttnc,
+  type = type_spineplot(weights = ttnc$Freq, xlevels = c("Female", "Male"), ylevels = 2:1)
+)
+
 # Note: It's possible to use "by" on its own (without faceting), but the
 # overlaid result isn't great. We will likely overhaul this behaviour in a
 # future version of tinyplot...
