diff --git a/lib/phlexy_ui.rb b/lib/phlexy_ui.rb
index 5ddbe74..24d655b 100644
--- a/lib/phlexy_ui.rb
+++ b/lib/phlexy_ui.rb
@@ -17,6 +17,7 @@ module PhlexyUI
   autoload :Tabs, "phlexy_ui/tabs"
   autoload :Drawer, "phlexy_ui/drawer"
   autoload :Dropdown, "phlexy_ui/dropdown"
+  autoload :Menu, "phlexy_ui/menu"
 end
 
 loader.eager_load
diff --git a/lib/phlexy_ui/menu.rb b/lib/phlexy_ui/menu.rb
new file mode 100644
index 0000000..bf74a03
--- /dev/null
+++ b/lib/phlexy_ui/menu.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module PhlexyUI
+  class Menu < Base
+    def view_template(&)
+      generate_classes!(
+        component_html_class: :menu,
+        modifiers_map: MENU_MODIFIERS_MAP,
+        base_modifiers:,
+        options:
+      ).then do |classes|
+        ul(class: classes, **options, &)
+      end
+    end
+
+    def title(*, **, &)
+      li(class: "menu-title", **, &)
+    end
+
+    def item(*base_modifiers, **, &)
+      generate_classes!(
+        modifiers_map: MENU_ITEM_MODIFIERS_MAP,
+        base_modifiers:,
+        options:
+      ).then do |classes|
+        li(class: classes, &)
+      end
+    end
+
+    def submenu(*, **, &)
+      render SubMenu.new(*, **, &)
+    end
+
+    private
+
+    MENU_MODIFIERS_MAP = {
+      xs: "menu-xs",
+      sm: "menu-sm",
+      md: "menu-md",
+      lg: "menu-lg",
+      vertical: "menu-vertical",
+      horizontal: "menu-horizontal"
+    }.freeze
+
+    MENU_ITEM_MODIFIERS_MAP = {
+      disabled: "disabled",
+      active: "active",
+      focus: "focus"
+    }
+  end
+end
diff --git a/lib/phlexy_ui/menu/sub_menu.rb b/lib/phlexy_ui/menu/sub_menu.rb
new file mode 100644
index 0000000..9a26fef
--- /dev/null
+++ b/lib/phlexy_ui/menu/sub_menu.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module PhlexyUI
+  class Menu
+    # @private
+    class SubMenu < Base
+      include Phlex::DeferredRender
+
+      def initialize(*, **)
+        super
+        @items ||= []
+      end
+
+      def view_template(&)
+        attributes = ATTRIBUTES_MAP.select do |key|
+          base_modifiers.include?(key)
+        end
+
+        details(**attributes) do
+          if @subtitle
+            summary do
+              render @subtitle
+            end
+          end
+
+          if @items.any?
+            ul do
+              @items.each do |item|
+                li do
+                  render item
+                end
+              end
+            end
+          end
+        end
+      end
+
+      def subtitle(&block)
+        @subtitle = block
+      end
+
+      def item(&block)
+        @items << block
+      end
+
+      def submenu(*, **, &)
+        @items << self.class.new(*, **, &)
+      end
+
+      private
+
+      ATTRIBUTES_MAP = {
+        open: true
+      }.freeze
+    end
+  end
+end
diff --git a/spec/lib/phlexy_ui/menu_spec.rb b/spec/lib/phlexy_ui/menu_spec.rb
new file mode 100644
index 0000000..0df3990
--- /dev/null
+++ b/spec/lib/phlexy_ui/menu_spec.rb
@@ -0,0 +1,92 @@
+require "spec_helper"
+
+describe PhlexyUI::Dropdown do
+  subject(:output) { render described_class.new }
+
+  describe "rendering a full menu" do
+    let(:component) do
+      Class.new(Phlex::HTML) do
+        def view_template(&)
+          render PhlexyUI::Menu.new(:xs) do |menu|
+            menu.title do
+              "My Menu"
+            end
+
+            menu.item do
+              "Item 1"
+            end
+
+            menu.item(:disabled) do
+              "Item 2"
+            end
+
+            menu.item(:active) do
+              "Item 3"
+            end
+
+            menu.item(:focus) do
+              "Item 4"
+            end
+
+            menu.item do
+              menu.submenu do |submenu_1|
+                submenu_1.subtitle do
+                  "Parent 1"
+                end
+
+                submenu_1.item do
+                  "Child 1"
+                end
+
+                submenu_1.submenu(:open) do |submenu_2|
+                  submenu_2.subtitle do
+                    "Parent 2"
+                  end
+
+                  submenu_2.item do
+                    "Child 2"
+                  end
+                end
+              end
+            end
+          end
+        end
+      end
+    end
+
+    subject(:output) do
+      render component.new
+    end
+
+    it "is expected to match the formatted HTML" do
+      expected_html = html <<~HTML
+        <ul class="menu menu-xs">
+          <li class="menu-title">My Menu</li>
+          <li>Item 1</li>
+          <li class="disabled">Item 2</li>
+          <li class="active">Item 3</li>
+          <li class="focus">Item 4</li>
+          <li>
+            <details>
+              <summary>Parent 1</summary>
+
+              <ul>
+                <li>Child 1</li>
+                <li>
+                  <details open>
+                    <summary>Parent 2</summary>
+                    <ul>
+                      <li>Child 2</li>
+                    </ul>
+                  </details>
+                </li>
+              </ul>
+            </details>
+          </li>
+        </ul>
+      HTML
+
+      is_expected.to eq(expected_html)
+    end
+  end
+end