diff --git a/loader/CMakeLists.txt b/loader/CMakeLists.txt
index 41cc8351..9470dfc4 100644
--- a/loader/CMakeLists.txt
+++ b/loader/CMakeLists.txt
@@ -66,6 +66,7 @@ file(GLOB SOURCES CONFIGURE_DEPENDS
 	src/utils/*.cpp
 	src/ui/*.cpp
 	src/ui/nodes/*.cpp
+	src/ui/mods/*.cpp
 	src/ui/internal/*.cpp
 	src/ui/internal/info/*.cpp
 	src/ui/internal/list/*.cpp
diff --git a/loader/include/Geode/ui/General.hpp b/loader/include/Geode/ui/General.hpp
index 941e2dbf..3214e71b 100644
--- a/loader/include/Geode/ui/General.hpp
+++ b/loader/include/Geode/ui/General.hpp
@@ -11,6 +11,31 @@ namespace geode {
      */
     GEODE_DLL cocos2d::CCSprite* createLayerBG();
 
+    enum class SideArt {
+        BottomLeft   = 0b0001,
+        BottomRight  = 0b0010,
+        TopLeft      = 0b0100,
+        TopRight     = 0b1000,
+        Bottom       = 0b0011,
+        Top          = 0b1100,
+        All          = 0b1111,
+    };
+    constexpr SideArt operator|(SideArt a, SideArt b) {
+        return static_cast<SideArt>(static_cast<int>(a) | static_cast<int>(b));
+    }
+    constexpr bool operator&(SideArt a, SideArt b) {
+        return static_cast<bool>(static_cast<int>(a) & static_cast<int>(b));
+    }
+
+    /**
+     * Add side art (corner pieces) for a layer
+     * @param to Layer to add corner pieces to
+     * @param sides Which corners to populate; by default, populates all
+     * @param useAnchorLayout If true, `to` is given an `AnchorLayout` and the 
+     * corners' positions are dynamically updated
+     */
+    GEODE_DLL void addSideArt(cocos2d::CCNode* to, SideArt sides = SideArt::All, bool useAnchorLayout = false);
+
     /**
      * Add the rounded comment borders to a node
      */
diff --git a/loader/include/Geode/ui/GeodeUI.hpp b/loader/include/Geode/ui/GeodeUI.hpp
index c4552eaa..2a8fdae3 100644
--- a/loader/include/Geode/ui/GeodeUI.hpp
+++ b/loader/include/Geode/ui/GeodeUI.hpp
@@ -30,23 +30,14 @@ namespace geode {
     GEODE_DLL void openSettingsPopup(Mod* mod);
     /**
      * Create a default logo sprite
-     * @param size Size of the sprite
      */
-    GEODE_DLL cocos2d::CCNode* createDefaultLogo(
-        cocos2d::CCSize const& size
-    );
+    GEODE_DLL cocos2d::CCNode* createDefaultLogo();
     /**
      * Create a logo sprite for a mod
-     * @param size Size of the sprite
      */
-    GEODE_DLL cocos2d::CCNode* createModLogo(
-        Mod* mod, cocos2d::CCSize const& size
-    );
+    GEODE_DLL cocos2d::CCNode* createModLogo(Mod* mod);
     /**
      * Create a logo sprite for an index item
-     * @param size Size of the sprite
      */
-    GEODE_DLL cocos2d::CCNode* createIndexItemLogo(
-        IndexItemHandle item, cocos2d::CCSize const& size
-    );
+    GEODE_DLL cocos2d::CCNode* createIndexItemLogo(IndexItemHandle item);
 }
diff --git a/loader/resources/download.png b/loader/resources/download.png
new file mode 100644
index 00000000..475d2b20
Binary files /dev/null and b/loader/resources/download.png differ
diff --git a/loader/resources/mod.json.in b/loader/resources/mod.json.in
index 0fc0bbbe..03dfe1f3 100644
--- a/loader/resources/mod.json.in
+++ b/loader/resources/mod.json.in
@@ -50,6 +50,9 @@
             ],
             "BlankSheet": [
                 "blanks/*.png"
+            ],
+            "SwelveSheet": [
+                "swelve/*.png"
             ]
         }
     },
diff --git a/loader/resources/mods-list-bottom.png b/loader/resources/mods-list-bottom.png
new file mode 100644
index 00000000..049f94a2
Binary files /dev/null and b/loader/resources/mods-list-bottom.png differ
diff --git a/loader/resources/mods-list-side.png b/loader/resources/mods-list-side.png
new file mode 100644
index 00000000..e87ddbc3
Binary files /dev/null and b/loader/resources/mods-list-side.png differ
diff --git a/loader/resources/mods-list-top.png b/loader/resources/mods-list-top.png
new file mode 100644
index 00000000..a0cb5d61
Binary files /dev/null and b/loader/resources/mods-list-top.png differ
diff --git a/loader/resources/search.png b/loader/resources/search.png
new file mode 100644
index 00000000..c29bca66
Binary files /dev/null and b/loader/resources/search.png differ
diff --git a/loader/resources/swelve/layer0.png b/loader/resources/swelve/layer0.png
new file mode 100644
index 00000000..cbd6dc3f
Binary files /dev/null and b/loader/resources/swelve/layer0.png differ
diff --git a/loader/resources/swelve/layer1.png b/loader/resources/swelve/layer1.png
new file mode 100644
index 00000000..8ae3edc3
Binary files /dev/null and b/loader/resources/swelve/layer1.png differ
diff --git a/loader/resources/swelve/layer2.png b/loader/resources/swelve/layer2.png
new file mode 100644
index 00000000..fe9221c3
Binary files /dev/null and b/loader/resources/swelve/layer2.png differ
diff --git a/loader/resources/swelve/layer3.png b/loader/resources/swelve/layer3.png
new file mode 100644
index 00000000..f1f6aac8
Binary files /dev/null and b/loader/resources/swelve/layer3.png differ
diff --git a/loader/resources/tab-bg.png b/loader/resources/tab-bg.png
new file mode 100644
index 00000000..afb22ad6
Binary files /dev/null and b/loader/resources/tab-bg.png differ
diff --git a/loader/src/hooks/MenuLayer.cpp b/loader/src/hooks/MenuLayer.cpp
index 2ff81663..a2a97b45 100644
--- a/loader/src/hooks/MenuLayer.cpp
+++ b/loader/src/hooks/MenuLayer.cpp
@@ -1,5 +1,4 @@
-#include "../ui/internal/list/ModListLayer.hpp"
-
+#include "../ui/mods/ModsLayer.hpp"
 #include <Geode/loader/Index.hpp>
 #include <Geode/modify/MenuLayer.hpp>
 #include <Geode/modify/Modify.hpp>
@@ -334,6 +333,6 @@ struct CustomMenuLayer : Modify<CustomMenuLayer, MenuLayer> {
     }
 
     void onGeode(CCObject*) {
-        ModListLayer::scene();
+        ModsLayer::scene();
     }
 };
diff --git a/loader/src/ui/internal/GeodeUI.cpp b/loader/src/ui/internal/GeodeUI.cpp
index 0115ea37..9010584d 100644
--- a/loader/src/ui/internal/GeodeUI.cpp
+++ b/loader/src/ui/internal/GeodeUI.cpp
@@ -66,66 +66,40 @@ void geode::openSettingsPopup(Mod* mod) {
     }
 }
 
-CCNode* geode::createDefaultLogo(CCSize const& size) {
+CCNode* geode::createDefaultLogo() {
     CCNode* spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
     if (!spr) {
         spr = CCLabelBMFont::create("OwO", "goldFont.fnt");
     }
-    limitNodeSize(spr, size, 1.f, .01f);
     return spr;
 }
 
-CCNode* geode::createModLogo(Mod* mod, CCSize const& size) {
-    CCNode* spr = mod == Mod::get() ?
-        CCSprite::createWithSpriteFrameName("geode-logo.png"_spr) :
-        CCSprite::create(fmt::format("{}/logo.png", mod->getID()).c_str());
-    if (!spr) spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
-    if (!spr) spr = CCLabelBMFont::create("N/A", "goldFont.fnt");
-    limitNodeSize(spr, size, 1.f, .01f);
-    spr->setPosition(size/2);
-    spr->setAnchorPoint({.5f, .5f});
-
-    auto node = CCNode::create();
-    node->addChild(spr);
-    node->setContentSize(size);
-    return node;
+CCNode* geode::createModLogo(Mod* mod) {
+    CCNode* ret = nullptr;
+    if (mod == Mod::get()) {
+        ret = CCSprite::createWithSpriteFrameName("geode-logo.png"_spr);
+    }
+    else {
+        ret = CCSprite::create(fmt::format("{}/logo.png", mod->getID()).c_str());
+    }
+    if (!ret) {
+        ret = createDefaultLogo();
+    }
+    return ret;
 }
 
-CCNode* geode::createIndexItemLogo(IndexItemHandle item, CCSize const& size) {
+CCNode* geode::createIndexItemLogo(IndexItemHandle item) {
     auto logoPath = ghc::filesystem::absolute(item->getRootPath() / "logo.png");
     CCNode* spr = CCSprite::create(logoPath.string().c_str());
     if (!spr) {
-        spr = CCSprite::createWithSpriteFrameName("no-logo.png"_spr);
-    }
-    if (!spr) {
-        spr = CCLabelBMFont::create("N/A", "goldFont.fnt");
+        spr = createDefaultLogo();
     }
     if (item->isFeatured()) {
-        auto glowSize = size + CCSize(4.f, 4.f);
-
         auto logoGlow = CCSprite::createWithSpriteFrameName("logo-glow.png"_spr);
-        logoGlow->setScaleX(glowSize.width / logoGlow->getContentSize().width);
-        logoGlow->setScaleY(glowSize.height / logoGlow->getContentSize().height);
-
-        // i dont know why + 1 is needed and its too late for me to figure out why
-        spr->setPosition(
-            logoGlow->getContentSize().width / 2 + 1,
-            logoGlow->getContentSize().height / 2 - 1
-        );
-        // scary mathematics
-        spr->setScaleX(size.width / spr->getContentSize().width / logoGlow->getScaleX());
-        spr->setScaleY(size.height / spr->getContentSize().height / logoGlow->getScaleY());
-        logoGlow->addChild(spr);
+        spr->setScaleX(logoGlow->getContentWidth() / spr->getContentWidth());
+        spr->setScaleY(logoGlow->getContentHeight() / spr->getContentHeight());
+        logoGlow->addChildAtPosition(spr, Anchor::Center);
         spr = logoGlow;
     }
-    else {
-        limitNodeSize(spr, size, 1.f, .01f);
-    }
-    spr->setPosition(size/2);
-    spr->setAnchorPoint({.5f, .5f});
-
-    auto node = CCNode::create();
-    node->addChild(spr);
-    node->setContentSize(size);
-    return node;
+    return spr;
 }
diff --git a/loader/src/ui/internal/info/ModInfoPopup.cpp b/loader/src/ui/internal/info/ModInfoPopup.cpp
index 8e9740e1..735be430 100644
--- a/loader/src/ui/internal/info/ModInfoPopup.cpp
+++ b/loader/src/ui/internal/info/ModInfoPopup.cpp
@@ -444,7 +444,7 @@ bool LocalModInfoPopup::init(Mod* mod, ModListLayer* list) {
 }
 
 CCNode* LocalModInfoPopup::createLogo(CCSize const& size) {
-    return geode::createModLogo(m_mod, size);
+    return geode::createModLogo(m_mod);
 }
 
 ModMetadata LocalModInfoPopup::getMetadata() const {
@@ -677,7 +677,7 @@ void IndexItemInfoPopup::onInstall(CCObject*) {
 }
 
 CCNode* IndexItemInfoPopup::createLogo(CCSize const& size) {
-    return geode::createIndexItemLogo(m_item, size);
+    return geode::createIndexItemLogo(m_item);
 }
 
 ModMetadata IndexItemInfoPopup::getMetadata() const {
diff --git a/loader/src/ui/internal/list/InstallListCell.cpp b/loader/src/ui/internal/list/InstallListCell.cpp
index 007a26d3..0eb8a0ba 100644
--- a/loader/src/ui/internal/list/InstallListCell.cpp
+++ b/loader/src/ui/internal/list/InstallListCell.cpp
@@ -168,7 +168,7 @@ ModInstallListCell* ModInstallListCell::create(Mod* mod, InstallListPopup* list,
 }
 
 CCNode* ModInstallListCell::createLogo(CCSize const& size) {
-    return geode::createModLogo(m_mod, size);
+    return geode::createModLogo(m_mod);
 }
 std::string ModInstallListCell::getID() const {
     return m_mod->getID();
@@ -292,7 +292,7 @@ IndexItemInstallListCell* IndexItemInstallListCell::create(
 }
 
 CCNode* IndexItemInstallListCell::createLogo(CCSize const& size) {
-    return geode::createIndexItemLogo(m_item, size);
+    return geode::createIndexItemLogo(m_item);
 }
 std::string IndexItemInstallListCell::getID() const {
     return m_item->getMetadata().getID();
@@ -354,7 +354,7 @@ UnknownInstallListCell* UnknownInstallListCell::create(
 }
 
 CCNode* UnknownInstallListCell::createLogo(CCSize const& size) {
-    return geode::createDefaultLogo(size);
+    return geode::createDefaultLogo();
 }
 std::string UnknownInstallListCell::getID() const {
     return m_dependency.id;
@@ -401,7 +401,7 @@ SelectVersionCell* SelectVersionCell::create(IndexItemHandle item, SelectVersion
 }
 
 CCNode* SelectVersionCell::createLogo(CCSize const& size) {
-    return geode::createIndexItemLogo(m_item, size);
+    return geode::createIndexItemLogo(m_item);
 }
 std::string SelectVersionCell::getID() const {
     return m_item->getMetadata().getID();
diff --git a/loader/src/ui/internal/list/ModListCell.cpp b/loader/src/ui/internal/list/ModListCell.cpp
index 088f7f33..3e24d1e6 100644
--- a/loader/src/ui/internal/list/ModListCell.cpp
+++ b/loader/src/ui/internal/list/ModListCell.cpp
@@ -344,7 +344,7 @@ std::optional<ModMetadata> ModCell::getModMetadata() const {
 }
 
 CCNode* ModCell::createLogo(CCSize const& size) {
-    return geode::createModLogo(m_mod, size);
+    return geode::createModLogo(m_mod);
 }
 
 // IndexItemCell
@@ -434,7 +434,7 @@ std::optional<ModMetadata> IndexItemCell::getModMetadata() const {
 }
 
 CCNode* IndexItemCell::createLogo(CCSize const& size) {
-    return geode::createIndexItemLogo(m_item, size);
+    return geode::createIndexItemLogo(m_item);
 }
 
 // InvalidGeodeFileCell
diff --git a/loader/src/ui/mods/ModItem.cpp b/loader/src/ui/mods/ModItem.cpp
new file mode 100644
index 00000000..ccd76d74
--- /dev/null
+++ b/loader/src/ui/mods/ModItem.cpp
@@ -0,0 +1,83 @@
+#include "ModItem.hpp"
+#include <Geode/ui/GeodeUI.hpp>
+
+bool BaseModItem::init() {
+    if (!CCNode::init())
+        return false;
+    
+    return true;
+}
+
+void BaseModItem::setupCommonInfo() {
+    auto meta = this->getMetadata();
+
+    m_logo = this->createModLogo();
+    this->addChild(m_logo);
+
+    m_title = CCLabelBMFont::create(meta.getName().c_str(), "bigFont.fnt");
+    m_title->setAnchorPoint({ .0f, .5f });
+    this->addChild(m_title);
+
+    auto by = "By " + ModMetadata::formatDeveloperDisplayString(meta.getDevelopers());
+    auto developersBtn = CCMenuItemSpriteExtra::create(
+        CCLabelBMFont::create(by.c_str(), "goldFont.fnt"),
+        this, nullptr
+    );
+    m_developers = CCMenu::create();
+    m_developers->ignoreAnchorPointForPosition(false);
+    m_developers->setContentSize(developersBtn->getScaledContentSize());
+    m_developers->addChildAtPosition(developersBtn, Anchor::Center);
+    m_developers->setAnchorPoint({ .0f, .5f });
+    this->addChild(m_developers);
+}
+
+void BaseModItem::updateSize(float width, bool big) {
+    this->setContentSize({ width, big ? 40.f : 25.f });
+
+    if (m_logo) {
+        auto logoSize = m_obContentSize.height - 5;
+        limitNodeSize(m_logo, { logoSize, logoSize }, 999, .1f);
+        m_logo->setPosition(m_obContentSize.height / 2 + 5, m_obContentSize.height / 2);
+    }
+    CCSize titleSpace {
+        m_obContentSize.width / 2 - m_obContentSize.height,
+        m_obContentSize.height / 2
+    };
+    if (m_title) {
+        m_title->setPosition(m_obContentSize.height + 10, m_obContentSize.height * .7f);
+        limitNodeSize(m_title, titleSpace, 1.f, .1f);
+    }
+    if (m_developers) {
+        m_developers->setPosition(m_obContentSize.height + 10, m_obContentSize.height * .3f);
+        limitNodeSize(m_developers, titleSpace, .6f, .1f);
+    }
+}
+
+bool InstalledModItem::init(Mod* mod) {
+    if (!BaseModItem::init())
+        return false;
+    
+    m_mod = mod;
+    
+    this->setupCommonInfo();
+    
+    return true;
+}
+
+InstalledModItem* InstalledModItem::create(Mod* mod) {
+    auto ret = new InstalledModItem();
+    if (ret && ret->init(mod)) {
+        ret->autorelease();
+        return ret;
+    }
+    CC_SAFE_DELETE(ret);
+    return nullptr;
+}
+
+ModMetadata InstalledModItem::getMetadata() const {
+    return m_mod->getMetadata();
+}
+
+CCNode* InstalledModItem::createModLogo() const {
+    return geode::createModLogo(m_mod);
+}
diff --git a/loader/src/ui/mods/ModItem.hpp b/loader/src/ui/mods/ModItem.hpp
new file mode 100644
index 00000000..035ba1e4
--- /dev/null
+++ b/loader/src/ui/mods/ModItem.hpp
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <Geode/ui/General.hpp>
+
+using namespace geode::prelude;
+
+class BaseModItem : public CCNode {
+protected:
+    CCNode* m_logo = nullptr;
+    CCNode* m_title = nullptr;
+    CCNode* m_developers = nullptr;
+
+    bool init();
+
+    void setupCommonInfo();
+
+public:
+    virtual ModMetadata getMetadata() const = 0;
+    virtual CCNode* createModLogo() const = 0;
+
+    virtual void updateSize(float width, bool big);
+};
+
+class InstalledModItem : public BaseModItem {
+protected:
+    Mod* m_mod;
+
+    bool init(Mod* mod);
+
+public:
+    static InstalledModItem* create(Mod* mod);
+
+    ModMetadata getMetadata() const override;
+    CCNode* createModLogo() const override;
+};
diff --git a/loader/src/ui/mods/ModsLayer.cpp b/loader/src/ui/mods/ModsLayer.cpp
new file mode 100644
index 00000000..ed889438
--- /dev/null
+++ b/loader/src/ui/mods/ModsLayer.cpp
@@ -0,0 +1,193 @@
+#include "ModsLayer.hpp"
+#include "SwelvyBG.hpp"
+
+static bool BIG_VIEW = false;
+
+bool ModsLayer::init() {
+    if (!CCLayer::init())
+        return false;
+
+    auto winSize = CCDirector::get()->getWinSize();
+    
+    this->addChild(SwelvyBG::create());
+    
+    auto backMenu = CCMenu::create();
+    backMenu->setContentWidth(100.f);
+    backMenu->setAnchorPoint({ .0f, .5f });
+    
+    auto backSpr = CCSprite::createWithSpriteFrameName("GJ_arrow_03_001.png");
+    auto backBtn = CCMenuItemSpriteExtra::create(
+        backSpr, this, menu_selector(ModsLayer::onBack)
+    );
+    backMenu->addChild(backBtn);
+    backMenu->setLayout(
+        RowLayout::create()
+            ->setAxisAlignment(AxisAlignment::Start)
+    );
+
+    this->addChildAtPosition(backMenu, Anchor::TopLeft, ccp(12, -25), false);
+
+    auto frame = CCNode::create();
+    frame->setAnchorPoint({ .5f, .5f });
+    frame->setContentSize({ 380, 205 });
+
+    auto frameBG = CCLayerColor::create({ 25, 17, 37, 255 });
+    frameBG->setContentSize(frame->getContentSize());
+    frameBG->ignoreAnchorPointForPosition(false);
+    frame->addChildAtPosition(frameBG, Anchor::Center);
+
+    auto tabsTop = CCSprite::createWithSpriteFrameName("mods-list-top.png"_spr);
+    tabsTop->setAnchorPoint({ .5f, .0f });
+    frame->addChildAtPosition(tabsTop, Anchor::Top, ccp(0, -2));
+
+    auto tabsLeft = CCSprite::createWithSpriteFrameName("mods-list-side.png"_spr);
+    tabsLeft->setScaleY(frame->getContentHeight() / tabsLeft->getContentHeight());
+    frame->addChildAtPosition(tabsLeft, Anchor::Left, ccp(6, 0));
+
+    auto tabsRight = CCSprite::createWithSpriteFrameName("mods-list-side.png"_spr);
+    tabsRight->setFlipX(true);
+    tabsRight->setScaleY(frame->getContentHeight() / tabsRight->getContentHeight());
+    frame->addChildAtPosition(tabsRight, Anchor::Right, ccp(-6, 0));
+
+    auto tabsBottom = CCSprite::createWithSpriteFrameName("mods-list-bottom.png"_spr);
+    tabsBottom->setAnchorPoint({ .5f, 1.f });
+    frame->addChildAtPosition(tabsBottom, Anchor::Bottom, ccp(0, 2));
+
+    this->addChildAtPosition(frame, Anchor::Center, ccp(0, -10), false);
+
+    m_list = ScrollLayer::create(frame->getContentSize() - ccp(24, 0));
+    m_list->m_contentLayer->setLayout(
+        ColumnLayout::create()
+            ->setAxisReverse(true)
+            ->setAxisAlignment(AxisAlignment::End)
+            ->setAutoGrowAxis(frame->getContentHeight())
+    );
+    this->addChildAtPosition(m_list, Anchor::Center, -m_list->getScaledContentSize() / 2 - ccp(0, 10), false);
+
+    auto mainTabs = CCMenu::create();
+    mainTabs->setContentWidth(tabsTop->getContentWidth() - 45);
+    mainTabs->setAnchorPoint({ .5f, .0f });
+    mainTabs->setPosition(frame->convertToWorldSpace(tabsTop->getPosition() + ccp(0, 10)));
+
+    for (auto item : std::initializer_list<std::tuple<const char*, const char*, const char*>> {
+        { "download.png"_spr, "Installed", "installed" },
+        { "GJ_bigStar_noShadow_001.png", "Featured", "featured" },
+        { "GJ_sTrendingIcon_001.png", "Trending", "trending" },
+        { "gj_folderBtn_001.png", "Mod Packs", "mod-packs" },
+        { "search.png"_spr, "Search", "search" },
+    }) {
+        const CCSize itemSize { 100, 40 };
+        const CCSize iconSize { 20, 20 };
+
+        auto spr = CCNode::create();
+        spr->setContentSize(itemSize);
+        spr->setAnchorPoint({ .5f, .5f });
+
+        auto disabledBG = CCScale9Sprite::createWithSpriteFrameName("tab-bg.png"_spr);
+        disabledBG->setContentSize(itemSize);
+        disabledBG->setID("disabled-bg");
+        disabledBG->setColor({ 26, 24, 29 });
+        spr->addChildAtPosition(disabledBG, Anchor::Center);
+
+        auto enabledBG = CCScale9Sprite::createWithSpriteFrameName("tab-bg.png"_spr);
+        enabledBG->setContentSize(itemSize);
+        enabledBG->setID("enabled-bg");
+        enabledBG->setColor({ 168, 147, 185 });
+        spr->addChildAtPosition(enabledBG, Anchor::Center);
+
+        auto icon = CCSprite::createWithSpriteFrameName(std::get<0>(item));
+        limitNodeSize(icon, iconSize, 3.f, .1f);
+        spr->addChildAtPosition(icon, Anchor::Left, ccp(iconSize.width / 2 + 5, 0), false);
+
+        auto title = CCLabelBMFont::create(std::get<1>(item), "bigFont.fnt");
+        title->limitLabelWidth(spr->getContentWidth() - iconSize.width - 15, .55f, .1f);
+        title->setAnchorPoint({ .0f, .5f });
+        spr->addChildAtPosition(title, Anchor::Left, ccp((iconSize.width + 10), 0), false);
+
+        auto btn = CCMenuItemSpriteExtra::create(spr, this, menu_selector(ModsLayer::onTab));
+        btn->setID(std::get<2>(item));
+        mainTabs->addChild(btn);
+        m_tabs.push_back(btn);
+    }
+
+    mainTabs->setLayout(RowLayout::create());
+    this->addChild(mainTabs);
+
+    this->gotoTab("installed");
+
+    this->setKeypadEnabled(true);
+    cocos::handleTouchPriority(this, true);
+
+    return true;
+}
+
+void ModsLayer::loadList(std::string const& id, bool update) {
+    if (m_currentList) {
+        m_currentList->scrollPosition = m_list->m_contentLayer->getPositionY();
+    }
+    m_list->m_contentLayer->removeAllChildren();
+    if (!m_listItemsCache.contains(id) || update) {
+        ListCache cache;
+        switch (hash(id.c_str())) {
+            case hash("installed"): {
+                for (auto mod : Loader::get()->getAllMods()) {
+                    cache.items.push_back(InstalledModItem::create(mod));
+                }
+            } break;
+        }
+        cache.scrollPosition = std::numeric_limits<float>::max();
+        m_listItemsCache[id] = std::move(cache);
+    }
+    auto& cache = m_listItemsCache.at(id);
+    for (auto item : cache.items) {
+        m_list->m_contentLayer->addChild(item);
+        item->updateSize(m_list->getContentWidth(), BIG_VIEW);
+    }
+    m_list->m_contentLayer->updateLayout();
+    auto listTopScrollPos = -m_list->m_contentLayer->getContentHeight() + m_list->getContentHeight();
+    if (cache.scrollPosition > 0.f || cache.scrollPosition < listTopScrollPos) {
+        cache.scrollPosition = listTopScrollPos;
+    }
+    m_list->m_contentLayer->setPositionY(cache.scrollPosition);
+    m_currentList = &cache;
+}
+
+void ModsLayer::gotoTab(std::string const& id) {
+    for (auto tab : m_tabs) {
+        auto selected = tab->getID() == id;
+        tab->getNormalImage()->getChildByID("disabled-bg")->setVisible(!selected);
+        tab->getNormalImage()->getChildByID("enabled-bg")->setVisible(selected);
+        tab->setEnabled(!selected);
+    }
+    this->loadList(id);
+}
+
+void ModsLayer::onTab(CCObject* sender) {
+    this->gotoTab(static_cast<CCNode*>(sender)->getID());
+}
+
+void ModsLayer::keyBackClicked() {
+    this->onBack(nullptr);
+}
+
+void ModsLayer::onBack(CCObject*) {
+    CCDirector::get()->replaceScene(CCTransitionFade::create(.5f, MenuLayer::scene(false)));
+}
+
+ModsLayer* ModsLayer::create() {
+    auto ret = new ModsLayer();
+    if (ret && ret->init()) {
+        ret->autorelease();
+        return ret;
+    }
+    CC_SAFE_DELETE(ret);
+    return nullptr;
+}
+
+ModsLayer* ModsLayer::scene() {
+    auto scene = CCScene::create();
+    auto layer = ModsLayer::create();
+    scene->addChild(layer);
+    CCDirector::sharedDirector()->replaceScene(CCTransitionFade::create(.5f, scene));
+    return layer;
+}
diff --git a/loader/src/ui/mods/ModsLayer.hpp b/loader/src/ui/mods/ModsLayer.hpp
new file mode 100644
index 00000000..e39254ae
--- /dev/null
+++ b/loader/src/ui/mods/ModsLayer.hpp
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <Geode/ui/General.hpp>
+#include <Geode/ui/ScrollLayer.hpp>
+#include "ModItem.hpp"
+
+using namespace geode::prelude;
+
+struct ListCache {
+    std::vector<Ref<BaseModItem>> items;
+    float scrollPosition;
+};
+
+class ModsLayer : public CCLayer {
+protected:
+    std::vector<CCMenuItemSpriteExtra*> m_tabs;
+    ScrollLayer* m_list;
+    ListCache* m_currentList = nullptr;
+    std::unordered_map<std::string, ListCache> m_listItemsCache;
+
+    bool init();
+
+    void keyBackClicked() override;
+    void onTab(CCObject* sender);
+    void gotoTab(std::string const& id);
+    void loadList(std::string const& id, bool update = false);
+
+public:
+    static ModsLayer* create();
+    static ModsLayer* scene();
+
+    void onBack(CCObject*);
+};
diff --git a/loader/src/ui/mods/SwelvyBG.cpp b/loader/src/ui/mods/SwelvyBG.cpp
new file mode 100644
index 00000000..806d641b
--- /dev/null
+++ b/loader/src/ui/mods/SwelvyBG.cpp
@@ -0,0 +1,67 @@
+#include "SwelvyBG.hpp"
+#include <random>
+
+bool SwelvyBG::init() {
+    if (!CCSpriteBatchNode::initWithTexture(CCTextureCache::get()->textureForKey("SwelveSheet.png"_spr), 20))
+        return false;
+    
+    auto winSize = CCDirector::get()->getWinSize();
+    this->setContentSize(winSize);
+    this->setAnchorPoint({ 0.f, 0.f });
+
+    std::random_device rd;
+    std::mt19937 gen(rd());
+    std::uniform_int_distribution<> sign(0, 1);
+    std::uniform_real_distribution<float> dis(.05f, .15f);
+
+    float y = m_obContentSize.height + 5;
+    for (auto layer : std::initializer_list<std::pair<ccColor3B, const char*>> {
+        { ccc3(244, 212, 142), "layer3.png"_spr },
+        { ccc3(245, 174, 125), "layer0.png"_spr },
+        { ccc3(236, 137, 124), "layer1.png"_spr },
+        { ccc3(213, 105, 133), "layer2.png"_spr },
+        { ccc3(173, 84,  146), "layer1.png"_spr },
+        { ccc3(113, 74,  154), "layer0.png"_spr },
+    }) {
+        float speed = dis(gen);
+        if (sign(gen) == 0) {
+            speed = -speed;
+        }
+        auto frame = CCSpriteFrameCache::get()->spriteFrameByName(layer.second);
+        auto repeatCount = static_cast<int>(floor(winSize.width / frame->getRect().size.width)) + 2;
+        for (int i = 0; i < repeatCount; i += 1) {
+            auto sprite = CCSprite::createWithSpriteFrame(frame);
+            sprite->setColor(layer.first);
+            sprite->setAnchorPoint({ (speed < 0 ? 0.f : 1.f), 1 });
+            sprite->setPosition({ (i + 1) * (sprite->getContentWidth() - 1), y });
+            sprite->schedule(schedule_selector(SwelvyBG::updateSpritePosition));
+            sprite->setUserData(std::bit_cast<void*>(speed));
+            sprite->setTag(repeatCount);
+            this->addChild(sprite);
+        }
+        y -= m_obContentSize.height / 6;
+    }
+
+    return true;
+}
+
+void SwelvyBG::updateSpritePosition(float dt) {
+    auto speed = std::bit_cast<float>(this->getUserData());
+    this->setPositionX(this->getPositionX() - speed);
+    if (speed > 0 && this->getPositionX() < 0.f) {
+        this->setPositionX(this->getPositionX() + (this->getContentWidth() - 1) * this->getTag());
+    }
+    else if (speed < 0 && this->getPositionX() > this->getParent()->getContentWidth()) {
+        this->setPositionX(this->getPositionX() - (this->getContentWidth() - 1) * this->getTag());
+    }
+}
+
+SwelvyBG* SwelvyBG::create() {
+    auto ret = new SwelvyBG();
+    if (ret && ret->init()) {
+        ret->autorelease();
+        return ret;
+    }
+    CC_SAFE_DELETE(ret);
+    return nullptr;
+}
diff --git a/loader/src/ui/mods/SwelvyBG.hpp b/loader/src/ui/mods/SwelvyBG.hpp
new file mode 100644
index 00000000..77981742
--- /dev/null
+++ b/loader/src/ui/mods/SwelvyBG.hpp
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <Geode/ui/General.hpp>
+
+using namespace geode::prelude;
+
+class SwelvyBG : public CCSpriteBatchNode {
+protected:
+    bool init();
+
+    void updateSpritePosition(float dt);
+
+public:
+    static SwelvyBG* create();
+};
diff --git a/loader/src/ui/nodes/General.cpp b/loader/src/ui/nodes/General.cpp
index b5163d29..be47b48e 100644
--- a/loader/src/ui/nodes/General.cpp
+++ b/loader/src/ui/nodes/General.cpp
@@ -18,6 +18,29 @@ CCSprite* geode::createLayerBG() {
     return bg;
 }
 
+void geode::addSideArt(CCNode* to, SideArt sides, bool useAnchorLayout) {
+    if (sides & SideArt::BottomLeft) {
+        auto spr = CCSprite::createWithSpriteFrameName("GJ_sideArt_001.png");
+        to->addChildAtPosition(spr, Anchor::BottomLeft, ccp(35, 35), useAnchorLayout);
+    }
+    if (sides & SideArt::BottomRight) {
+        auto spr = CCSprite::createWithSpriteFrameName("GJ_sideArt_001.png");
+        spr->setFlipX(true);
+        to->addChildAtPosition(spr, Anchor::BottomRight, ccp(-35, 35), useAnchorLayout);
+    }
+    if (sides & SideArt::TopLeft) {
+        auto spr = CCSprite::createWithSpriteFrameName("GJ_sideArt_001.png");
+        spr->setFlipY(true);
+        to->addChildAtPosition(spr, Anchor::TopLeft, ccp(35, -35), useAnchorLayout);
+    }
+    if (sides & SideArt::TopRight) {
+        auto spr = CCSprite::createWithSpriteFrameName("GJ_sideArt_001.png");
+        spr->setFlipX(true);
+        spr->setFlipY(true);
+        to->addChildAtPosition(spr, Anchor::TopRight, ccp(-35, -35), useAnchorLayout);
+    }
+}
+
 void geode::addListBorders(CCNode* to, CCPoint const& center, CCSize const& size) {
     // if the size is 346.f, the top aligns perfectly by default :3
     if (size.width == 346.f) {
diff --git a/loader/src/utils/cocos.cpp b/loader/src/utils/cocos.cpp
index e7db8687..f55e779d 100644
--- a/loader/src/utils/cocos.cpp
+++ b/loader/src/utils/cocos.cpp
@@ -418,32 +418,16 @@ CCRect geode::cocos::calculateChildCoverage(CCNode* parent) {
     return calculateNodeCoverage(parent->getChildren());
 }
 
-void geode::cocos::limitNodeSize(cocos2d::CCNode* spr, cocos2d::CCSize const& size, float def, float min) {
-    spr->setScale(1.f);
-    auto [cwidth, cheight] = spr->getContentSize();
-
-    float scale = def;
-    if (size.height && size.height < cheight) {
-        scale = size.height / cheight;
-    }
-    if (size.width && size.width < cwidth) {
-        if (size.width / cwidth < scale) scale = size.width / cwidth;
-    }
-    if (def && def < scale) {
-        scale = def;
-    }
-    if (min && scale < min) {
-        scale = min;
-    }
-    spr->setScale(scale);
+void geode::cocos::limitNodeSize(CCNode* spr, CCSize const& size, float def, float min) {
+    spr->setScale(clamp(std::min(size.height / spr->getContentHeight(), size.width / spr->getContentWidth()), min, def));
 }
 
-bool geode::cocos::nodeIsVisible(cocos2d::CCNode* node) {
+bool geode::cocos::nodeIsVisible(CCNode* node) {
     if (!node->isVisible()) {
         return false;
     }
-    if (node->getParent()) {
-        return nodeIsVisible(node->getParent());
+    if (auto parent = node->getParent()) {
+        return nodeIsVisible(parent);
     }
     return true;
 }