diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index d80b4e794..248d5e131 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -138,6 +138,7 @@ dependencies {
     implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta02")
     implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
     implementation("androidx.core:core-ktx:1.7.0-alpha01")
+    implementation("androidx.core:core-splashscreen:1.0.0-alpha01")
     implementation("androidx.multidex:multidex:2.0.1")
     implementation("androidx.preference:preference-ktx:1.1.1")
     implementation("androidx.recyclerview:recyclerview:1.2.1")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 02f12e32a..898f8b6bc 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -38,7 +38,7 @@
         <activity
             android:name=".ui.main.MainActivity"
             android:launchMode="singleTop"
-            android:theme="@style/Theme.Splash">
+            android:theme="@style/Theme.Tachiyomi.SplashScreen">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt
index 43a39dc85..83c5c22e0 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/SourceController.kt
@@ -35,7 +35,9 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
 import eu.kanade.tachiyomi.ui.browse.source.index.IndexController
 import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
 import eu.kanade.tachiyomi.ui.category.sources.ChangeSourceCategoriesDialog
+import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.util.system.toast
+import eu.kanade.tachiyomi.util.view.onAnimationsFinished
 import exh.ui.smartsearch.SmartSearchController
 import kotlinx.parcelize.Parcelize
 import uy.kohesive.injekt.Injekt
@@ -102,6 +104,9 @@ class SourceController(bundle: Bundle? = null) :
         // Create recycler and set adapter.
         binding.recycler.layoutManager = LinearLayoutManager(view.context)
         binding.recycler.adapter = adapter
+        binding.recycler.onAnimationsFinished {
+            (activity as? MainActivity)?.ready = true
+        }
         adapter?.fastScroller = binding.fastScroller
 
         requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt
index a9dff00e9..434843eab 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt
@@ -20,9 +20,11 @@ import eu.kanade.tachiyomi.databinding.LibraryCategoryBinding
 import eu.kanade.tachiyomi.ui.category.CategoryAdapter
 import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
 import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
+import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.util.lang.plusAssign
 import eu.kanade.tachiyomi.util.system.toast
 import eu.kanade.tachiyomi.util.view.inflate
+import eu.kanade.tachiyomi.util.view.onAnimationsFinished
 import eu.kanade.tachiyomi.widget.AutofitRecyclerView
 import exh.ui.LoadingHandle
 import kotlinx.coroutines.CoroutineScope
@@ -128,6 +130,10 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
             }
             .launchIn(scope)
 
+        recycler.onAnimationsFinished {
+            (controller.activity as? MainActivity)?.ready = true
+        }
+
         // Double the distance required to trigger sync
         binding.swipeRefresh.setDistanceToTriggerSync((2 * 64 * resources.displayMetrics.density).toInt())
         binding.swipeRefresh.refreshes()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
index 7a116c82b..e7ee34673 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
@@ -316,6 +316,7 @@ class LibraryController(
             binding.emptyView.hide()
         } else {
             binding.emptyView.show(R.string.information_empty_library)
+            (activity as? MainActivity)?.ready = true
         }
 
         // Get the current active category.
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
index 739a9d8b9..84143c3cc 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
@@ -1,7 +1,10 @@
 package eu.kanade.tachiyomi.ui.main
 
+import android.animation.ValueAnimator
 import android.app.SearchManager
 import android.content.Intent
+import android.graphics.Color
+import android.os.Build
 import android.os.Bundle
 import android.os.Looper
 import android.view.Gravity
@@ -10,11 +13,17 @@ import android.view.View
 import android.view.ViewGroup
 import android.widget.Toast
 import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.animation.doOnEnd
+import androidx.core.splashscreen.SplashScreen
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
 import androidx.core.view.ViewCompat
 import androidx.core.view.WindowCompat
 import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
 import androidx.core.view.isVisible
 import androidx.core.view.updateLayoutParams
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator
+import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
 import androidx.lifecycle.lifecycleScope
 import androidx.preference.PreferenceDialogController
 import com.bluelinelabs.conductor.Conductor
@@ -49,6 +58,7 @@ import eu.kanade.tachiyomi.ui.recent.history.HistoryController
 import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.lang.launchUI
+import eu.kanade.tachiyomi.util.system.dpToPx
 import eu.kanade.tachiyomi.util.system.toast
 import eu.kanade.tachiyomi.util.view.setNavigationBarTransparentCompat
 import exh.EXHMigrations
@@ -87,6 +97,9 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
 
     private var fixedViewsToBottom = mutableMapOf<View, AppBarLayout.OnOffsetChangedListener>()
 
+    // To be checked by splash screen. If true then splash screen will be removed.
+    var ready = false
+
     // SY -->
     // Idle-until-urgent
     private var firstPaint = false
@@ -107,6 +120,9 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
     // SY <--
 
     override fun onCreate(savedInstanceState: Bundle?) {
+        // Prevent splash screen showing up on configuration changes
+        val splashScreen = if (savedInstanceState == null) installSplashScreen() else null
+
         super.onCreate(savedInstanceState)
 
         val didMigration = if (savedInstanceState == null) EXHMigrations.upgrade(preferences) else false
@@ -140,13 +156,12 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
             }
         }
 
-        // Make sure navigation bar is on bottom before we modify it
-        ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
-            if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) {
-                window.setNavigationBarTransparentCompat(this)
-            }
-            insets
+        val startTime = System.currentTimeMillis()
+        splashScreen?.setKeepVisibleCondition {
+            val elapsed = System.currentTimeMillis() - startTime
+            elapsed <= SPLASH_MIN_DURATION || (!ready && elapsed <= SPLASH_MAX_DURATION)
         }
+        setSplashScreenExitAnimation(splashScreen)
 
         tabAnimator = ViewHeightAnimator(binding.tabs, 0L)
 
@@ -308,6 +323,79 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
         // SY <--
     }
 
+    /**
+     * Sets custom splash screen exit animation on devices prior to Android 12.
+     *
+     * When custom animation is used, status and navigation bar color will be set to transparent and will be restored
+     * after the animation is finished.
+     */
+    private fun setSplashScreenExitAnimation(splashScreen: SplashScreen?) {
+        val setNavbarScrim = {
+            // Make sure navigation bar is on bottom before we modify it
+            ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
+                if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) {
+                    window.setNavigationBarTransparentCompat(this@MainActivity)
+                }
+                insets
+            }
+            ViewCompat.requestApplyInsets(binding.root)
+        }
+
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+            val oldStatusColor = window.statusBarColor
+            val oldNavigationColor = window.navigationBarColor
+            window.statusBarColor = Color.TRANSPARENT
+            window.navigationBarColor = Color.TRANSPARENT
+
+            val wicc = WindowInsetsControllerCompat(window, window.decorView)
+            val isLightStatusBars = wicc.isAppearanceLightStatusBars
+            val isLightNavigationBars = wicc.isAppearanceLightNavigationBars
+            wicc.isAppearanceLightStatusBars = false
+            wicc.isAppearanceLightNavigationBars = false
+
+            splashScreen?.setOnExitAnimationListener { splashProvider ->
+                // For some reason the SplashScreen applies (incorrect) Y translation to the iconView
+                splashProvider.iconView.translationY = 0F
+
+                val activityAnim = ValueAnimator.ofFloat(1F, 0F).apply {
+                    interpolator = LinearOutSlowInInterpolator()
+                    duration = SPLASH_EXIT_ANIM_DURATION
+                    addUpdateListener { va ->
+                        val value = va.animatedValue as Float
+                        binding.root.translationY = value * 16.dpToPx
+                    }
+                }
+
+                var barColorRestored = false
+                val splashAnim = ValueAnimator.ofFloat(1F, 0F).apply {
+                    interpolator = FastOutSlowInInterpolator()
+                    duration = SPLASH_EXIT_ANIM_DURATION
+                    addUpdateListener { va ->
+                        val value = va.animatedValue as Float
+                        splashProvider.view.alpha = value
+
+                        if (!barColorRestored && value <= 0.5F) {
+                            barColorRestored = true
+                            wicc.isAppearanceLightStatusBars = isLightStatusBars
+                            wicc.isAppearanceLightNavigationBars = isLightNavigationBars
+                        }
+                    }
+                    doOnEnd {
+                        splashProvider.remove()
+                        window.statusBarColor = oldStatusColor
+                        window.navigationBarColor = oldNavigationColor
+                        setNavbarScrim()
+                    }
+                }
+
+                activityAnim.start()
+                splashAnim.start()
+            }
+        } else {
+            setNavbarScrim()
+        }
+    }
+
     override fun onNewIntent(intent: Intent) {
         if (!handleIntentAction(intent)) {
             super.onNewIntent(intent)
@@ -408,6 +496,7 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
             }
         }
 
+        ready = true
         isHandlingShortcut = false
         return true
     }
@@ -532,7 +621,6 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
                     updateNavMenu(it.menu)
                     // SY <--
                 }
-
                 bottomViewNavigationBehavior?.slideUp(it)
             } else {
                 if (collapse) {
@@ -601,6 +689,11 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
     }
 
     companion object {
+        // Splash screen
+        private const val SPLASH_MIN_DURATION = 500 // ms
+        private const val SPLASH_MAX_DURATION = 5000 // ms
+        private const val SPLASH_EXIT_ANIM_DURATION = 400L // ms
+
         // Shortcut actions
         const val SHORTCUT_LIBRARY = "eu.kanade.tachiyomi.SHOW_LIBRARY"
         const val SHORTCUT_RECENTLY_UPDATED = "eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt
index a4dcd26e5..14347c9eb 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt
@@ -23,9 +23,11 @@ import eu.kanade.tachiyomi.ui.base.controller.NucleusController
 import eu.kanade.tachiyomi.ui.base.controller.RootController
 import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
 import eu.kanade.tachiyomi.ui.browse.source.browse.ProgressItem
+import eu.kanade.tachiyomi.ui.main.MainActivity
 import eu.kanade.tachiyomi.ui.manga.MangaController
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 import eu.kanade.tachiyomi.util.system.toast
+import eu.kanade.tachiyomi.util.view.onAnimationsFinished
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -110,6 +112,9 @@ class HistoryController :
         } else {
             adapter?.onLoadMoreComplete(mangaHistory)
         }
+        binding.recycler.onAnimationsFinished {
+            (activity as? MainActivity)?.ready = true
+        }
     }
 
     /**
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt
index edf04ede6..2a917c0b6 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/updates/UpdatesController.kt
@@ -27,6 +27,7 @@ import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChaptersAdapter
 import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 import eu.kanade.tachiyomi.util.system.notificationManager
 import eu.kanade.tachiyomi.util.system.toast
+import eu.kanade.tachiyomi.util.view.onAnimationsFinished
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import reactivecircus.flowbinding.recyclerview.scrollStateChanges
@@ -224,6 +225,9 @@ class UpdatesController :
     fun onNextRecentChapters(chapters: List<IFlexible<*>>) {
         destroyActionModeIfNeeded()
         adapter?.updateDataSet(chapters)
+        binding.recycler.onAnimationsFinished {
+            (activity as? MainActivity)?.ready = true
+        }
     }
 
     override fun onUpdateEmptyView(size: Int) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt
index cfd6a6d3d..d455d7f60 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt
@@ -197,3 +197,20 @@ inline fun TextView.setMaxLinesAndEllipsize(_ellipsize: TextUtils.TruncateAt = T
     maxLines = (measuredHeight - paddingTop - paddingBottom) / lineHeight
     ellipsize = _ellipsize
 }
+
+/**
+ * Callback will be run immediately when no animation running
+ */
+fun RecyclerView.onAnimationsFinished(callback: (RecyclerView) -> Unit) = post(
+    object : Runnable {
+        override fun run() {
+            if (isAnimating) {
+                itemAnimator?.isRunning {
+                    post(this)
+                }
+            } else {
+                callback(this@onAnimationsFinished)
+            }
+        }
+    }
+)
diff --git a/app/src/main/res/drawable/ic_tachi_splash.xml b/app/src/main/res/drawable/ic_tachi_splash.xml
new file mode 100644
index 000000000..fe41a2871
--- /dev/null
+++ b/app/src/main/res/drawable/ic_tachi_splash.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item
+        android:maxHeight="72dp"
+        android:drawable="@drawable/splash_icon"
+        android:gravity="center" />
+
+</layer-list>
diff --git a/app/src/main/res/drawable/splash_background.xml b/app/src/main/res/drawable/splash_background.xml
deleted file mode 100644
index c819fcb9a..000000000
--- a/app/src/main/res/drawable/splash_background.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <item android:drawable="@color/splash" />
-
-    <item>
-        <bitmap
-            android:gravity="center"
-            android:src="@drawable/splash_icon" />
-    </item>
-
-</layer-list>
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index b784ce3d4..e1ae64e4b 100755
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -178,8 +178,10 @@
     <!--===============-->
 
     <!--== Splash Theme ==-->
-    <style name="Theme.Splash" parent="Theme.Tachiyomi">
-        <item name="android:windowBackground">@drawable/splash_background</item>
+    <style name="Theme.Tachiyomi.SplashScreen" parent="Theme.SplashScreen">
+        <item name="windowSplashScreenAnimatedIcon">@drawable/ic_tachi_splash</item>
+        <item name="windowSplashScreenBackground">@color/splash</item>
+        <item name="postSplashScreenTheme">@style/Theme.Tachiyomi</item>
         <item name="android:statusBarColor">@android:color/transparent</item>
         <item name="android:navigationBarColor">@android:color/transparent</item>
         <item name="android:windowLightStatusBar">false</item>