diff --git a/lib/phlexy_ui/theme_controller.rb b/lib/phlexy_ui/theme_controller.rb
new file mode 100644
index 0000000..14a716d
--- /dev/null
+++ b/lib/phlexy_ui/theme_controller.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module PhlexyUI
+ # @component html class="theme-controller"
+ # Theme controller changes the theme when checkbox/radio is checked
+ # Supports common patterns: swap, toggle, dropdown, radio buttons
+ class ThemeController < Base
+ register_modifiers(
+ # "sm:swap"
+ # "md:swap"
+ # "lg:swap"
+ swap: "swap",
+ # "sm:swap sm:swap-rotate"
+ # "md:swap md:swap-rotate"
+ # "lg:swap lg:swap-rotate"
+ swap_rotate: "swap swap-rotate",
+ # "sm:swap sm:swap-flip"
+ # "md:swap md:swap-flip"
+ # "lg:swap lg:swap-flip"
+ swap_flip: "swap swap-flip",
+ # "sm:toggle"
+ # "md:toggle"
+ # "lg:toggle"
+ toggle: "toggle"
+ )
+
+ def initialize(*, theme_value: nil, checked: false, as: :input, **)
+ super(*, **)
+ @theme_value = theme_value
+ @checked = checked
+ @as = as
+ end
+
+ def view_template(&block)
+ input_classes = generate_classes!(
+ # "theme-controller"
+ component_html_class: :"theme-controller",
+ modifiers_map: {},
+ base_modifiers: [],
+ options: {}
+ )
+
+ attrs = {type: :checkbox, class: input_classes}
+ attrs[:value] = theme_value if theme_value
+ attrs[:checked] = true if checked
+ attrs.merge!(options)
+
+ if block
+ # If block given, wrap in label with modifier classes (swap, toggle, etc.)
+ wrapper_classes = generate_classes!(
+ component_html_class: nil,
+ modifiers_map: modifiers,
+ base_modifiers:,
+ options: {}
+ )
+
+ label(class: wrapper_classes) do
+ public_send(as, **attrs)
+ whitespace
+ block.call
+ whitespace
+ end
+ else
+ # Just render the input
+ public_send(as, **attrs)
+ end
+ end
+
+ private
+
+ attr_reader :theme_value, :checked
+ end
+end
diff --git a/spec/lib/phlexy_ui/theme_controller_spec.rb b/spec/lib/phlexy_ui/theme_controller_spec.rb
new file mode 100644
index 0000000..dd6a446
--- /dev/null
+++ b/spec/lib/phlexy_ui/theme_controller_spec.rb
@@ -0,0 +1,74 @@
+require "spec_helper"
+
+describe PhlexyUI::ThemeController do
+ subject(:output) { render described_class.new }
+
+ it "is expected to match the formatted HTML" do
+ expected_html = html <<~HTML
+
+ HTML
+
+ is_expected.to eq(expected_html)
+ end
+
+ describe "with theme_value" do
+ subject(:output) do
+ render described_class.new(theme_value: "dark")
+ end
+
+ it "renders with value attribute" do
+ expected_html = html <<~HTML
+
+ HTML
+
+ expect(output).to eq(expected_html)
+ end
+ end
+
+ describe "with checked" do
+ subject(:output) do
+ render described_class.new(theme_value: "dark", checked: true)
+ end
+
+ it "renders as checked" do
+ expected_html = html <<~HTML
+
+ HTML
+
+ expect(output).to eq(expected_html)
+ end
+ end
+
+ describe "with block for swap pattern" do
+ subject(:output) do
+ render described_class.new(:swap_rotate, theme_value: "dark") do
+ "🌞 🌙"
+ end
+ end
+
+ it "wraps in label with icons" do
+ expected_html = html <<~HTML
+
+ HTML
+
+ expect(output).to eq(expected_html)
+ end
+ end
+
+ describe "data" do
+ subject(:output) do
+ render described_class.new(data: {foo: "bar"})
+ end
+
+ it "renders it correctly" do
+ expected_html = html <<~HTML
+
+ HTML
+
+ expect(output).to eq(expected_html)
+ end
+ end
+end