diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b631e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# elm-package generated files +elm-stuff +# elm-repl generated files +repl-temp-* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..362fa72 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2019, Orasund +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 4e407d4..dd3c3a2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,15 @@ -# elm-widgets -Collection of reusable views for elm-ui. +# Elm-Ui-Widgets + +Usefull Widgets written in for Elm-ui. +These include: + +* Select +* Multi Select +* Collapsable +* Dialog +* Carousel +* Snackbar +* Sort Table +* Filter Select +* Validated Input +* Scrolling Nav \ No newline at end of file diff --git a/docs.json b/docs.json new file mode 100644 index 0000000..89b845a --- /dev/null +++ b/docs.json @@ -0,0 +1 @@ +[{"name":"Framework","comment":" This module includes the basic building bocks.\r\nMaybe start by copying the follow code into your project:\r\n\r\n```\r\nview : Html msg\r\nview =\r\n Framework.layout [] <|\r\n Element.el Framework.container <|\r\n Element.text \"the first element should be a container.\"\r\n```\r\n\r\n@docs layout, container, layoutOptions, layoutAttributes\r\n\r\n","unions":[],"aliases":[],"values":[{"name":"container","comment":" The container should be the outer most element.\r\nIt centers the content and sets the background to white.\r\n","type":"List.List (Element.Attribute msg)"},{"name":"layout","comment":" A replacement of Element.layout adding both the Framework.layoutOptions and the Framework.layoutAttributes.\r\n","type":"List.List (Element.Attribute msg) -> Element.Element msg -> Html.Html msg"},{"name":"layoutAttributes","comment":" The default Attributes. Check the source code if you want to know more.\r\n","type":"List.List (Element.Attribute msg)"},{"name":"layoutOptions","comment":" The default layoutOptions. Check the source code if you want to know more.\r\n","type":"List.List Element.Option"}],"binops":[]},{"name":"Framework.Button","comment":" This module contains a attribute to style buttons.\r\n\r\n```\r\nInput.button (Button.simple ++ Color.primary) <|\r\n { onPress = Nothing\r\n , label = Element.text \"Button.simple ++ Color.primary\"\r\n }\r\n```\r\n\r\nThe attribute can only be used on `Input.button` but it may be with additional attibutes from this package.\r\n\r\n@docs simple\r\n\r\n","unions":[],"aliases":[],"values":[{"name":"simple","comment":" a simple Button styling. Check the source-code for more information.\r\n","type":"List.List (Element.Attribute msg)"}],"binops":[]},{"name":"Framework.Card","comment":" The Card attributes can be used almost anywere in the elm-ui elements.\r\n\r\nHere are some examples:\r\n\r\n```\r\nElement.column (Card.simple ++ Grid.simple) <|\r\n [ Element.text <| \"adds a border around the column\"\r\n ]\r\n```\r\n\r\n```\r\nElement.el Card.small <|\r\n Element.text \"a basic card\"\r\n```\r\n\r\n```\r\nInput.button (Button.simple ++ Card.large) <|\r\n { onPress = Nothing\r\n , label = Element.text \"a clickable card\"\r\n }\r\n```\r\n\r\n@docs simple, small, large, fill\r\n\r\n","unions":[],"aliases":[],"values":[{"name":"fill","comment":" A card filling all the avaiable space.\r\nCheck the source-code for more information.\r\n","type":"List.List (Element.Attribute msg)"},{"name":"large","comment":" A 480px wide card.\r\nCheck the source-code for more information.\r\n","type":"List.List (Element.Attribute msg)"},{"name":"simple","comment":" A basic card.\r\nCheck the source-code for more information.\r\n","type":"List.List (Element.Attribute msg)"},{"name":"small","comment":" A 240px wide card.\r\nCheck the source-code for more information.\r\n","type":"List.List (Element.Attribute msg)"}],"binops":[]},{"name":"Framework.Color","comment":" This module contains the colors used in the framework.\r\n\r\n@docs cyan, green, lighterGrey, lightGrey, grey, darkGrey, darkerGrey, red, turquoise, yellow\r\n\r\nSome colors also have a Attribute that can be used nearly everywhere.\r\n\r\n@docs danger, light, dark, disabled, info, primary, success, warning\r\n\r\n","unions":[],"aliases":[],"values":[{"name":"cyan","comment":" ","type":"Element.Color"},{"name":"danger","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"dark","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"darkGrey","comment":" ","type":"Element.Color"},{"name":"darkerGrey","comment":" ","type":"Element.Color"},{"name":"disabled","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"green","comment":" ","type":"Element.Color"},{"name":"grey","comment":" ","type":"Element.Color"},{"name":"info","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"light","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"lightGrey","comment":" ","type":"Element.Color"},{"name":"lighterGrey","comment":" ","type":"Element.Color"},{"name":"primary","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"red","comment":" ","type":"Element.Color"},{"name":"success","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"turquoise","comment":" ","type":"Element.Color"},{"name":"warning","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"yellow","comment":" ","type":"Element.Color"}],"binops":[]},{"name":"Framework.Grid","comment":" This module include the basic attributes for columns and rows and two variants.\r\nAny of these Attributes can be used for columns and rows.\r\n\r\n```\r\nElement.row Grid.spacedEvenly <|\r\n [ Element.text \"left item\"\r\n , Element.text \"center item\"\r\n , Element.text \"right item\"\r\n ]\r\n```\r\n\r\n@docs simple, spacedEvenly, section\r\n\r\n","unions":[],"aliases":[],"values":[{"name":"section","comment":" The simple attributes but with a horizontal line at the top\r\nCheck the source-code for more information.\r\n","type":"List.List (Element.Attribute msg)"},{"name":"simple","comment":" The basic attributes for columns and rows.\r\nCheck the source-code for more information.\r\n","type":"List.List (Element.Attribute msg)"},{"name":"spacedEvenly","comment":" The simple attibutes but with evenly spaced elements.\r\nCheck the source-code for more information.\r\n","type":"List.List (Element.Attribute msg)"}],"binops":[]},{"name":"Framework.Heading","comment":" Styling for heading\r\n\r\n```\r\nElement.el Heading.h1 <| Element.text \"Only Element.el may be styled as a heading\"\r\n```\r\n\r\n@docs h1, h2, h3, h4, h5, h6\r\n\r\n","unions":[],"aliases":[],"values":[{"name":"h1","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"h2","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"h3","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"h4","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"h5","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"h6","comment":" ","type":"List.List (Element.Attribute msg)"}],"binops":[]},{"name":"Framework.Input","comment":" This module exposes simple attibutes for Inputs (beside Buttons) and\r\nstyling for labels.\r\n\r\n```\r\nInput.text Input.simple\r\n { onChange = always ()\r\n , text = \"Input.simple\"\r\n , placeholder = Nothing\r\n , label = Input.labelLeft Input.label <| Element.text \"Input.label\"\r\n }\r\n```\r\n\r\n@docs simple, label\r\n\r\n","unions":[],"aliases":[],"values":[{"name":"label","comment":" ","type":"List.List (Element.Attribute msg)"},{"name":"simple","comment":" ","type":"List.List (Element.Attribute msg)"}],"binops":[]}] \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..448753b --- /dev/null +++ b/docs/index.html @@ -0,0 +1,13358 @@ + + + + + Example + + + + + +

+
+
+
+
+
\ No newline at end of file
diff --git a/docs/select.png b/docs/select.png
new file mode 100644
index 0000000..b544c66
Binary files /dev/null and b/docs/select.png differ
diff --git a/elm.json b/elm.json
new file mode 100644
index 0000000..3d7f294
--- /dev/null
+++ b/elm.json
@@ -0,0 +1,29 @@
+{
+    "type": "package",
+    "name": "Orasund/elm-ui-widgets",
+    "summary": "Collection of reusable views for elm-ui.",
+    "license": "BSD-3-Clause",
+    "version": "1.0.0",
+    "exposed-modules": [
+        "View",
+        "View.FilterSelect",
+        "View.ValidatedInput",
+        "View.WrappedColumn"
+    ],
+    "elm-version": "0.19.0 <= v < 0.20.0",
+    "dependencies": {
+        "Orasund/elm-ui-framework": "1.6.1 <= v < 2.0.0",
+        "elm/browser": "1.0.2 <= v < 2.0.0",
+        "elm/core": "1.0.0 <= v < 2.0.0",
+        "elm/html": "1.0.0 <= v < 2.0.0",
+        "elm/time": "1.0.0 <= v < 2.0.0",
+        "elm-community/intdict": "3.0.0 <= v < 4.0.0",
+        "jasonliang512/elm-heroicons": "1.0.2 <= v < 2.0.0",
+        "mdgriffith/elm-ui": "1.1.5 <= v < 2.0.0",
+        "turboMaCk/queue": "1.0.2 <= v < 2.0.0",
+        "wernerdegroot/listzipper": "4.0.0 <= v < 5.0.0"
+    },
+    "test-dependencies": {
+        "elm-explorations/test": "1.2.1 <= v < 2.0.0"
+    }
+}
diff --git a/example/elm.json b/example/elm.json
new file mode 100644
index 0000000..a891a32
--- /dev/null
+++ b/example/elm.json
@@ -0,0 +1,32 @@
+{
+    "type": "application",
+    "source-directories": [
+        "src",
+        "../src"
+    ],
+    "elm-version": "0.19.1",
+    "dependencies": {
+        "direct": {
+            "Orasund/elm-ui-framework": "1.6.1",
+            "elm/browser": "1.0.2",
+            "elm/core": "1.0.5",
+            "elm/html": "1.0.0",
+            "elm/time": "1.0.0",
+            "elm-community/intdict": "3.0.0",
+            "jasonliang512/elm-heroicons": "1.0.2",
+            "mdgriffith/elm-ui": "1.1.5",
+            "turboMaCk/queue": "1.0.2",
+            "wernerdegroot/listzipper": "4.0.0"
+        },
+        "indirect": {
+            "elm/json": "1.1.3",
+            "elm/svg": "1.0.1",
+            "elm/url": "1.0.0",
+            "elm/virtual-dom": "1.0.2"
+        }
+    },
+    "test-dependencies": {
+        "direct": {},
+        "indirect": {}
+    }
+}
diff --git a/example/src/Component.elm b/example/src/Component.elm
new file mode 100644
index 0000000..9f50690
--- /dev/null
+++ b/example/src/Component.elm
@@ -0,0 +1,179 @@
+module Component exposing (Model,Msg(..),init,update,view)
+
+import Browser
+import Element exposing (Element,Color)
+import Element.Input as Input
+import Element.Background as Background
+import Framework
+import Framework.Button as Button
+import Framework.Card as Card
+import Framework.Color as Color
+import Framework.Grid as Grid
+import Framework.Group as Group
+import Framework.Heading as Heading
+import Framework.Input as Input
+import Framework.Tag as Tag
+import Html exposing (Html)
+import Html.Attributes as Attributes
+import Set exposing (Set)
+import Widget
+import Widget.FilterSelect as FilterSelect
+import Widget.ScrollingNav as ScrollingNav
+import Widget.ValidatedInput as ValidatedInput
+import Widget.Snackbar as Snackbar
+import Time
+import Heroicons.Solid as Heroicons
+
+type alias Model =
+    { filterSelect : FilterSelect.Model
+    , validatedInput : ValidatedInput.Model () (( String, String ))
+    }
+
+type Msg
+    = FilterSelectSpecific FilterSelect.Msg
+    | ValidatedInputSpecific ValidatedInput.Msg
+    
+
+init :  Model
+init  =
+    
+    { filterSelect =
+            [ "Apple"
+            , "Kiwi"
+            , "Strawberry"
+            , "Pineapple"
+            , "Mango"
+            , "Grapes"
+            , "Watermelon"
+            , "Orange"
+            , "Lemon"
+            , "Blueberry"
+            , "Grapefruit"
+            , "Coconut"
+            , "Cherry"
+            , "Banana"
+            ]
+                |> Set.fromList
+                |> FilterSelect.init
+      , validatedInput =
+            ValidatedInput.init
+                { value = ("John","Doe")
+                , validator =
+                    \string ->
+                        case string |> String.split " " of
+                            [ first, second ] ->
+                                Ok (( first, second ))
+
+                            _ ->
+                                Err ()
+                , toString =
+                    (\( first, second ) -> first ++ " " ++ second)
+                }
+      }
+    
+
+update : Msg -> Model -> ( Model, Cmd Msg )
+update msg model =
+    case msg of
+        FilterSelectSpecific m ->
+            ( { model
+                | filterSelect = model.filterSelect |> FilterSelect.update m
+              }
+            , Cmd.none
+            )
+
+        ValidatedInputSpecific m ->
+            ( { model
+                | validatedInput = model.validatedInput |> ValidatedInput.update m
+              }
+            , Cmd.none
+            )
+
+filterSelect : FilterSelect.Model -> Element Msg
+filterSelect model =
+    Element.column (Grid.simple ++ Card.small) <|
+        [ Element.el Heading.h3 <| Element.text "Filter Select"
+        , case model.selected of
+            Just selected ->
+                Element.row Grid.compact
+                    [ Element.el (Tag.simple ++ Group.left) <| Element.text <| selected
+                    , Input.button (Tag.simple ++ Group.right ++ Color.danger)
+                        { onPress = Just <| FilterSelectSpecific <| FilterSelect.Selected Nothing
+                        , label = Element.html <| Heroicons.x [Attributes.width 16]
+                        }
+                    ]
+
+            Nothing ->
+                Element.column Grid.simple
+                    [ FilterSelect.viewInput Input.simple
+                        model
+                        { msgMapper = FilterSelectSpecific
+                        , placeholder = Just <| Input.placeholder [] <| Element.text <|
+                            "Fruit"
+                        , label = "Fruit"
+                        }
+                    , model
+                        |> FilterSelect.viewOptions
+                        |> List.map
+                            (\string ->
+                                Input.button (Button.simple ++ Tag.simple)
+                                    { onPress = Just <| FilterSelectSpecific <| FilterSelect.Selected <| Just <| string
+                                    , label = Element.text string
+                                    }
+                            )
+                        |> Element.wrappedRow [ Element.spacing 10 ]
+                    ]
+        
+        ]
+
+
+validatedInput : ValidatedInput.Model () ( ( String, String )) -> Element Msg
+validatedInput model =
+    Element.column (Grid.simple ++ Card.small) <|
+        [ Element.el Heading.h3 <| Element.text "Validated Input"
+        , ValidatedInput.view Input.simple
+            model
+            { label = "First Name, Sir Name"
+            , msgMapper = ValidatedInputSpecific
+            , placeholder = Nothing
+            , readOnly = \maybeTuple ->
+                Element.row Grid.compact
+                    [ maybeTuple 
+                    |>  (\(a,b) -> a ++ " " ++ b)
+                    |> Element.text
+                      
+                      |> Element.el (Tag.simple ++ Group.left)
+                    , Heroicons.pencil [Attributes.width 16]
+                      |> Element.html
+                      |>Element.el (Tag.simple ++ Group.right ++ Color.primary)
+                    ]
+            }
+        ]
+
+
+
+
+scrollingNavCard : Element msg
+scrollingNavCard  =
+    [Element.el Heading.h3 <| Element.text "Scrolling Nav"
+    , Element.text "Resize the screen and use the scrollbar to see the scrolling navigation in action."
+        |> List.singleton
+        |> Element.paragraph []
+    ]
+    |> Element.column (Grid.simple ++ Card.small)
+
+view : Model -> Element Msg
+view model =
+    Element.column (Grid.section ++ [Element.centerX])
+        [ Element.el Heading.h2 <| Element.text "Components"
+        , "Components have a Model, an Update- and sometimes even a Subscription-function. It takes some time to set them up correctly."
+            |> Element.text
+            |> List.singleton
+            |> Element.paragraph []
+        , Element.wrappedRow (Grid.simple ++ [Element.centerX])
+        <|
+            [ filterSelect model.filterSelect 
+            , validatedInput model.validatedInput 
+            , scrollingNavCard 
+            ]
+        ]
\ No newline at end of file
diff --git a/example/src/Example.elm b/example/src/Example.elm
new file mode 100644
index 0000000..03c4b57
--- /dev/null
+++ b/example/src/Example.elm
@@ -0,0 +1,274 @@
+module Example exposing (main)
+
+import Browser
+import Element exposing (Element)
+import Element.Input as Input
+import Framework
+import Framework.Button as Button
+import Framework.Card as Card
+import Framework.Color as Color
+import Framework.Grid as Grid
+import Framework.Group as Group
+import Framework.Heading as Heading
+import Framework.Input as Input
+import Framework.Tag as Tag
+import Html exposing (Html)
+import Html.Attributes as Attributes
+import Set exposing (Set)
+import Widget
+import Widget.FilterSelect as FilterSelect
+import Widget.ScrollingNav as ScrollingNav
+import Widget.ValidatedInput as ValidatedInput
+import Widget.Snackbar as Snackbar
+import Stateless
+import Reusable
+import Component
+import Time
+
+type Section
+    = ComponentViews
+    | ReusableViews
+    | StatelessViews
+
+type alias Model =
+    { component : Component.Model
+    , stateless : Stateless.Model
+    , reusable : Reusable.Model
+    , scrollingNav : ScrollingNav.Model Section
+    , snackbar : Snackbar.Model String
+    , displayDialog : Bool
+    }
+
+type Msg
+    = StatelessSpecific Stateless.Msg
+    | ReusableSpecific Reusable.Msg
+    | ComponentSpecific Component.Msg
+    | ScrollingNavSpecific (ScrollingNav.Msg Section)
+    | TimePassed Int
+    | AddSnackbar String
+    | ToggleDialog Bool
+
+
+init : () -> ( Model, Cmd Msg )
+init () =
+    let
+        ( scrollingNav, cmd ) =
+            ScrollingNav.init
+                { labels =
+                    \section ->
+                        case section of
+                            ComponentViews ->
+                                "Component Views"
+                            ReusableViews ->
+                                "Reusable Views"
+
+                            StatelessViews ->
+                                "Stateless Views"
+                , arrangement = [ StatelessViews, ReusableViews, ComponentViews ]
+                }
+    in
+    ({ component = Component.init
+      , stateless = Stateless.init
+      , reusable = Reusable.init
+      , scrollingNav = scrollingNav
+      , snackbar = Snackbar.init
+      , displayDialog = False
+      }
+    
+    , cmd |> Cmd.map ScrollingNavSpecific
+    )
+
+
+
+
+view : Model -> Html Msg
+view model =
+    [ Element.el [ Element.height <| Element.px <| 42 ] <| Element.none
+    , [ Element.el Heading.h1 <| Element.text "Elm-Ui-Widgets"
+        , model.scrollingNav
+            |> ScrollingNav.view
+                (\section ->
+                    case section of
+                        ComponentViews ->
+                            model.component
+                            |> Component.view
+                            |> Element.map ComponentSpecific
+                        ReusableViews ->
+                            Reusable.view 
+                                {addSnackbar = AddSnackbar
+                                ,model = model.reusable 
+                                ,msgMapper = ReusableSpecific
+                                }
+                                
+                        StatelessViews ->
+                            Stateless.view
+                                { msgMapper = StatelessSpecific
+                                , showDialog = ToggleDialog True
+                                }
+                                model.stateless
+                )
+        ]
+        |> Element.column Framework.container
+    ]
+        |> Element.column Grid.compact
+        |> Framework.responsiveLayout
+            [ Element.inFront <|
+                (model.scrollingNav
+                    |> ScrollingNav.viewSections
+                        { fromString =
+                            \string ->
+                                case string of
+                                    "Component Views" ->
+                                        Just ComponentViews
+                                    "Reusable Views" ->
+                                        Just ReusableViews
+
+                                    "Stateless Views" ->
+                                        Just StatelessViews
+
+                                    _ ->
+                                        Nothing
+                        , label = Element.text 
+                        , msgMapper = ScrollingNavSpecific
+                        }
+                    |> Widget.select
+                    |> List.map (\(config,selected) ->
+                    Input.button (Button.simple
+                                        ++ Group.center
+                                         ++ (if selected then
+                                                Color.primary
+
+                                            else
+                                                Color.dark
+                                           ))
+                                           config
+                    )
+                    |> Element.row
+                        (Color.dark ++ Grid.compact)
+                    |> Element.el
+                        (Framework.container
+                            ++ [ Element.padding <| 0
+                               , Element.centerX
+                               ]
+                        )
+                    |> Element.el
+                        (Color.dark
+                            ++ [ Element.alignTop
+                               , Element.height <| Element.px <| 42
+                               , Element.width <| Element.fill
+                               ]
+                        )
+                )
+            , Element.inFront <|
+                ( model.snackbar
+                    |> Snackbar.current
+                    |> Maybe.map
+                        (Element.text >>
+                            List.singleton >>
+                            Element.paragraph (Card.simple ++ Color.dark)
+                            >> Element.el [Element.padding 8,Element.alignBottom
+                            , Element.alignRight]
+
+
+                        )
+                    |> Maybe.withDefault Element.none
+                )
+            , Element.inFront <|
+                if model.displayDialog then
+                    Widget.dialog {
+                        onDismiss = Just <| ToggleDialog False
+                        ,content =
+                            [ Element.el Heading.h3 <| Element.text "Dialog"
+                            , "This is a dialog window"
+                            |> Element.text 
+                            |> List.singleton
+                            |> Element.paragraph []
+                            , Input.button (Button.simple ++ [Element.alignRight])
+                                {onPress = Just <| ToggleDialog False
+                                , label = Element.text "Ok"
+                                }
+                            ]
+                            |>Element.column (Grid.simple ++ Card.large)
+             } else Element.none
+            ]
+        
+
+
+update : Msg -> Model -> ( Model, Cmd Msg )
+update msg model =
+    case msg of
+        ComponentSpecific m ->
+            model.component
+                |> Component.update m
+                |> Tuple.mapBoth
+                    (\component ->
+                        { model
+                            | component = component
+                        }
+                    )
+                    (Cmd.map ComponentSpecific)
+
+        ReusableSpecific m ->
+            (model.reusable
+                |> Reusable.update m
+                |> (\reusable ->
+                        { model
+                            | reusable = reusable
+                        }
+                    ),Cmd.none)
+        
+        StatelessSpecific m ->
+            model.stateless
+                |> Stateless.update m
+                |> Tuple.mapBoth
+                    (\stateless ->
+                        { model
+                            | stateless = stateless
+                        }
+                    )
+                    (Cmd.map StatelessSpecific)
+        
+
+        ScrollingNavSpecific m ->
+            model.scrollingNav
+                |> ScrollingNav.update m
+                |> Tuple.mapBoth
+                    (\scrollingNav ->
+                        { model
+                            | scrollingNav = scrollingNav
+                        }
+                    )
+                    (Cmd.map ScrollingNavSpecific)
+    
+        TimePassed int ->
+            ({ model
+            | snackbar = model.snackbar |> Snackbar.timePassed int
+            },Cmd.none)
+        
+        AddSnackbar string ->
+            ( { model | snackbar = model.snackbar |> Snackbar.insert string}
+            , Cmd.none
+            )
+        
+        ToggleDialog bool ->
+            ( { model | displayDialog = bool }
+            , Cmd.none 
+            )
+
+subscriptions : Model -> Sub Msg
+subscriptions model=
+    Sub.batch
+    [ScrollingNav.subscriptions
+        |> Sub.map ScrollingNavSpecific
+    , Time.every 500 (always ( TimePassed 500))
+    ]
+
+
+main : Program () Model Msg
+main =
+    Browser.element
+        { init = init
+        , view = view
+        , update = update
+        , subscriptions = subscriptions
+        }
diff --git a/example/src/Reusable.elm b/example/src/Reusable.elm
new file mode 100644
index 0000000..defe47d
--- /dev/null
+++ b/example/src/Reusable.elm
@@ -0,0 +1,144 @@
+module Reusable exposing (Model,Msg,init,view,update)
+
+import Browser
+import Element exposing (Element,Color)
+import Element.Input as Input
+import Element.Background as Background
+import Framework
+import Framework.Button as Button
+import Framework.Card as Card
+import Framework.Color as Color
+import Framework.Grid as Grid
+import Framework.Group as Group
+import Framework.Heading as Heading
+import Framework.Input as Input
+import Framework.Tag as Tag
+import Html exposing (Html)
+import Html.Attributes as Attributes
+import Set exposing (Set)
+import Widget
+import Widget.FilterSelect as FilterSelect
+import Widget.ScrollingNav as ScrollingNav
+import Widget.ValidatedInput as ValidatedInput
+import Widget.Snackbar as Snackbar
+import Widget.SortTable as SortTable
+import Time
+import Heroicons.Solid as Heroicons
+import Element.Font as Font
+
+type alias Model =
+    SortTable.Model
+
+type Msg =
+    SortBy {title : String, asc : Bool }
+
+type alias Item =
+    {name : String
+    ,amount : Int
+    ,price : Float
+    }
+
+update : Msg -> Model -> Model
+update msg model =
+    case msg of
+        SortBy m ->
+            m
+
+init : Model
+init =
+    SortTable.sortBy {title="Name",asc=True}
+
+snackbar : (String -> msg) -> Element msg
+snackbar addSnackbar =
+    [ Element.el Heading.h3 <| Element.text "Snackbar"
+    , Input.button Button.simple
+        { onPress = Just <| addSnackbar "This is a notification. It will disappear after 10 seconds."
+        , label = "Add Notification"
+            |> Element.text
+            |> List.singleton
+            |> Element.paragraph []
+        }
+    ]
+    |> Element.column (Grid.simple ++ Card.small)
+
+sortTable : SortTable.Model -> Element Msg
+sortTable model =
+    [ Element.el Heading.h3 <| Element.text "Sort Table"
+    , SortTable.view
+        { content =
+            [ {id = 1, name = "Antonio", rating = 2.456}
+            , {id = 2, name = "Ana", rating = 1.34}
+            , {id = 3, name = "Alfred", rating = 4.22}
+            , {id = 4, name = "Thomas", rating = 3 }
+            ]
+        , columns =
+            [ SortTable.intColumn 
+                { title = "Id"
+                , value = .id
+                , toString = \int -> "#" ++ String.fromInt int
+                }
+            , SortTable.stringColumn
+                { title = "Name"
+                , value = .name
+                , toString = identity
+                }
+            , SortTable.floatColumn
+                { title = "rating"
+                , value = .rating
+                , toString = String.fromFloat
+                }
+            ]
+        , model = model
+        }
+        |> (\{data,columns} ->
+            {data = data
+            ,columns = columns
+                |> List.map (\config->
+                        { header =
+                            Input.button [Font.bold]
+                                { onPress = {title = config.header
+                                    ,asc = if config.header == model.title then
+                                        not model.asc
+                                        else
+                                        True
+                                        }|> SortBy |> Just
+                                        , label = 
+                            if config.header == model.title then
+                                [config.header |> Element.text
+                                , Element.html <|if model.asc then
+                                    Heroicons.cheveronUp [Attributes.width 16]
+                                
+                                    else
+                                    Heroicons.cheveronDown [Attributes.width 16]
+                                ]
+                                |> Element.row (Grid.simple ++ [Font.bold])
+                            else
+                                
+                                config.header  |> Element.text 
+                                }
+                                
+                        , view = config.view >> Element.text
+                        , width = Element.fill
+                        }
+                    )
+            })
+        |> Element.table Grid.simple
+    ]
+    |> Element.column (Grid.simple ++ Card.small)
+
+view : { addSnackbar : String -> msg
+    , msgMapper : Msg -> msg
+    , model : Model } -> Element msg
+view {addSnackbar,msgMapper,model} =
+    Element.column (Grid.section ++ [Element.centerX])
+        [ Element.el Heading.h2 <| Element.text "Reusable Views"
+        , "Reusable views have an internal state but no update function. You will need to do some wiring, but nothing complicated."
+            |> Element.text
+            |> List.singleton
+            |> Element.paragraph []
+        , Element.wrappedRow (Grid.simple ++ [Element.centerX])
+        <|
+            [ snackbar addSnackbar
+            , sortTable model |> Element.map msgMapper
+            ]
+        ]
\ No newline at end of file
diff --git a/example/src/Stateless.elm b/example/src/Stateless.elm
new file mode 100644
index 0000000..8eb707b
--- /dev/null
+++ b/example/src/Stateless.elm
@@ -0,0 +1,219 @@
+module Stateless exposing (Model,Msg,init,update,view)
+
+import Element exposing (Element)
+import Element.Input as Input
+import Element.Background as Background
+import Set exposing (Set)
+import Framework.Grid as Grid
+import Framework.Button as Button
+import Framework.Card as Card
+import Framework.Color as Color
+import Framework.Group as Group
+import Framework.Heading as Heading
+import Framework.Input as Input
+import Framework.Tag as Tag
+import Heroicons.Solid  as Heroicons
+import Widget
+import Html exposing (Html)
+import Html.Attributes as Attributes
+import Array exposing (Array)
+
+type alias Model =
+    { selected : Maybe Int
+    , multiSelected : Set Int
+    , isCollapsed : Bool
+    , carousel : Int
+    }
+
+type Msg =
+    ChangedSelected Int
+    | ChangedMultiSelected Int
+    | ToggleCollapsable Bool
+    | SetCarousel Int
+
+init : Model
+init =
+    { selected = Nothing
+    , multiSelected = Set.empty
+    , isCollapsed = False
+    , carousel = 0
+    }
+
+update : Msg -> Model -> (Model,Cmd Msg)
+update msg model = 
+    case msg of
+        ChangedSelected int ->
+            ( { model
+                | selected = Just int
+              }
+            , Cmd.none
+            )
+        
+        ChangedMultiSelected int ->
+            ( { model
+                | multiSelected = 
+                     model.multiSelected |> 
+                     if model.multiSelected |> Set.member int then
+                       Set.remove int
+                    else
+                        Set.insert int
+                }
+            , Cmd.none )
+        
+        ToggleCollapsable bool ->
+            ( { model 
+                | isCollapsed = bool
+                }
+                , Cmd.none)
+        
+        SetCarousel int ->
+            ( if (int < 0) || (int > 3) then
+                model
+              else
+                {model
+                | carousel = int
+                }
+            , Cmd.none
+            )
+
+select : Model -> Element Msg
+select model =
+    [ Element.el Heading.h3 <| Element.text "Select"
+    , Widget.select
+        { selected = model.selected
+        , options = [ 1, 2, 42 ]
+        , label = String.fromInt >> Element.text
+        , onChange = ChangedSelected
+        }
+        |> List.indexedMap (\i (config,selected)->
+            Input.button 
+            (Button.simple
+            ++ (if i == 0 then
+                    Group.left
+                else if i == 2 then
+                    Group.right
+                else
+                    Group.center)
+            ++ (if selected then
+                                Color.primary
+
+                            else
+                                []
+                           )
+            )
+            config
+        )
+        |> Element.row Grid.compact
+    ]
+        |> Element.column (Grid.simple ++ Card.small)
+
+multiSelect : Model -> Element Msg
+multiSelect model =
+    [ Element.el Heading.h3 <| Element.text "Multi Select"
+    , Widget.multiSelect
+        { selected = model.multiSelected
+        , options = [ 1, 2, 42 ]
+        , label = String.fromInt >> Element.text
+        , onChange = ChangedMultiSelected
+        }
+        |> List.indexedMap (\i (config,selected)->
+            Input.button 
+            (Button.simple
+            ++ (if i == 0 then
+                    Group.left
+                else if i == 2 then
+                    Group.right
+                else
+                    Group.center)
+            ++ (if selected then
+                                Color.primary
+
+                            else
+                                []
+                           )
+            )
+            config
+        )
+        |> Element.row Grid.compact
+    ]
+        |> Element.column (Grid.simple ++ Card.small)
+    
+
+collapsable : Model -> Element Msg
+collapsable model =
+    [Element.el Heading.h3 <| Element.text "Collapsable"
+    ,Widget.collapsable
+        {onToggle = ToggleCollapsable
+        ,isCollapsed = model.isCollapsed
+        ,label = Element.row Grid.compact
+            [ Element.html <| 
+                if model.isCollapsed then 
+                    Heroicons.cheveronRight  [ Attributes.width 20] 
+                else 
+                    Heroicons.cheveronDown [ Attributes.width 20] 
+            , Element.el Heading.h4 <|Element.text <| "Title"
+            ]
+        ,content = Element.text <| "Hello World"
+        }
+    ]
+    |> Element.column (Grid.simple ++ Card.small)
+
+dialog : msg -> Model -> Element msg
+dialog showDialog model =
+    [ Element.el Heading.h3 <| Element.text "Dialog"
+    , Input.button Button.simple
+        { onPress = Just showDialog
+        , label = Element.text <| "Show Dialog"
+        }
+    ]
+    |> Element.column (Grid.simple ++ Card.small)
+
+carousel : Model -> Element Msg
+carousel model =
+    [ Element.el Heading.h3 <| Element.text "Carousel"
+    , Widget.carousel
+            {content = (Color.cyan,[Color.yellow, Color.green , Color.red ]|> Array.fromList)
+            ,current = model.carousel
+            , label = \c ->
+                [ Input.button [Element.centerY]
+                    { onPress = Just <| SetCarousel <| model.carousel - 1
+                    , label = Heroicons.cheveronLeft [Attributes.width 20]
+                        |> Element.html
+                    }
+                , Element.el 
+                    (Card.simple
+                    ++  [ Background.color <| c
+                        , Element.height <| Element.px <| 100
+                        , Element.width <| Element.px <| 100
+                        ]
+                    ) <| Element.none
+                , Input.button [Element.centerY]
+                    { onPress = Just <| SetCarousel <| model.carousel + 1
+                    , label = Heroicons.cheveronRight [Attributes.width 20]
+                    |> Element.html
+                    }
+                ]
+                |> Element.row (Grid.simple ++ [Element.centerX, Element.width<| Element.shrink])
+            }
+    ]
+    |> Element.column (Grid.simple ++ Card.small)
+    
+
+view : { msgMapper : Msg -> msg, showDialog : msg} -> Model -> Element msg
+view {msgMapper,showDialog} model =
+    Element.column (Grid.section)
+        [ Element.el Heading.h2 <| Element.text "Stateless Views"
+        , "Stateless views are simple functions that view some content. No wiring required."
+            |> Element.text
+            |> List.singleton
+            |> Element.paragraph []
+        , Element.wrappedRow
+            Grid.simple
+            <|
+            [ select model |> Element.map msgMapper
+            , multiSelect model |> Element.map msgMapper
+            , collapsable model |> Element.map msgMapper
+            , dialog showDialog model
+            , carousel model |> Element.map msgMapper
+            ]
+        ]
\ No newline at end of file
diff --git a/src/Widget.elm b/src/Widget.elm
new file mode 100644
index 0000000..165e1fa
--- /dev/null
+++ b/src/Widget.elm
@@ -0,0 +1,247 @@
+module Widget exposing (select, multiSelect, collapsable, carousel, dialog)
+
+{-| This module contains functions for displaying data.
+
+@docs select, multiSelect, collapsable, carousel, dialog
+
+-}
+
+import Array exposing (Array)
+import Element exposing (Element)
+import Element.Background as Background
+import Element.Events as Events
+import Element.Input as Input
+import Set exposing (Set)
+
+
+{-| Selects one out of multiple options. This can be used for radio buttons or Menus.
+
+```
+    Widget.select
+        { selected = model.selected
+        , options = [ 1, 2, 42 ]
+        , label = String.fromInt >> Element.text
+        , onChange = ChangedSelected
+        }
+        |> List.map (\(config,selected)->
+            Input.button (if selected then [Font.bold] else []) config
+          )
+        |> Element.row []
+```
+
+-}
+select :
+    { selected : Maybe a
+    , options : List a
+    , label : a -> Element msg
+    , onChange : a -> msg
+    }
+    ->
+        List
+            ( { label : Element msg
+              , onPress : Maybe msg
+              }
+            , Bool
+            )
+select { selected, options, label, onChange } =
+    options
+        |> List.map
+            (\a ->
+                ( { onPress = a |> onChange |> Just
+                  , label = label a
+                  }
+                , selected == Just a
+                )
+            )
+
+
+{-| Selects multible options. This can be used for checkboxes.
+
+```
+    Widget.multiSelect
+        { selected = model.multiSelected
+        , options = [ 1, 2, 42 ]
+        , label = String.fromInt >> Element.text
+        , onChange = ChangedMultiSelected
+        }
+        |> List.map (\(config,selected)->
+            Input.button
+                (if selected then
+                    [Font.bold]
+
+                else
+                    []
+                )
+                config
+        )
+        |> Element.row []
+```
+
+-}
+multiSelect :
+    { selected : Set comparable
+    , options : List comparable
+    , label : comparable -> Element msg
+    , onChange : comparable -> msg
+    }
+    ->
+        List
+            ( { label : Element msg
+              , onPress : Maybe msg
+              }
+            , Bool
+            )
+multiSelect { selected, options, label, onChange } =
+    options
+        |> List.map
+            (\a ->
+                ( { onPress = a |> onChange |> Just
+                  , label =
+                        label a
+                  }
+                , selected |> Set.member a
+                )
+            )
+
+
+{-| Some collapsable content.
+
+```
+    Widget.collapsable
+        {onToggle = ToggleCollapsable
+        ,isCollapsed = model.isCollapsed
+        ,label = Element.row Grid.compact
+            [ Element.html <|
+                if model.isCollapsed then
+                    Heroicons.cheveronRight  [ Attributes.width 20]
+                else
+                    Heroicons.cheveronDown [ Attributes.width 20]
+            , Element.el Heading.h4 <|Element.text <| "Title"
+            ]
+        ,content = Element.text <| "Hello World"
+        }
+```
+
+-}
+collapsable :
+    { onToggle : Bool -> msg
+    , isCollapsed : Bool
+    , label : Element msg
+    , content : Element msg
+    }
+    -> Element msg
+collapsable { onToggle, isCollapsed, label, content } =
+    Element.column [] <|
+        [ Input.button []
+            { onPress = Just <| onToggle <| not isCollapsed
+            , label = label
+            }
+        ]
+            ++ (if isCollapsed then
+                    []
+
+                else
+                    [ content ]
+               )
+
+
+{-| A dialog element displaying important information.
+
+```
+    Framework.Layout
+        [ Element.inFront <|
+            if model.displayDialog then
+                Widget.dialog
+                    { onDismiss = Just <| ToggleDialog False
+                    , content =
+                        [ "This is a dialog window"
+                            |> Element.text
+                        , Input.button []
+                            {onPress = Just <| ToggleDialog False
+                            , label = Element.text "Ok"
+                            }
+                        ]
+                        |> Element.column []
+                    }
+            else Element.none
+        ] <|
+            Element.text "some Content"
+```
+
+-}
+dialog :
+    { onDismiss : Maybe msg
+    , content : Element msg
+    }
+    -> Element msg
+dialog { onDismiss, content } =
+    content
+        |> Element.el
+            [ Element.centerX
+            , Element.centerY
+            ]
+        |> Element.el
+            ([ Element.width <| Element.fill
+             , Element.height <| Element.fill
+             , Background.color <| Element.rgba255 0 0 0 0.5
+             ]
+                ++ (onDismiss
+                        |> Maybe.map (Events.onClick >> List.singleton)
+                        |> Maybe.withDefault []
+                   )
+            )
+
+
+{-| A Carousel circles through a non empty list of contents.
+
+```
+    Widget.carousel
+        {content = ("Blue",["Yellow", "Green" , "Red" ]|> Array.fromList)
+        ,current = model.carousel
+        , label = \c ->
+            [ Input.button [Element.centerY]
+                { onPress = Just <|
+                     SetCarousel <|
+                        (\x -> if x < 0 then 0 else x) <|
+                            model.carousel - 1
+                , label = "<" |> Element.text
+                }
+            , c |> Element.text
+            , Input.button [Element.centerY]
+                { onPress = Just <|
+                    SetCarousel <|
+                        (\x -> if x > 3 then 3 else x) <|
+                        model.carousel + 1
+                , label = ">" |> Element.text
+                }
+            ]
+            |> Element.row [Element.centerX, Element.width<| Element.shrink]
+        }
+```
+
+-}
+carousel :
+    { content : ( a, Array a )
+    , current : Int
+    , label : a -> Element msg
+    }
+    -> Element msg
+carousel { content, current, label } =
+    let
+        ( head, tail ) =
+            content
+    in
+    (if current <= 0 then
+        head
+
+     else if current > Array.length tail then
+        tail
+            |> Array.get (Array.length tail - 1)
+            |> Maybe.withDefault head
+
+     else
+        tail
+            |> Array.get (current - 1)
+            |> Maybe.withDefault head
+    )
+        |> label
diff --git a/src/Widget/FilterSelect.elm b/src/Widget/FilterSelect.elm
new file mode 100644
index 0000000..ea0a734
--- /dev/null
+++ b/src/Widget/FilterSelect.elm
@@ -0,0 +1,93 @@
+module Widget.FilterSelect exposing (Model, Msg(..), init, update, viewInput, viewOptions)
+
+{-|
+
+@docs Model, Msg, init, update, viewInput, viewOptions
+
+-}
+
+import Element exposing (Attribute, Element)
+import Element.Input as Input exposing (Placeholder)
+import Set exposing (Set)
+
+
+{-| The Model
+-}
+type alias Model =
+    { raw : String
+    , selected : Maybe String
+    , options : Set String
+    }
+
+
+{-| The Msg is exposed by design. You can unselect by sending `Selected Nothing`.
+-}
+type Msg
+    = ChangedRaw String
+    | Selected (Maybe String)
+
+
+{-| The initial state contains the set of possible options.
+-}
+init : Set String -> Model
+init options =
+    { raw = ""
+    , selected = Nothing
+    , options = options
+    }
+
+
+{-| Updates the Model
+-}
+update : Msg -> Model -> Model
+update msg model =
+    case msg of
+        ChangedRaw string ->
+            { model
+                | raw = string
+            }
+
+        Selected maybe ->
+            { model
+                | selected = maybe
+            }
+                |> (case maybe of
+                        Just string ->
+                            \m -> { m | raw = string }
+
+                        Nothing ->
+                            identity
+                   )
+
+
+{-| A wrapper around Input.text.
+-}
+viewInput :
+    List (Attribute msg)
+    -> Model
+    ->
+        { msgMapper : Msg -> msg
+        , placeholder : Maybe (Placeholder msg)
+        , label : String
+        }
+    -> Element msg
+viewInput attributes model { msgMapper, placeholder, label } =
+    Input.text attributes
+        { onChange = ChangedRaw >> msgMapper
+        , text = model.raw
+        , placeholder = placeholder
+        , label = Input.labelHidden label
+        }
+
+
+{-| Returns a List of all options that matches the filter.
+-}
+viewOptions : Model -> List String
+viewOptions { raw, options } =
+    if raw == "" then
+        []
+
+    else
+        options
+            |> Set.filter (String.toUpper >> String.contains (raw |> String.toUpper))
+            |> Set.toList
diff --git a/src/Widget/ScrollingNav.elm b/src/Widget/ScrollingNav.elm
new file mode 100644
index 0000000..3286711
--- /dev/null
+++ b/src/Widget/ScrollingNav.elm
@@ -0,0 +1,195 @@
+module Widget.ScrollingNav exposing
+    ( Model, Msg, init, update, subscriptions, view, viewSections
+    , jumpTo, syncPositions
+    )
+
+{-| The Scrolling Nav is a navigation bar thats updates while you scroll through
+the page. Clicking on a navigation button will scroll directly to that section.
+
+
+# Basics
+
+@docs Model, Msg, init, update, subscriptions, view, viewSections
+
+
+# Operations
+
+@docs jumpTo, syncPositions
+
+-}
+
+import Browser.Dom as Dom
+import Element exposing (Element)
+import Framework.Grid as Grid
+import Html.Attributes as Attributes
+import IntDict exposing (IntDict)
+import Task
+import Time
+
+
+{-| -}
+type alias Model elem =
+    { labels : elem -> String
+    , positions : IntDict String
+    , arrangement : List elem
+    , scrollPos : Int
+    }
+
+
+{-| -}
+type Msg elem
+    = GotHeaderPos elem (Result Dom.Error Int)
+    | ChangedViewport (Result Dom.Error ())
+    | SyncPosition Int
+    | JumpTo elem
+    | TimePassed
+
+
+{-| -}
+init :
+    { labels : elem -> String
+    , arrangement : List elem
+    }
+    -> ( Model elem, Cmd (Msg elem) )
+init { labels, arrangement } =
+    { labels = labels
+    , positions = IntDict.empty
+    , arrangement = arrangement
+    , scrollPos = 0
+    }
+        |> (\a ->
+                ( a
+                , syncPositions a
+                )
+           )
+
+
+{-| -}
+update : Msg elem -> Model elem -> ( Model elem, Cmd (Msg elem) )
+update msg model =
+    case msg of
+        GotHeaderPos label result ->
+            ( case result of
+                Ok pos ->
+                    { model
+                        | positions =
+                            model.positions
+                                |> IntDict.insert pos
+                                    (label |> model.labels)
+                    }
+
+                Err _ ->
+                    model
+            , Cmd.none
+            )
+
+        ChangedViewport _ ->
+            ( model, Cmd.none )
+
+        SyncPosition pos ->
+            ( { model
+                | scrollPos = pos
+              }
+            , Cmd.none
+            )
+
+        TimePassed ->
+            ( model
+            , Dom.getViewport
+                |> Task.map (.viewport >> .y >> round)
+                |> Task.perform SyncPosition
+            )
+
+        JumpTo elem ->
+            ( model
+            , model
+                |> jumpTo elem
+            )
+
+
+{-| -}
+subscriptions : Sub (Msg msg)
+subscriptions =
+    Time.every 1000 (always TimePassed)
+
+
+{-| -}
+jumpTo : elem -> Model elem -> Cmd (Msg msg)
+jumpTo section { labels } =
+    Dom.getElement (section |> labels)
+        |> Task.andThen
+            (\{ element } ->
+                Dom.setViewport 0 element.y
+            )
+        |> Task.attempt ChangedViewport
+
+
+{-| -}
+syncPositions : Model elem -> Cmd (Msg elem)
+syncPositions { labels, arrangement } =
+    arrangement
+        |> List.map
+            (\label ->
+                Dom.getElement (labels label)
+                    |> Task.map
+                        (.element
+                            >> .y
+                            >> round
+                        )
+                    |> Task.attempt
+                        (GotHeaderPos label)
+            )
+        |> Cmd.batch
+
+
+{-| -}
+viewSections :
+    { label : String -> Element msg
+    , fromString : String -> Maybe elem
+    , msgMapper : Msg elem -> msg
+    }
+    -> Model elem
+    ->
+        { selected : Maybe elem
+        , options : List elem
+        , label : elem -> Element msg
+        , onChange : elem -> msg
+        }
+viewSections { label, fromString, msgMapper } { arrangement, scrollPos, labels, positions } =
+    let
+        current =
+            positions
+                |> IntDict.before (scrollPos + 1)
+                |> Maybe.map Just
+                |> Maybe.withDefault (positions |> IntDict.after (scrollPos + 1))
+                |> Maybe.map Tuple.second
+                |> Maybe.andThen fromString
+    in
+    { selected = current
+    , options = arrangement
+    , label = \elem -> label (elem |> labels)
+    , onChange = JumpTo >> msgMapper
+    }
+
+
+{-| -}
+view :
+    (elem -> Element msg)
+    -> Model elem
+    -> Element msg
+view asElement { labels, arrangement } =
+    arrangement
+        |> List.map
+            (\header ->
+                Element.el
+                    [ header
+                        |> labels
+                        |> Attributes.id
+                        |> Element.htmlAttribute
+                    , Element.width <| Element.fill
+                    ]
+                <|
+                    asElement <|
+                        header
+            )
+        |> Element.column Grid.simple
diff --git a/src/Widget/Snackbar.elm b/src/Widget/Snackbar.elm
new file mode 100644
index 0000000..aa2eadc
--- /dev/null
+++ b/src/Widget/Snackbar.elm
@@ -0,0 +1,94 @@
+module Widget.Snackbar exposing
+    ( Model, init, current, timePassed
+    , insert, insertFor, dismiss
+    )
+
+{-| A [snackbar](https://material.io/components/snackbars/) shows notification, one at a time.
+
+
+# Basics
+
+@docs Model, init, current, timePassed
+
+
+# Operations
+
+@docs insert, insertFor, dismiss
+
+-}
+
+import Queue exposing (Queue)
+
+
+{-| A snackbar has a queue of Notifications, each with the amount of ms the message should be displayed
+-}
+type alias Model a =
+    { queue : Queue ( a, Int )
+    , current : Maybe ( a, Int )
+    }
+
+
+{-| Inital state
+-}
+init : Model a
+init =
+    { queue = Queue.empty
+    , current = Nothing
+    }
+
+
+{-| Insert a message that will last for 10 seconds.
+-}
+insert : a -> Model a -> Model a
+insert =
+    insertFor 10000
+
+
+{-| Insert a message for a specific amount of milli seconds.
+-}
+insertFor : Int -> a -> Model a -> Model a
+insertFor removeIn a model =
+    case model.current of
+        Nothing ->
+            { model | current = Just ( a, removeIn ) }
+
+        Just _ ->
+            { model | queue = model.queue |> Queue.enqueue ( a, removeIn ) }
+
+
+{-| Dismiss the current message.
+-}
+dismiss : Model a -> Model a
+dismiss model =
+    { model | current = Nothing }
+
+
+{-| Updates the model. This functions should be called regularly.
+The first argument is the milli seconds that passed since the last time the function was called.
+-}
+timePassed : Int -> Model a -> Model a
+timePassed ms model =
+    case model.current of
+        Nothing ->
+            let
+                ( c, queue ) =
+                    model.queue |> Queue.dequeue
+            in
+            { model
+                | current = c
+                , queue = queue
+            }
+
+        Just ( _, removeIn ) ->
+            if removeIn <= ms then
+                model |> dismiss
+
+            else
+                { model | current = model.current |> Maybe.map (Tuple.mapSecond ((+) -ms)) }
+
+
+{-| Returns the current element.
+-}
+current : Model a -> Maybe a
+current model =
+    model.current |> Maybe.map Tuple.first
diff --git a/src/Widget/SortTable.elm b/src/Widget/SortTable.elm
new file mode 100644
index 0000000..719d157
--- /dev/null
+++ b/src/Widget/SortTable.elm
@@ -0,0 +1,219 @@
+module Widget.SortTable exposing
+    ( Model, init, view, sortBy
+    , intColumn, floatColumn, stringColumn
+    )
+
+{-| A Sortable list allows you to sort coulmn.
+
+```
+    SortTable.view
+        { content =
+            [ {id = 1, name = "Antonio", rating = 2.456}
+            , {id = 2, name = "Ana", rating = 1.34}
+            , {id = 3, name = "Alfred", rating = 4.22}
+            , {id = 4, name = "Thomas", rating = 3 }
+            ]
+        , columns =
+            [ SortTable.intColumn
+                { title = "Id"
+                , value = .id
+                , toString = \int -> "#" ++ String.fromInt int
+                }
+            , SortTable.stringColumn
+                { title = "Name"
+                , value = .name
+                , toString = identity
+                }
+            , SortTable.floatColumn
+                { title = "rating"
+                , value = .rating
+                , toString = String.fromFloat
+                }
+            ]
+        , model = model
+        }
+        |> (\{data,columns} ->
+            {data = data
+            ,columns = columns
+                |> List.map (\config->
+                        { header =
+                            Input.button [Font.bold]
+                                { onPress =
+                                    { title = config.header
+                                    , asc =
+                                        if config.header == model.title then
+                                            not model.asc
+                                        else
+                                            True
+                                    }
+                                        |> SortBy
+                                        |> Just
+                                , label =
+                                    if config.header == model.title then
+                                        [ config.header |> Element.text
+                                        , Element.text <|
+                                            if model.asc then
+                                                "/\"
+                                            else
+                                                "\/"
+                                        ]
+                                            |> Element.row [Font.bold]
+                                    else
+                                        config.header  |> Element.text
+                                }
+                        , view = config.view >> Element.text
+                        , width = Element.fill
+                        }
+                    )
+            })
+        |> Element.table []
+```
+
+
+# Basics
+
+@docs Model, init, view, sortBy
+
+
+# Columns
+
+@docs intColumn, floatColumn, stringColumn
+
+-}
+
+
+type ColumnType a
+    = StringColumn { value : a -> String, toString : String -> String }
+    | IntColumn { value : a -> Int, toString : Int -> String }
+    | FloatColumn { value : a -> Float, toString : Float -> String }
+
+
+{-| The Model contains the sorting column name and if ascending or descending.
+-}
+type alias Model =
+    { title : String
+    , asc : Bool
+    }
+
+
+type alias Column a =
+    { title : String
+    , content : ColumnType a
+    }
+
+
+{-| The initial State setting the sorting column name to the empty string.
+-}
+init : Model
+init =
+    { title = "", asc = True }
+
+
+{-| A Column containing a Int
+-}
+intColumn : { title : String, value : a -> Int, toString : Int -> String } -> Column a
+intColumn { title, value, toString } =
+    { title = title
+    , content = IntColumn { value = value, toString = toString }
+    }
+
+
+{-| A Column containing a Float
+-}
+floatColumn : { title : String, value : a -> Float, toString : Float -> String } -> Column a
+floatColumn { title, value, toString } =
+    { title = title
+    , content = FloatColumn { value = value, toString = toString }
+    }
+
+
+{-| A Column containing a String
+-}
+stringColumn : { title : String, value : a -> String, toString : String -> String } -> Column a
+stringColumn { title, value, toString } =
+    { title = title
+    , content = StringColumn { value = value, toString = toString }
+    }
+
+
+{-| Change the sorting criteras.
+
+```
+    sortBy =
+        identity
+```
+
+-}
+sortBy : { title : String, asc : Bool } -> Model
+sortBy =
+    identity
+
+
+{-| The View
+-}
+view :
+    { content : List a
+    , columns : List (Column a)
+    , model : Model
+    }
+    ->
+        { data : List a
+        , columns : List { header : String, view : a -> String }
+        }
+view { content, columns, model } =
+    let
+        findTitle : List (Column a) -> Maybe (ColumnType a)
+        findTitle list =
+            case list of
+                [] ->
+                    Nothing
+
+                head :: tail ->
+                    if head.title == model.title then
+                        Just head.content
+
+                    else
+                        findTitle tail
+    in
+    { data =
+        content
+            |> (columns
+                    |> findTitle
+                    |> Maybe.map
+                        (\c ->
+                            case c of
+                                StringColumn { value } ->
+                                    List.sortBy value
+
+                                IntColumn { value } ->
+                                    List.sortBy value
+
+                                FloatColumn { value } ->
+                                    List.sortBy value
+                        )
+                    |> Maybe.withDefault identity
+               )
+            |> (if model.asc then
+                    identity
+
+                else
+                    List.reverse
+               )
+    , columns =
+        columns
+            |> List.map
+                (\column ->
+                    { header = column.title
+                    , view =
+                        case column.content of
+                            IntColumn { value, toString } ->
+                                value >> toString
+
+                            FloatColumn { value, toString } ->
+                                value >> toString
+
+                            StringColumn { value, toString } ->
+                                value >> toString
+                    }
+                )
+    }
diff --git a/src/Widget/ValidatedInput.elm b/src/Widget/ValidatedInput.elm
new file mode 100644
index 0000000..980c579
--- /dev/null
+++ b/src/Widget/ValidatedInput.elm
@@ -0,0 +1,144 @@
+module Widget.ValidatedInput exposing
+    ( Model, Msg, init, update, view
+    , getError, getRaw, getValue
+    )
+
+{-| The validated Input is a wrapper around `Input.text`.
+They can validate the input and return an error if nessarry.
+
+
+# Basics
+
+@docs Model, Msg, init, update, view
+
+
+# Access the Model
+
+@docs getError, getRaw, getValue
+
+-}
+
+import Element exposing (Attribute, Element)
+import Element.Events as Events
+import Element.Input as Input exposing (Placeholder)
+
+
+{-| -}
+type Model err a
+    = Model
+        { raw : Maybe String
+        , value : a
+        , err : Maybe err
+        , validator : String -> Result err a
+        , toString : a -> String
+        }
+
+
+{-| -}
+getRaw : Model err a -> String
+getRaw (Model { raw, value, toString }) =
+    case raw of
+        Just string ->
+            string
+
+        Nothing ->
+            value |> toString
+
+
+{-| -}
+getValue : Model err a -> a
+getValue (Model { value }) =
+    value
+
+
+{-| -}
+getError : Model err a -> Maybe err
+getError (Model { err }) =
+    err
+
+
+{-| -}
+type Msg
+    = ChangedRaw String
+    | LostFocus
+    | StartEditing
+
+
+{-| -}
+init : { value : a, validator : String -> Result err a, toString : a -> String } -> Model err a
+init { validator, toString, value } =
+    Model
+        { raw = Nothing
+        , value = value
+        , err = Nothing
+        , validator = validator
+        , toString = toString
+        }
+
+
+{-| -}
+update : Msg -> Model err a -> Model err a
+update msg (Model model) =
+    case msg of
+        StartEditing ->
+            Model
+                { model
+                    | raw = model.value |> model.toString |> Just
+                }
+
+        ChangedRaw string ->
+            Model
+                { model
+                    | raw = Just string
+                    , err = Nothing
+                }
+
+        LostFocus ->
+            case model.raw of
+                Just string ->
+                    case model.validator string of
+                        Ok value ->
+                            Model
+                                { model
+                                    | value = value
+                                    , raw = Nothing
+                                    , err = Nothing
+                                }
+
+                        Err err ->
+                            Model
+                                { model
+                                    | raw = Nothing
+                                    , err = Just err
+                                }
+
+                Nothing ->
+                    Model model
+
+
+{-| -}
+view :
+    List (Attribute msg)
+    -> Model err a
+    ->
+        { msgMapper : Msg -> msg
+        , placeholder : Maybe (Placeholder msg)
+        , label : String
+        , readOnly : a -> Element msg
+        }
+    -> Element msg
+view attributes (Model model) { msgMapper, placeholder, label, readOnly } =
+    case model.raw of
+        Just string ->
+            Input.text (attributes ++ [ Events.onLoseFocus <| msgMapper <| LostFocus ])
+                { onChange = ChangedRaw >> msgMapper
+                , text = string
+                , placeholder = placeholder
+                , label = Input.labelHidden label
+                }
+
+        Nothing ->
+            Input.button []
+                { onPress = Just (StartEditing |> msgMapper)
+                , label = model.value |> readOnly
+                }