diff --git a/esoui/ingame/collections/keyboard/dlcbook_keyboard.lua b/esoui/ingame/collections/keyboard/dlcbook_keyboard.lua
new file mode 100644
index 0000000..5e14288
--- /dev/null
+++ b/esoui/ingame/collections/keyboard/dlcbook_keyboard.lua
@@ -0,0 +1,308 @@
+local NOTIFICATIONS_PROVIDER = NOTIFICATIONS:GetCollectionsProvider()
+
+------------------
+--Initialization--
+------------------
+
+local DLCBook_Keyboard = ZO_Object:Subclass()
+
+function DLCBook_Keyboard:New(...)
+    local object = ZO_Object.New(self)
+    object:Initialize(...)
+    return object
+end
+
+function DLCBook_Keyboard:Initialize(control)
+    control.owner = self
+    self.control = control
+
+    self:InitializeControls()
+    self:InitializeNavigationList()
+    self:InitializeEvents()
+
+    local dlcBook = ZO_Scene:New("dlcBook", SCENE_MANAGER)
+
+    dlcBook:RegisterCallback("StateChange", function(oldState, newState)
+                                                if (newState == SCENE_SHOWN) then
+                                                    self:RefreshListIfDirty()
+                                                end
+                                            end)
+end
+
+function DLCBook_Keyboard:InitializeControls()
+    self.navigationList = self.control:GetNamedChild("NavigationList")
+
+    local contents = self.control:GetNamedChild("Contents")
+    self.imageControl = contents:GetNamedChild("Image")
+    self.nameControl = contents:GetNamedChild("Name")
+
+    local scrollSection = contents:GetNamedChild("ScrollContainer"):GetNamedChild("ScrollChild")
+    self.descriptionControl = scrollSection:GetNamedChild("Description")
+    self.unlockStatusControl = scrollSection:GetNamedChild("UnlockStatusLabel")
+    self.questStatusControl = scrollSection:GetNamedChild("QuestStatusLabel")
+    self.questAvailableControl = scrollSection:GetNamedChild("QuestAvailable")
+    self.questDescriptionControl = scrollSection:GetNamedChild("QuestDescription")
+
+    local buttons = contents:GetNamedChild("DLCInteractButtons")
+    self.questAcceptButton = buttons:GetNamedChild("QuestAccept")
+    self.unlockPermanentlyButton = buttons:GetNamedChild("UnlockPermanently")
+
+    self.subscribeButton = contents:GetNamedChild("SubscribeButton")
+    self.subscribeButton:SetHidden(IsESOPlusSubscriber())
+end
+
+function DLCBook_Keyboard:InitializeNavigationList()
+    self.navigationTree = ZO_Tree:New(self.navigationList:GetNamedChild("ScrollChild"), 60, -10, 300)
+
+    local openTexture = "EsoUI/Art/Buttons/tree_open_up.dds"
+    local closedTexture = "EsoUI/Art/Buttons/tree_closed_up.dds"
+    local overOpenTexture = "EsoUI/Art/Buttons/tree_open_over.dds"
+    local overClosedTexture = "EsoUI/Art/Buttons/tree_closed_over.dds"
+
+    local function TreeHeaderSetup(node, control, name, open)
+        control.text:SetModifyTextType(MODIFY_TEXT_TYPE_UPPERCASE)
+        control.text:SetText(name)
+
+        control.icon:SetTexture(open and openTexture or closedTexture)
+        control.iconHighlight:SetTexture(open and overOpenTexture or overClosedTexture)
+
+        local ENABLED = true
+        local DISABLE_SCALING = true
+        ZO_IconHeader_Setup(control, open, ENABLED, DISABLE_SCALING)
+    end
+
+    self.navigationTree:AddTemplate("ZO_DLCBookNavigationHeader_Keyboard", TreeHeaderSetup, nil, nil, nil, 0)
+
+    local function TreeEntrySetup(node, control, data, open)
+        control:SetText(data.name)
+        control:SetSelected(false)
+
+        control.statusIcon = control:GetNamedChild("StatusIcon")
+        data.notificationId = NOTIFICATIONS_PROVIDER:GetNotificationIdForCollectible(data.collectibleId)
+        control.statusIcon:SetHidden(data.notificationId == nil)
+    end
+
+    local function TreeEntryOnSelected(control, data, selected, reselectingDuringRebuild)
+        control:SetSelected(selected)
+
+         if selected then
+            self.selectedId = data.collectibleId
+            if not reselectingDuringRebuild then
+                self:RefreshDetails()
+            end
+
+            if data.notificationId then
+                if SCENE_MANAGER:IsShowing("dlcBook") then
+                    RemoveCollectibleNotification(data.notificationId)
+                else
+                    self.dirty = true
+                end
+            end
+        end
+    end
+
+    local function TreeEntryEquality(left, right)
+        return left.name == right.name
+    end
+
+    self.navigationTree:AddTemplate("ZO_DLCBookNavigationEntry_Keyboard", TreeEntrySetup, TreeEntryOnSelected, TreeEntryEquality)
+    self.navigationTree:SetOpenAnimation("ZO_TreeOpenAnimation")
+
+    self.headerNodes = {}
+    self.collectibleIdToTreeNode = {}
+
+    self:RefreshList()
+end
+
+function DLCBook_Keyboard:RefreshListIfDirty()
+    if self.dirty then
+        self:RefreshList()
+        self.dirty = false
+    end
+end
+
+function DLCBook_Keyboard:InitializeEvents()
+    COLLECTIONS_BOOK_SINGLETON:RegisterCallback("OnCollectibleUpdated", function(...) self:OnCollectibleUpdated(...) end)
+    COLLECTIONS_BOOK_SINGLETON:RegisterCallback("OnCollectionUpdated", function() self:OnCollectionUpdated() end)
+    COLLECTIONS_BOOK_SINGLETON:RegisterCallback("OnCollectionNotificationRemoved", function(...) self:OnCollectionNotificationRemoved(...) end)
+end
+
+---------------
+--Interaction--
+---------------
+
+function DLCBook_Keyboard:GetSelectedDLCData()
+    return self.navigationTree:GetSelectedData()
+end
+
+function DLCBook_Keyboard:FocusDLCWithCollectibleId(id)
+    local node = self.collectibleIdToTreeNode[id]
+
+    if node then
+        if self.navigationTree:GetSelectedNode() == node then
+            local RESELECT = true
+            node:OnSelected(RESELECT)
+        else
+            self.navigationTree:SelectNode(node)
+        end
+    end
+end
+
+function DLCBook_Keyboard:RefreshList()
+    ZO_ClearTable(self.headerNodes)
+    ZO_ClearTable(self.collectibleIdToTreeNode)
+    self.navigationTree:Reset()
+    local firstNode = nil
+    local selectedNode = nil
+
+    for i = 1, GetTotalCollectiblesByCategoryType(COLLECTIBLE_CATEGORY_TYPE_DLC) do
+        local collectibleId = GetCollectibleIdFromType(COLLECTIBLE_CATEGORY_TYPE_DLC, i)
+        local name, description, _, _, unlocked, _, active, _, _, isPlaceholder = GetCollectibleInfo(collectibleId)
+        if not isPlaceholder then
+            local unlockState = GetCollectibleUnlockStateById(collectibleId)
+            local simplifiedUnlockState = unlockState == COLLECTIBLE_UNLOCK_STATE_LOCKED and COLLECTIBLE_UNLOCK_STATE_LOCKED or COLLECTIBLE_UNLOCK_STATE_UNLOCKED_OWNED
+            local background = GetCollectibleKeyboardBackgroundImage(collectibleId)
+            if not self.headerNodes[simplifiedUnlockState] then
+                self.headerNodes[simplifiedUnlockState] = self.navigationTree:AddNode("ZO_DLCBookNavigationHeader_Keyboard", GetString("SI_COLLECTIBLEUNLOCKSTATE", simplifiedUnlockState), nil, SOUNDS.JOURNAL_PROGRESS_CATEGORY_SELECTED)
+            end
+            local headerNode = self.headerNodes[simplifiedUnlockState]
+
+            local data =
+            {
+                collectibleId = collectibleId,
+                name = name,
+                description = description,
+                background = background,
+                unlockState = unlockState,
+                active = active,
+            }
+            local dlcNode = self.navigationTree:AddNode("ZO_DLCBookNavigationEntry_Keyboard", data, headerNode, SOUNDS.JOURNAL_PROGRESS_SUB_CATEGORY_SELECTED)
+            self.collectibleIdToTreeNode[collectibleId] = dlcNode
+            if not firstNode then
+                firstNode = dlcNode
+            end
+
+            if self.selectedId and self.selectedId == data.collectibleId then
+                selectedNode = dlcNode
+            end
+        end
+    end
+
+    self.navigationTree:Commit()
+    local navigateToNode = selectedNode or firstNode
+
+    if navigateToNode then
+        self.navigationTree:SelectNode(navigateToNode)
+    end
+    self:RefreshDetails()
+end
+
+function DLCBook_Keyboard:RefreshDetails()
+    local data = self.navigationTree:GetSelectedData()
+
+    if data then
+        self.imageControl:SetTexture(data.background)
+        self.nameControl:SetText(data.name)
+        self.descriptionControl:SetText(data.description)
+        self.unlockStatusControl:SetText(GetString("SI_COLLECTIBLEUNLOCKSTATE", data.unlockState))
+
+        local questAcceptLabelStringId = data.active and SI_DLC_BOOK_QUEST_STATUS_ACCEPTED or SI_DLC_BOOK_QUEST_STATUS_NOT_ACCEPTED
+        local questName = GetCollectibleQuestPreviewInfo(data.collectibleId)
+        self.questStatusControl:SetText(zo_strformat(SI_DLC_BOOK_QUEST_STATUS, questName, GetString(questAcceptLabelStringId)))
+
+        local showsQuest = not (data.active or data.unlockState == COLLECTIBLE_UNLOCK_STATE_LOCKED)
+        local questAvailableControl = self.questAvailableControl
+        local questDescriptionControl = self.questDescriptionControl
+        if showsQuest then
+            questAvailableControl:SetText(GetString(SI_COLLECTIONS_QUEST_AVAILABLE))
+            questAvailableControl:SetHidden(false)
+            
+            local questDescription = select(2, GetCollectibleQuestPreviewInfo(data.collectibleId))
+            questDescriptionControl:SetText(questDescription)
+            questDescriptionControl:SetHidden(false)
+        elseif data.unlockState == COLLECTIBLE_UNLOCK_STATE_LOCKED then
+            questAvailableControl:SetText(GetString(SI_COLLECTIONS_QUEST_AVAILABLE_WITH_UNLOCK))
+            questAvailableControl:SetHidden(false)
+            questDescriptionControl:SetHidden(true)
+        else
+            questAvailableControl:SetHidden(true)
+            questDescriptionControl:SetHidden(true)
+        end
+
+        local questAcceptButtonStringId = data.active and SI_DLC_BOOK_ACTION_QUEST_ACCEPTED or SI_DLC_BOOK_ACTION_ACCEPT_QUEST
+        self.questAcceptButton:SetText(GetString(questAcceptButtonStringId))
+        self.questAcceptButton:SetEnabled(data.unlockState ~= COLLECTIBLE_UNLOCK_STATE_LOCKED and not data.active)
+        self.unlockPermanentlyButton:SetHidden(data.unlockState == COLLECTIBLE_UNLOCK_STATE_UNLOCKED_OWNED)
+    end
+end
+
+function DLCBook_Keyboard:UseSelectedDLC()
+    local data = self.navigationTree:GetSelectedData()
+    UseCollectible(data.collectibleId)
+end
+
+function DLCBook_Keyboard:SearchSelectedDLCInStore()
+    local data = self.navigationTree:GetSelectedData()
+    ShowMarketAndSearch(data.name, MARKET_OPEN_OPERATION_COLLECTIONS_DLC)
+end
+
+function DLCBook_Keyboard:BrowseToCollectible(collectibleId)
+    self:FocusDLCWithCollectibleId(collectibleId)
+    SCENE_MANAGER:Show("dlcBook")
+end
+
+function DLCBook_Keyboard:IsCategoryIndexDLC(categoryIndex)
+    local categoryType = select(7, GetCollectibleCategoryInfo(categoryIndex))
+    return categoryType == COLLECTIBLE_CATEGORY_TYPE_DLC
+end
+
+----------
+--Events--
+----------
+
+function DLCBook_Keyboard:OnCollectibleUpdated(collectibleId, justUnlocked)
+    if justUnlocked then
+        self:RefreshList()
+    else
+        local node = self.collectibleIdToTreeNode[collectibleId]
+        if node then
+            local data = node:GetData()
+            data.active = select(7, GetCollectibleInfo(collectibleId))
+
+            local unlockState = GetCollectibleUnlockStateById(collectibleId)
+            if data.unlockState ~= unlockState then
+                self:RefreshList()
+            else
+                self:RefreshDetails()
+            end
+        end
+    end
+end
+
+function DLCBook_Keyboard:OnCollectionNotificationRemoved(notificationId, collectibleId)
+    local node = self.collectibleIdToTreeNode[collectibleId]
+
+    if node then
+        self:RefreshList()
+    end
+end
+
+function DLCBook_Keyboard:OnCollectionUpdated()
+    self:RefreshList()
+end
+
+function ZO_DLCBook_Keyboard_OnQuestAcceptClicked(control)
+    DLC_BOOK_KEYBOARD:UseSelectedDLC()
+end
+
+function ZO_DLCBook_Keyboard_OnUnlockPermanentlyClicked(control)
+    DLC_BOOK_KEYBOARD:SearchSelectedDLCInStore()
+end
+
+function ZO_DLCBook_Keyboard_OnSubscribeClicked(control)
+    ZO_Dialogs_ShowDialog("CONFIRM_OPEN_URL_BY_TYPE", { urlType = APPROVED_URL_ESO_ACCOUNT_SUBSCRIPTION }, { mainTextParams = { GetString(SI_ESO_PLUS_SUBSCRIPTION_LINK_TEXT), GetString(SI_URL_APPLICATION_WEB) } })
+end
+
+function ZO_DLCBook_Keyboard_OnInitialize(control)
+    DLC_BOOK_KEYBOARD = DLCBook_Keyboard:New(control)
+end
\ No newline at end of file