diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,10 @@
+### 0.0.2.0 (2023-06-26)
+
+Add module `OsPath.Text.UTF8`, with the same API but using
+the new `OsPath` type from the `filepath` library.
+
+Minimum GHC version is raised to 9.6.
+
 ### 0.0.1.12 (2023-06-26)
 
 Raise language version to GHC2021
diff --git a/path-text-utf8.cabal b/path-text-utf8.cabal
--- a/path-text-utf8.cabal
+++ b/path-text-utf8.cabal
@@ -1,7 +1,7 @@
 cabal-version: 3.0
 
 name: path-text-utf8
-version: 0.0.1.12
+version: 0.0.2.0
 category: Filesystem, Text
 
 synopsis: Read and write UTF-8 text files
@@ -36,10 +36,13 @@
 
     exposed-modules:
         Path.Text.UTF8
+        OsPath.Text.UTF8
 
     build-depends:
-      , base ^>= 4.16 || ^>= 4.17 || ^>= 4.18
+      , base ^>= 4.18
       , bytestring ^>= 0.10.12 || ^>= 0.11
+      , file-io ^>= 0.1.0
+      , filepath ^>= 1.4.100
       , path ^>= 0.9.2
       , safe-exceptions ^>= 0.1.7
       , text ^>= 1.2.5 || ^>= 2.0
diff --git a/readme.md b/readme.md
new file mode 100644
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,7 @@
+This package provides the same API in two forms:
+
+* `OsPath.Text.UTF8` based on the [filepath] library
+* `Path.Text.UTF8` based on the [path] library
+
+  [filepath]: https://hackage.haskell.org/package/filepath
+  [path]: https://hackage.haskell.org/package/path
diff --git a/src/OsPath/Text/UTF8.hs b/src/OsPath/Text/UTF8.hs
new file mode 100644
--- /dev/null
+++ b/src/OsPath/Text/UTF8.hs
@@ -0,0 +1,78 @@
+-- | Read and write UTF-8 text files.
+module OsPath.Text.UTF8
+  ( -- * Reading
+    readFile,
+    tryReadFile,
+    ReadError (..),
+
+    -- * Writing
+    writeFile,
+    tryWriteFile,
+    WriteError,
+
+    -- * Re-exports
+    IOError,
+    UnicodeException (DecodeError),
+    encodeUtf,
+  )
+where
+
+import Control.Exception.Safe qualified as Exception
+import Data.Either (Either (..))
+import Data.Functor ((<$>))
+import Data.Text (Text)
+import Data.Text.Encoding qualified as TextEncoding
+import Data.Text.Lazy qualified as Text.Lazy
+import Data.Text.Lazy.Encoding qualified as TextEncoding.Lazy
+import Data.Text.Encoding.Error (UnicodeException (..))
+import System.OsPath (OsPath, encodeUtf)
+import System.File.OsPath qualified as OsPath.IO
+import System.IO (IO)
+import System.IO.Error (IOError)
+
+data ReadError
+  = ReadErrorIO IOError
+  | ReadErrorDecode UnicodeException
+
+type WriteError = IOError
+
+-- | Read the contents of a UTF-8 encoded text file
+--
+-- May throw 'IOError' or 'UnicodeException'. To handle these errors in 'Either'
+-- instead, use 'tryReadFile'.
+readFile :: OsPath -> IO Text
+readFile path =
+  f <$> OsPath.IO.readFile' path
+  where
+    f bs = let !text = TextEncoding.decodeUtf8 bs in text
+
+-- | Read the contents of a UTF-8 encoded text file
+--
+-- Any 'IOError' or 'UnicodeException' that occurs is caught and returned as a
+-- 'ReadError' on the 'Left' side of the 'Either'. To throw these exceptions
+-- instead, use 'readFile'.
+tryReadFile :: OsPath -> IO (Either ReadError Text)
+tryReadFile path =
+  f <$> Exception.tryIO (OsPath.IO.readFile' path)
+  where
+    f (Left e) = Left (ReadErrorIO e)
+    f (Right bs) = first ReadErrorDecode (TextEncoding.decodeUtf8' bs)
+
+-- | Write text to a file in a UTF-8 encoding
+--
+-- May throw 'IOError'. To handle this error in 'Either' instead, use
+-- 'tryWriteFile'.
+writeFile :: OsPath -> Text -> IO ()
+writeFile path text =
+  OsPath.IO.writeFile path (TextEncoding.Lazy.encodeUtf8 (Text.Lazy.fromStrict text))
+
+-- | Write text to a file in a UTF-8 encoding
+--
+-- Any 'IOError' that occurs is caught and returned on the 'Left' side of the
+-- 'Either'. To throw the exception instead, use 'writeFile'.
+tryWriteFile :: OsPath -> Text -> IO (Either WriteError ())
+tryWriteFile path text = Exception.tryIO (writeFile path text)
+
+first :: (a -> a') -> Either a b -> Either a' b
+first f (Left x) = Left (f x)
+first _ (Right x) = Right x
