diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown
--- a/CHANGELOG.markdown
+++ b/CHANGELOG.markdown
@@ -1,3 +1,6 @@
+## 0.1.9 [2023.08.06]
+* Support building with `template-haskell-2.21.*` (GHC 9.8).
+
 ## 0.1.8 [2023.02.22]
 * Avoid some dodgy uses of `unsafeCoerce#` from `Any` (a lifted type) to
   `MutableByteArray# s` (an unlifted type) in the internals of the library.
diff --git a/src/Data/Struct/TH.hs b/src/Data/Struct/TH.hs
--- a/src/Data/Struct/TH.hs
+++ b/src/Data/Struct/TH.hs
@@ -6,6 +6,8 @@
 import           Control.Monad (when, zipWithM)
 import           Control.Monad.Primitive (PrimMonad, PrimState)
 import           Data.Either (partitionEithers)
+import qualified Data.List.NonEmpty as NE
+import           Data.List.NonEmpty (NonEmpty(..))
 import           Data.Primitive
 import           Data.Struct
 import           Data.Struct.Internal (Dict(Dict), initializeUnboxedField, st)
@@ -21,7 +23,7 @@
 data StructRep = StructRep
   { srState       :: Name
   , srName        :: Name
-  , srTyVars      :: [TyVarBndrUnit]
+  , srTyVars      :: [TyVarBndrVis]
 #if MIN_VERSION_template_haskell(2,12,0)
   , srDerived     :: [DerivClause]
 #else
@@ -99,7 +101,7 @@
 
 -- A struct type's final type variable should be suitable for
 -- use as the ('PrimState' m) argument.
-validateStateType :: [TyVarBndrUnit] -> Q Name
+validateStateType :: [TyVarBndrVis] -> Q Name
 validateStateType xs =
   do when (null xs) (fail "state type expected but no type variables found")
      elimTV return validateKindedTV (last xs)
@@ -191,11 +193,11 @@
 repType :: StructRep -> TypeQ
 repType rep = repTypeHelper (srName rep) (srTyVars rep)
 
-repTypeHelper :: Name -> [TyVarBndrUnit] -> TypeQ
+repTypeHelper :: Name -> [TyVarBndrVis] -> TypeQ
 repTypeHelper c vs = foldl appT (conT c) (tyVarBndrT <$> vs)
 
 -- Construct a 'TypeQ' from a 'TyVarBndr'
-tyVarBndrT :: TyVarBndrUnit -> TypeQ
+tyVarBndrT :: TyVarBndrVis -> TypeQ
 tyVarBndrT = elimTV varT (sigT . varT)
 
 generateStructInstance :: StructRep -> DecsQ
@@ -225,7 +227,7 @@
 generateNew :: StructRep -> DecsQ
 generateNew rep =
   do this <- newName "this"
-     let ms = groupBy isNeighbor (srMembers rep)
+     let ms = NE.groupBy isNeighbor (srMembers rep)
 
          addName m = do n <- newName (nameBase (memberName m))
                         return (n,m)
@@ -245,26 +247,26 @@
 
      sequence
        [ sigD name (newStructType rep)
-       , funD name [ clause (varP . fst <$> concat msWithArgs)
+       , funD name [ clause (varP . fst <$> concatMap NE.toList msWithArgs)
                             (normalB [| st $body |] ) [] ]
        ]
 
 
-assignN :: ExpQ -> Int -> [(Name,Member)] -> ExpQ
+assignN :: ExpQ -> Int -> NonEmpty (Name,Member) -> ExpQ
 
-assignN this _ [(arg,Member BoxedField n _)] =
+assignN this _ ((arg,Member BoxedField n _) :| []) =
   [| setField $(varE n) $this $(varE arg) |]
 
-assignN this _ [(arg,Member Slot n _)] =
+assignN this _ ((arg,Member Slot n _) :| []) =
   [| set      $(varE n) $this $(varE arg)|]
 
 assignN this i us =
-  do let n = length us
+  do let n = NE.length us
      mba <- newName "mba"
-     let arg0 = fst (head us)
+     let arg0 = fst (NE.head us)
      doE $ bindS (varP mba) [| initializeUnboxedField i n (sizeOf $(varE arg0)) $this |]
          : [ noBindS [| writeByteArray $(varE mba) j $(varE arg) |]
-           | (j,(arg,_)) <- zip [0 :: Int ..] us ]
+           | (j,(arg,_)) <- zip [0 :: Int ..] (NE.toList us) ]
 
 -- | The type of the struct initializer is complicated enough to
 -- pull it out here.
@@ -370,6 +372,10 @@
 occurs n (ForallT _ _ t) = occurs n t -- all names are fresh in quoted code, see below
 occurs n (SigT t _) = occurs n t
 occurs _ _ = False
+
+#if !MIN_VERSION_template_haskell(2,21,0) && !MIN_VERSION_th_abstraction(0,6,0)
+type TyVarBndrVis = TyVarBndrUnit
+#endif
 
 -- Prelude Language.Haskell.TH> runQ (stringE . show =<< [t| forall a. a -> (forall a. a) |])
 -- LitE (StringL "ForallT [PlainTV a_0] [] (AppT (AppT ArrowT (VarT a_0)) (ForallT [PlainTV a_1] [] (VarT a_1)))")
diff --git a/structs.cabal b/structs.cabal
--- a/structs.cabal
+++ b/structs.cabal
@@ -1,6 +1,6 @@
 name:          structs
 category:      Data
-version:       0.1.8
+version:       0.1.9
 license:       BSD3
 cabal-version: 1.22
 license-file:  LICENSE
@@ -18,7 +18,9 @@
              , GHC == 8.8.4
              , GHC == 8.10.7
              , GHC == 9.0.2
-             , GHC == 9.2.2
+             , GHC == 9.2.8
+             , GHC == 9.4.5
+             , GHC == 9.6.2
 synopsis:      Strict GC'd imperative object-oriented programming with cheap pointers.
 description:
   This project is an experiment with a small GC'd strict mutable imperative universe with cheap pointers inside of the GHC runtime system.
@@ -36,8 +38,10 @@
   build-depends:
     base >= 4.9 && < 5,
     deepseq,
-    template-haskell >= 2.11 && < 2.20,
-    th-abstraction >= 0.4 && < 0.5,
+    template-haskell >= 2.11 && < 2.22,
+    -- TODO: Eventually, we should bump the lower version bounds to >=0.6 so that
+    -- we can remove some CPP in Data.Struct.TH.
+    th-abstraction >= 0.4 && < 0.7,
     ghc-prim,
     primitive
 
