diff --git a/example/Example/Contacts.hs b/example/Example/Contacts.hs
--- a/example/Example/Contacts.hs
+++ b/example/Example/Contacts.hs
@@ -61,10 +61,10 @@
   row (gap 10) $ do
     button (Reload Nothing) (bg GrayLight) "Reload"
 
-    dropdown Reload (== fil) $ do
-      option Nothing id ""
-      option (Just Active) id "Active!"
-      option (Just Inactive) id "Inactive"
+    dropdown Reload (== fil) id $ do
+      option Nothing ""
+      option (Just Active) "Active!"
+      option (Just Inactive) "Inactive"
 
     target (Contact 2) $ button Edit (bg GrayLight) "Edit 2"
 
diff --git a/hyperbole.cabal b/hyperbole.cabal
--- a/hyperbole.cabal
+++ b/hyperbole.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           hyperbole
-version:        0.1.1
+version:        0.1.2
 synopsis:       Web Framework inspired by HTMX
 description:    Web Framework inspired by HTMX.
 category:       Web
@@ -34,8 +34,8 @@
       Web.Hyperbole.Application
       Web.Hyperbole.Effect
       Web.Hyperbole.Embed
+      Web.Hyperbole.Forms
       Web.Hyperbole.HyperView
-      Web.Hyperbole.Input
       Web.Hyperbole.Route
   other-modules:
       Paths_hyperbole
@@ -58,7 +58,7 @@
     , bytestring >=0.11 && <0.13
     , casing >0.1.3.0 && <0.2
     , containers >=0.6 && <1
-    , effectful ==2.3.*
+    , effectful >=2.3 && <3
     , file-embed >=0.0.10 && <0.1
     , http-api-data ==0.6.*
     , http-types >=0.12.3 && <0.13
@@ -100,7 +100,7 @@
     , bytestring >=0.11 && <0.13
     , casing >0.1.3.0 && <0.2
     , containers >=0.6 && <1
-    , effectful ==2.3.*
+    , effectful >=2.3 && <3
     , file-embed >=0.0.10 && <0.1
     , http-api-data ==0.6.*
     , http-types >=0.12.3 && <0.13
diff --git a/src/Web/Hyperbole.hs b/src/Web/Hyperbole.hs
--- a/src/Web/Hyperbole.hs
+++ b/src/Web/Hyperbole.hs
@@ -3,7 +3,7 @@
   , module Web.Hyperbole.Effect
   , module Web.Hyperbole.HyperView
   , module Web.Hyperbole.Application
-  , module Web.Hyperbole.Input
+  , module Web.Hyperbole.Forms
   , module Web.View
   , Application
   , run
@@ -15,8 +15,8 @@
 import Web.Hyperbole.Application
 import Web.Hyperbole.Effect
 import Web.Hyperbole.Embed (scriptEmbed)
+import Web.Hyperbole.Forms
 import Web.Hyperbole.HyperView
-import Web.Hyperbole.Input
 import Web.Hyperbole.Route
 import Web.View hiding (button, form, input, label, link)
 
diff --git a/src/Web/Hyperbole/Forms.hs b/src/Web/Hyperbole/Forms.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/Hyperbole/Forms.hs
@@ -0,0 +1,145 @@
+{-# LANGUAGE DefaultSignatures #-}
+
+module Web.Hyperbole.Forms where
+
+import Data.Functor.Identity (Identity)
+import Data.Kind (Type)
+import Data.Text
+import Effectful
+import Effectful.Dispatch.Dynamic
+import GHC.Generics
+import Text.Casing (kebab)
+import Web.FormUrlEncoded qualified as FE
+import Web.Hyperbole.Effect
+import Web.Hyperbole.HyperView (HyperView (..), Param (..), dataTarget)
+import Web.Internal.FormUrlEncoded (GFromForm, defaultFormOptions, genericFromForm)
+import Web.View hiding (form, input)
+
+
+-- | The only time we can use Fields is inside a form
+newtype FormFields f id = FormFields id
+  deriving newtype (Show)
+
+
+instance (Param id, Show id) => Param (FormFields f id) where
+  parseParam t = FormFields <$> parseParam t
+  toParam (FormFields i) = toParam i
+
+
+instance (HyperView id, Show id) => HyperView (FormFields f id) where
+  type Action (FormFields f id) = Action id
+
+
+-- | TODO: there are many more of these: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
+data FieldInput
+  = NewPassword
+  | CurrentPassword
+  | Username
+  | Email
+  | Number
+  | TextInput
+  | Name
+  | OneTimeCode
+  | Organization
+  | StreetAddress
+  | Country
+  | CountryName
+  | PostalCode
+  | Search
+  deriving (Show)
+
+
+data Label a
+
+
+data Input a = Input
+
+
+newtype InputName = InputName Text
+
+
+field :: Mod -> View (Input id) () -> View (FormFields form id) ()
+field f cnt =
+  tag "label" (f . flexCol)
+    $ addContext Input cnt
+
+
+label :: Text -> View (Input id) ()
+label = text
+
+
+input :: FieldInput -> Mod -> InputName -> View (Input id) ()
+input fi f (InputName n) = tag "input" (f . name n . att "type" (typ fi) . att "autocomplete" (auto fi)) none
+ where
+  typ NewPassword = "password"
+  typ CurrentPassword = "password"
+  typ Number = "number"
+  typ Email = "email"
+  typ Search = "search"
+  typ _ = "text"
+
+  auto :: FieldInput -> Text
+  auto = pack . kebab . show
+
+
+form :: forall form id. (Form form, HyperView id) => Action id -> Mod -> (form Label -> View (FormFields form id) ()) -> View id ()
+form a f fcnt = do
+  vid <- context
+  let frm = formLabels :: form Label
+  let cnt = fcnt frm
+  tag "form" (onSubmit a . dataTarget vid . f . flexCol) $ addContext (FormFields vid) cnt
+ where
+  onSubmit :: (Param a) => a -> Mod
+  onSubmit = att "data-on-submit" . toParam
+
+
+submit :: Mod -> View (FormFields form id) () -> View (FormFields form id) ()
+submit f = tag "button" (att "type" "submit" . f)
+
+
+parseForm :: forall form es. (Form form, Hyperbole :> es) => Eff es (form Identity)
+parseForm = do
+  (f :: FE.Form) <- formData
+  let ef = fromForm f :: Either Text (form Identity)
+  either (send . HyperError . ParseError) pure ef
+
+
+class Form (form :: (Type -> Type) -> Type) where
+  formLabels :: form Label
+  default formLabels :: (Generic (form Label), GFormLabels (Rep (form Label))) => form Label
+  formLabels = to gFormLabels
+
+
+  fromForm :: FE.Form -> Either Text (form Identity)
+  default fromForm :: (Generic (form Identity), GFromForm (form Identity) (Rep (form Identity))) => FE.Form -> Either Text (form Identity)
+  fromForm = genericFromForm defaultFormOptions
+
+
+type family Field (context :: Type -> Type) a
+type instance Field Identity a = a
+type instance Field Label a = InputName
+
+
+-- | Automatically derive labels from form field names
+class GFormLabels f where
+  gFormLabels :: f p
+
+
+instance GFormLabels U1 where
+  gFormLabels = U1
+
+
+instance (GFormLabels f, GFormLabels g) => GFormLabels (f :*: g) where
+  gFormLabels = gFormLabels :*: gFormLabels
+
+
+instance (Selector s) => GFormLabels (M1 S s (K1 R InputName)) where
+  gFormLabels = M1 . K1 $ InputName $ pack (selName (undefined :: M1 S s (K1 R Text) p))
+
+
+instance (GFormLabels f) => GFormLabels (M1 D d f) where
+  gFormLabels = M1 gFormLabels
+
+
+instance (GFormLabels f) => GFormLabels (M1 C c f) where
+  gFormLabels = M1 gFormLabels
diff --git a/src/Web/Hyperbole/HyperView.hs b/src/Web/Hyperbole/HyperView.hs
--- a/src/Web/Hyperbole/HyperView.hs
+++ b/src/Web/Hyperbole/HyperView.hs
@@ -47,23 +47,23 @@
   :: (HyperView id)
   => (opt -> action)
   -> (opt -> Bool)
+  -> Mod
   -> View (Option opt id action) ()
   -> View id ()
-dropdown toAction isSel options = do
+dropdown toAction isSel f options = do
   c <- context
-  tag "select" (att "data-on-change" "" . dataTarget c) $ do
+  tag "select" (att "data-on-change" "" . dataTarget c . f) $ do
     addContext (Option toAction isSel) options
 
 
 option
   :: (HyperView id, Eq opt)
   => opt
-  -> Mod
   -> View (Option opt id (Action id)) ()
   -> View (Option opt id (Action id)) ()
-option opt f cnt = do
+option opt cnt = do
   os <- context
-  tag "option" (att "value" (toParam (os.toAction opt)) . selected (os.selected opt) . f) cnt
+  tag "option" (att "value" (toParam (os.toAction opt)) . selected (os.selected opt)) cnt
 
 
 selected :: Bool -> Mod
diff --git a/src/Web/Hyperbole/Input.hs b/src/Web/Hyperbole/Input.hs
deleted file mode 100644
--- a/src/Web/Hyperbole/Input.hs
+++ /dev/null
@@ -1,145 +0,0 @@
-{-# LANGUAGE DefaultSignatures #-}
-
-module Web.Hyperbole.Input where
-
-import Data.Functor.Identity (Identity)
-import Data.Kind (Type)
-import Data.Text
-import Effectful
-import Effectful.Dispatch.Dynamic
-import GHC.Generics
-import Text.Casing (kebab)
-import Web.FormUrlEncoded qualified as FE
-import Web.Hyperbole.Effect
-import Web.Hyperbole.HyperView (HyperView (..), Param (..), dataTarget)
-import Web.Internal.FormUrlEncoded (GFromForm, defaultFormOptions, genericFromForm)
-import Web.View hiding (form, input)
-
-
--- | The only time we can use Fields is inside a form
-newtype FormFields f id = FormFields id
-  deriving newtype (Show)
-
-
-instance (Param id, Show id) => Param (FormFields f id) where
-  parseParam t = FormFields <$> parseParam t
-  toParam (FormFields i) = toParam i
-
-
-instance (HyperView id, Show id) => HyperView (FormFields f id) where
-  type Action (FormFields f id) = Action id
-
-
--- | TODO: there are many more of these: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
-data FieldInput
-  = NewPassword
-  | CurrentPassword
-  | Username
-  | Email
-  | Number
-  | TextInput
-  | Name
-  | OneTimeCode
-  | Organization
-  | StreetAddress
-  | Country
-  | CountryName
-  | PostalCode
-  | Search
-  deriving (Show)
-
-
-data Label a
-
-
-data Input a = Input
-
-
-newtype InputName = InputName Text
-
-
-field :: Mod -> View (Input id) () -> View (FormFields form id) ()
-field f cnt =
-  tag "label" (f . flexCol)
-    $ addContext Input cnt
-
-
-label :: Text -> View (Input id) ()
-label = text
-
-
-input :: FieldInput -> Mod -> InputName -> View (Input id) ()
-input fi f (InputName n) = tag "input" (f . name n . att "type" (typ fi) . att "autocomplete" (auto fi)) none
- where
-  typ NewPassword = "password"
-  typ CurrentPassword = "password"
-  typ Number = "number"
-  typ Email = "email"
-  typ Search = "search"
-  typ _ = "text"
-
-  auto :: FieldInput -> Text
-  auto = pack . kebab . show
-
-
-form :: forall form id. (Form form, HyperView id) => Action id -> Mod -> (form Label -> View (FormFields form id) ()) -> View id ()
-form a f fcnt = do
-  vid <- context
-  let frm = formLabels :: form Label
-  let cnt = fcnt frm
-  tag "form" (onSubmit a . dataTarget vid . f . flexCol) $ addContext (FormFields vid) cnt
- where
-  onSubmit :: (Param a) => a -> Mod
-  onSubmit = att "data-on-submit" . toParam
-
-
-submit :: Mod -> View (FormFields form id) () -> View (FormFields form id) ()
-submit f = tag "button" (att "type" "submit" . f)
-
-
-parseForm :: forall form es. (Form form, Hyperbole :> es) => Eff es (form Identity)
-parseForm = do
-  (f :: FE.Form) <- formData
-  let ef = fromForm f :: Either Text (form Identity)
-  either (send . HyperError . ParseError) pure ef
-
-
-class Form (form :: (Type -> Type) -> Type) where
-  formLabels :: form Label
-  default formLabels :: (Generic (form Label), GFormLabels (Rep (form Label))) => form Label
-  formLabels = to gFormLabels
-
-
-  fromForm :: FE.Form -> Either Text (form Identity)
-  default fromForm :: (Generic (form Identity), GFromForm (form Identity) (Rep (form Identity))) => FE.Form -> Either Text (form Identity)
-  fromForm = genericFromForm defaultFormOptions
-
-
-type family Field (context :: Type -> Type) a
-type instance Field Identity a = a
-type instance Field Label a = InputName
-
-
--- | Automatically derive labels from form field names
-class GFormLabels f where
-  gFormLabels :: f p
-
-
-instance GFormLabels U1 where
-  gFormLabels = U1
-
-
-instance (GFormLabels f, GFormLabels g) => GFormLabels (f :*: g) where
-  gFormLabels = gFormLabels :*: gFormLabels
-
-
-instance (Selector s) => GFormLabels (M1 S s (K1 R InputName)) where
-  gFormLabels = M1 . K1 $ InputName $ pack (selName (undefined :: M1 S s (K1 R Text) p))
-
-
-instance (GFormLabels f) => GFormLabels (M1 D d f) where
-  gFormLabels = M1 gFormLabels
-
-
-instance (GFormLabels f) => GFormLabels (M1 C c f) where
-  gFormLabels = M1 gFormLabels
