One general approach is to use grobs (Grid objects) and use a rectGrob object to be displayed under the axis text.  I'll demonstrate with an example shown here.
library(ggplot2)
set.seed(8675309)
df <- data.frame(
  x=paste0('Test', 1:10),
  y=rnorm(10, 10)
)
ggplot(df, aes(x,y)) +
  geom_col(color='black', fill='gray', alpha=0.8) +
  scale_y_continuous(expand=expansion(mult=c(0,0.05))) +
  coord_cartesian(clip='off') +
  labs(x=NULL) +
  theme_classic()

Display the Boxes
To create the grob, you can use the grid package and rectGrob.  Since we want to draw many boxes, we can supply a vector for x (to draw one at x positions 1 through 10), and then supply the fill colors via a vector sent to fill.  Note that when using grobs, you can supply the various parameters through the gp argument inside of gpar().  Unlike a ggplot geom, the grobs are not matched to a data frame, so you'll have to manually specify the way colors/sizes are mapped via vectors as I've done here.
library(grid)
muh_grob <- grid::rectGrob(
  x=1:10, y=0, gp=gpar(
    color='black', fill=rainbow(10), alpha=0.2))
To use the grob, you can use annotation_custom(), where you need to specify the min and max values of y and x.  You'll have to likely mess around with the numbers to get things to look right.  Note the values are in npc, so 0 is left and 1 is all the way on the right in x axis here (discrete values).  It's also very important that you include coord_*(clip="off").  It can be any of the coord_ functions, but you need clipping off or you will not be able to see the grob.  I've also applied a margin to the top of the x axis text to move it downward a bit and make room for the box around it.
ggplot(df, aes(x,y)) +
  geom_col(color='black', fill='gray', alpha=0.8) +
  scale_y_continuous(expand=expansion(mult=c(0,0.05))) +
  coord_cartesian(clip='off') +
  labs(x=NULL) +
  theme_classic() +
  theme(
    axis.text.x = element_text(margin=margin(t=10)),
  ) +
  annotation_custom(
    grob=muh_grob, xmin = 0, xmax = 1, ymin = -0.5, ymax=0.1
  )

Multiple Facets
OP shared a plot with two facets that contained these boxes... so how to do that?  Well, it's not quite as straightforward to do, since annotation_custom() is applied the same way to each facet.  Each facet shares the same values of x and y, so if you specify a grob is from xmin=0 and xmax=0.5, this will apply your grob to the left side of each facet.
To get around this, there is a very nice adjustment to the method provided in another answer here, represented below:
library(gridExtra)
annotation_custom2 <- 
  function (grob, xmin = -Inf, xmax = Inf, ymin = -Inf, ymax = Inf, data) 
  {
    layer(data = data, stat = StatIdentity, position = PositionIdentity, 
          geom = ggplot2:::GeomCustomAnn,
          inherit.aes = TRUE, params = list(grob = grob, 
                                            xmin = xmin, xmax = xmax, 
                                            ymin = ymin, ymax = ymax))
  }
I'll then make a left and right grob, and apply it using that function, which allows us to specify a data argument to annotation_custom2() and place the grob on one facet.
muh_left_grob <- rectGrob(
  x=1:5, y=0, gp=gpar(color='black', fill='red', alpha=seq(0.7, 0.1, length.out = 5)))
muh_right_grob <- rectGrob(
  x=1:5, y=0, gp=gpar(color='black', fill='blue', alpha=seq(0.7, 0.1, length.out = 5)))
ggplot(df, aes(x,y)) +
  geom_col(color='black', fill='gray', alpha=0.8) +
  scale_y_continuous(expand=expansion(mult=c(0,0.05))) +
  coord_cartesian(clip='off') +
  labs(x=NULL) +
  theme_classic() +
  theme(axis.text.x = element_text(margin=margin(t=10))) +
  facet_wrap(~my_facet, scales='free_x') +
  annotation_custom2(
    data=subset(df, my_facet=='A Facet'), grob=muh_left_grob,
    xmin=0, xmax=1, ymin=-0.5, ymax=0.1) +
  annotation_custom2(
    data=subset(df, my_facet=='Another Facet'), grob=muh_right_grob,
    xmin=0, xmax=1, ymin=-0.5, ymax=0.1)
