diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,5 @@
-# Revision history for lucid-ui
+# Revision history for web-view
 
-## 0.1.0.0 -- YYYY-mm-dd
+## 0.2.3 -- 2023-11-27
 
 * First version. Released on an unsuspecting world.
diff --git a/example/Main.hs b/example/Main.hs
--- a/example/Main.hs
+++ b/example/Main.hs
@@ -16,6 +16,14 @@
   Warp.run 3010 app
 
 
+clash :: View c ()
+clash = col (gap 10 . pad 20) $ do
+  el (bold . fontSize 24) "Clash"
+  el_ "What happens if we set the same style twice?"
+
+  el (bg Primary . bg Warning) "Hello"
+
+
 buttons :: View c ()
 buttons = col (gap 10 . pad 20) $ do
   el (bold . fontSize 32) "My page"
@@ -83,12 +91,12 @@
 examples :: View c ()
 examples = col (pad 20 . gap 15) $ do
   el (bold . fontSize 24) "Layout"
-  link "Buttons"
-  link "Responsive"
-  link "Holy Grail"
+  link "buttons" lnk "Buttons"
+  link "responsive" lnk "Responsive"
+  link "holygrail" lnk "Holy Grail"
+  link "clash" lnk "Clash"
  where
-  link :: Text -> View c ()
-  link n = tag "a" (att "href" (url n) . color Primary) (text n)
+  lnk = color Primary
   url = toLower . T.filter (/= ' ')
 
 
@@ -99,6 +107,7 @@
     ["buttons"] -> view buttons
     ["responsive"] -> view responsive
     ["holygrail"] -> view holygrail
+    ["clash"] -> view clash
     _ -> notFound
  where
   html h =
diff --git a/src/Web/View.hs b/src/Web/View.hs
--- a/src/Web/View.hs
+++ b/src/Web/View.hs
@@ -54,6 +54,7 @@
   , name
   , value
   , label
+  , link
   , button
 
     -- ** Tables
diff --git a/src/Web/View/Element.hs b/src/Web/View/Element.hs
--- a/src/Web/View/Element.hs
+++ b/src/Web/View/Element.hs
@@ -60,6 +60,11 @@
 pre f t = tag "pre" f (text t)
 
 
+-- | A hyperlink to the given url
+link :: Url -> Mod -> View c () -> View c ()
+link (Url u) f = tag "a" (att "href" u . f)
+
+
 -- * Inputs
 
 
diff --git a/src/Web/View/Render.hs b/src/Web/View/Render.hs
--- a/src/Web/View/Render.hs
+++ b/src/Web/View/Render.hs
@@ -56,7 +56,7 @@
 
   styleElement :: Content
   styleElement =
-    Node $ Element "style" [] [("type", "text/css")] [Text $ intercalate "\n" css]
+    Node $ Element "style" (Attributes [] [("type", "text/css")]) [Text $ intercalate "\n" css]
 
   renderContent :: Content -> [Text]
   renderContent (Node t) = renderTag indent t
@@ -181,7 +181,7 @@
 flatAttributes :: Element -> FlatAttributes
 flatAttributes t =
   FlatAttributes
-    $ addClass (mconcat t.classes) t.attributes
+    $ addClass t.attributes.classes t.attributes.other
  where
   addClass [] atts = atts
   addClass cx atts = M.insert "class" (classAttValue cx) atts
diff --git a/src/Web/View/Reset.hs b/src/Web/View/Reset.hs
--- a/src/Web/View/Reset.hs
+++ b/src/Web/View/Reset.hs
@@ -7,7 +7,7 @@
 import Data.Text
 
 
-{- | Default CSS to clear unintuitive default styles. This or 'cssResetLink' is required.
+{- | Default CSS to remove unintuitive default styles. This or 'cssResetLink' is required.
 
 > import Data.String.Interpolate (i)
 >
diff --git a/src/Web/View/Style.hs b/src/Web/View/Style.hs
--- a/src/Web/View/Style.hs
+++ b/src/Web/View/Style.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DisambiguateRecordFields #-}
 {-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE OverloadedLists #-}
 
@@ -332,14 +333,13 @@
 
 
 mapModClass :: (Class -> Class) -> Mod -> Mod
-mapModClass fc fm el =
+mapModClass fc fm as =
   -- apply the function to all classes added by the mod
   -- ignore
-  let el' = fm $ Element "none" [] [] []
-   in el
-        { classes = el.classes <> (map fc <$> el'.classes)
-        , attributes = el.attributes <> el'.attributes
-        , children = el.children <> el'.children
+  let as' = fm $ Attributes [] []
+   in as'
+        { classes = as.classes <> map fc as'.classes
+        , other = as.other <> as'.other
         }
 
 
@@ -356,7 +356,11 @@
 >     & prop @Int "flex-shrink" 0
 -}
 addClass :: Class -> Mod
-addClass c t = t{classes = [c] : t.classes}
+addClass c attributes =
+  Attributes
+    { classes = c : attributes.classes
+    , other = attributes.other
+    }
 
 
 -- | Construct a class from a ClassName
diff --git a/src/Web/View/Types.hs b/src/Web/View/Types.hs
--- a/src/Web/View/Types.hs
+++ b/src/Web/View/Types.hs
@@ -21,19 +21,21 @@
 -- | A single HTML tag. Note that the class attribute is stored separately from the rest of the attributes to make adding styles easier
 data Element = Element
   { name :: Name
-  , classes :: [[Class]]
   , attributes :: Attributes
   , children :: [Content]
   }
 
 
-type Attributes = Map Name AttValue
+data Attributes = Attributes
+  { classes :: [Class]
+  , other :: Map Name AttValue
+  }
 type Attribute = (Name, AttValue)
 type Name = Text
 type AttValue = Text
 
 
--- * Element Modifiers
+-- * Attribute Modifiers
 
 
 {- | Element functions expect a Mod function as their first argument that adds attributes and classes.
@@ -43,7 +45,7 @@
 >   where
 >     active = isActive user then bold else id
 -}
-type Mod = Element -> Element
+type Mod = Attributes -> Attributes
 
 
 -- * Atomic CSS
@@ -193,7 +195,7 @@
 
 
 -- | Element's attributes do not include class, which is separated. FlatAttributes generate the class attribute and include it
-newtype FlatAttributes = FlatAttributes {attributes :: Attributes}
+newtype FlatAttributes = FlatAttributes {attributes :: Map Name AttValue}
   deriving (Generic)
 
 
diff --git a/src/Web/View/View.hs b/src/Web/View/View.hs
--- a/src/Web/View/View.hs
+++ b/src/Web/View/View.hs
@@ -108,10 +108,11 @@
   -- Applies the modifier and merges children into parent
   ctx <- context
   let st = runView ctx ct
-  let elm = f $ Element nm [] [] st.contents
+  let ats = f $ Attributes [] []
+  let elm = Element nm ats st.contents
   viewAddContent $ Node elm
   viewAddClasses $ M.elems st.css
-  viewAddClasses $ mconcat elm.classes
+  viewAddClasses elm.attributes.classes
 
 
 {- | Set an attribute, replacing existing value
@@ -120,4 +121,6 @@
 > hlink url content = tag "a" (att "href" url) content
 -}
 att :: Name -> AttValue -> Mod
-att n v (Element en ec ea ecs) = Element en ec (M.insert n v ea) ecs
+att n v attributes =
+  let atts = M.insert n v attributes.other
+   in attributes{other = atts}
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,3 @@
+{-# OPTIONS_GHC -F -pgmF sydtest-discover #-}
+
+
diff --git a/test/Test/RenderSpec.hs b/test/Test/RenderSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/RenderSpec.hs
@@ -0,0 +1,12 @@
+module Test.RenderSpec (spec) where
+
+import Test.Syd
+import Web.View
+
+
+spec :: Spec
+spec = do
+  describe "render" $ do
+    it "should match expected output" $ do
+      let res = renderText $ el bold "hi"
+      pureGoldenTextFile "test/resources/basic.txt" res
diff --git a/test/Tests.hs b/test/Tests.hs
deleted file mode 100644
--- a/test/Tests.hs
+++ /dev/null
@@ -1,4 +0,0 @@
-module Main (main) where
-
-main :: IO ()
-main = putStrLn "Test suite not yet implemented."
diff --git a/web-view.cabal b/web-view.cabal
--- a/web-view.cabal
+++ b/web-view.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           web-view
-version:        0.2.3
+version:        0.3.1
 synopsis:       Type-safe HTML and CSS with intuitive layouts and composable styles.
 description:    Type-safe HTML and CSS with intuitive layouts and composable styles. Inspired by Tailwindcss and Elm-UI . See documentation for the @Web.View@ module below
 category:       Web
@@ -90,19 +90,22 @@
 
 test-suite tests
   type: exitcode-stdio-1.0
-  main-is: Tests.hs
+  main-is: Spec.hs
   other-modules:
+      Test.RenderSpec
       Paths_web_view
   autogen-modules:
       Paths_web_view
   hs-source-dirs:
-      test
+      test/
   default-extensions:
       OverloadedStrings
       OverloadedRecordDot
       DuplicateRecordFields
       NoFieldSelectors
-  ghc-options: -Wall -fdefer-typed-holes
+  ghc-options: -Wall -fdefer-typed-holes -threaded -rtsopts -with-rtsopts=-N
+  build-tool-depends:
+      sydtest-discover:sydtest-discover
   build-depends:
       base >=4.16 && <5
     , bytestring >=0.11 && <0.13
@@ -111,5 +114,7 @@
     , effectful ==2.3.*
     , file-embed >=0.0.10 && <0.1
     , string-interpolate >=0.3.2 && <0.4
+    , sydtest ==0.15.*
     , text >=1.2 && <3
+    , web-view
   default-language: GHC2021
