MackeyRMS / elm-ui / Element.Input

Input elements have a lot of constraints!

We want all of our input elements to:

While these three goals may seem pretty obvious, Html and CSS have made it surprisingly difficult to achieve!

And incredibly difficult for developers to remember all the tricks necessary to make things work. If you've every tried to make a <textarea> be the height of it's content or restyle a radio button while maintaining accessibility, you may be familiar.

This module is intended to be accessible by default. You shouldn't have to wade through docs, articles, and books to find out exactly how accessible your html actually is.

Focus Styling

All Elements can be styled on focus by using Element.focusStyle to set a global focus style or Element.focused to set a focus style individually for an element.

focusedOnLoad : Element.Attribute msg

Attach this attribute to any Input that you would like to be automatically focused when the page loads.

You should only have a maximum of one per page.

Buttons

button : List (Element.Attribute msg) -> { onPress : Maybe msg, label : Element msg } -> Element msg

A standard button.

The onPress handler will be fired either onClick or when the element is focused and the Enter key has been pressed.

import Element exposing (rgb255, text)
import Element.Background as Background
import Element.Input as Input

blue =
    Element.rgb255 238 238 238

myButton =
    Input.button
        [ Background.color blue
        , Element.focused
            [ Background.color purple ]
        ]
        { onPress = Just ClickMsg
        , label = text "My Button"
        }

Note If you have an icon button but want it to be accessible, consider adding a Region.description, which will describe the button to screen readers.

Checkboxes

A checkbox requires you to store a Bool in your model.

This is also the first input element that has a required label.

import Element exposing (text)
import Element.Input as Input

type Msg
    = GuacamoleChecked Bool

view model =
    Input.checkbox []
        { onChange = GuacamoleChecked
        , icon = Input.defaultCheckbox
        , checked = model.guacamole
        , label =
            Input.labelRight []
                (text "Do you want Guacamole?")
        }

checkbox : List (Element.Attribute msg) -> { onChange : Basics.Bool -> msg, icon : Basics.Bool -> Element msg, checked : Basics.Bool, label : Label msg } -> Element msg

defaultCheckbox : Basics.Bool -> Element msg

The blue default checked box icon.

You'll likely want to make your own checkbox at some point that fits your design.

Text

text : List (Element.Attribute msg) -> { onChange : String -> msg, text : String, placeholder : Maybe (Placeholder msg), label : Label msg } -> Element msg

multiline : List (Element.Attribute msg) -> { onChange : String -> msg, text : String, placeholder : Maybe (Placeholder msg), label : Label msg, spellcheck : Basics.Bool } -> Element msg

A multiline text input.

By default it will have a minimum height of one line and resize based on it's contents.

Use Element.spacing to change its line-height.


type Placeholder msg

placeholder : List (Element.Attribute msg) -> Element msg -> Placeholder msg

Text with autofill

If we want to play nicely with a browser's ability to autofill a form, we need to be able to give it a hint about what we're expecting.

The following inputs are very similar to Input.text, but they give the browser a hint to allow autofill to work correctly.

username : List (Element.Attribute msg) -> { onChange : String -> msg, text : String, placeholder : Maybe (Placeholder msg), label : Label msg } -> Element msg

newPassword : List (Element.Attribute msg) -> { onChange : String -> msg, text : String, placeholder : Maybe (Placeholder msg), label : Label msg, show : Basics.Bool } -> Element msg

A password input that allows the browser to autofill.

It's newPassword instead of just password because it gives the browser a hint on what type of password input it is.

A password takes all the arguments a normal Input.text would, and also show, which will remove the password mask (e.g. **** vs pass1234)

currentPassword : List (Element.Attribute msg) -> { onChange : String -> msg, text : String, placeholder : Maybe (Placeholder msg), label : Label msg, show : Basics.Bool } -> Element msg

email : List (Element.Attribute msg) -> { onChange : String -> msg, text : String, placeholder : Maybe (Placeholder msg), label : Label msg } -> Element msg

search : List (Element.Attribute msg) -> { onChange : String -> msg, text : String, placeholder : Maybe (Placeholder msg), label : Label msg } -> Element msg

spellChecked : List (Element.Attribute msg) -> { onChange : String -> msg, text : String, placeholder : Maybe (Placeholder msg), label : Label msg } -> Element msg

If spell checking is available, this input will be spellchecked.

Sliders

A slider is great for choosing between a range of numerical values.

slider : List (Element.Attribute msg) -> { onChange : Basics.Float -> msg, label : Label msg, min : Basics.Float, max : Basics.Float, value : Basics.Float, thumb : Thumb, step : Maybe Basics.Float } -> Element msg

A slider input, good for capturing float values.

Input.slider
    [ Element.height (Element.px 30)

    -- Here is where we're creating/styling the "track"
    , Element.behindContent
        (Element.el
            [ Element.width Element.fill
            , Element.height (Element.px 2)
            , Element.centerY
            , Background.color grey
            , Border.rounded 2
            ]
            Element.none
        )
    ]
    { onChange = AdjustValue
    , label =
        Input.labelAbove []
            (text "My Slider Value")
    , min = 0
    , max = 75
    , step = Nothing
    , value = model.sliderValue
    , thumb =
        Input.defaultThumb
    }

Element.behindContent is used to render the track of the slider. Without it, no track would be rendered. The thumb is the icon that you can move around.

The slider can be vertical or horizontal depending on the width/height of the slider.

Note If you want a slider for an Int value:


type Thumb

thumb : List (Element.Attribute Basics.Never) -> Thumb

defaultThumb : Thumb

Radio Selection

The fact that we still call this a radio selection is fascinating. I can't remember the last time I actually used an honest-to-goodness button on a radio. Chalk it up along with the floppy disk save icon or the word Dashboard.

Perhaps a better name would be Input.chooseOne, because this allows you to select one of a set of options!

Nevertheless, here we are. Here's how you put one together

Input.radio
    [ padding 10
    , spacing 20
    ]
    { onChange = ChooseLunch
    , selected = Just model.lunch
    , label = Input.labelAbove [] (text "Lunch")
    , options =
        [ Input.option Burrito (text "Burrito")
        , Input.option Taco (text "Taco!")
        , Input.option Gyro (text "Gyro")
        ]
    }

Note we're using Input.option, which will render the default radio icon you're probably used to. If you want compeltely custom styling, use Input.optionWith!

radio : List (Element.Attribute msg) -> { onChange : option -> msg, options : List (Option option msg), selected : Maybe option, label : Label msg } -> Element msg

radioRow : List (Element.Attribute msg) -> { onChange : option -> msg, options : List (Option option msg), selected : Maybe option, label : Label msg } -> Element msg

Same as radio, but displayed as a row


type Option value msg

option : value -> Element msg -> Option value msg

Add a choice to your radio element. This will be rendered with the default radio icon.

optionWith : value -> (OptionState -> Element msg) -> Option value msg

Customize exactly what your radio option should look like in different states.


type OptionState
    = Idle
    | Focused
    | Selected

Labels

Every input has a required Label.


type Label msg

labelAbove : List (Element.Attribute msg) -> Element msg -> Label msg

labelBelow : List (Element.Attribute msg) -> Element msg -> Label msg

labelLeft : List (Element.Attribute msg) -> Element msg -> Label msg

labelRight : List (Element.Attribute msg) -> Element msg -> Label msg

labelHidden : String -> Label msg

Sometimes you may need to have a label which is not visible, but is still accessible to screen readers.

Seriously consider a visible label before using this.

The situations where a hidden label makes sense:

Basically, a hidden label works when there are other contextual clues that sighted people can pick up on.

Form Elements

You might be wondering where something like <form> is.

What I've found is that most people who want <form> usually want it for the implicit submission behavior or to be clearer, they want to do something when the Enter key is pressed.

Instead of implicit submission behavior, try making an onEnter event handler like in this Ellie Example. Then everything is explicit!

And no one has to look up obtuse html documentation to understand the behavior of their code :).

File Inputs

Presently, elm-ui does not expose a replacement for <input type="file">; in the meantime, an Input.button and elm/file's File.Select may meet your needs.

Disabling Inputs

You also might be wondering how to disable an input.

Disabled inputs can be a little problematic for user experience, and doubly so for accessibility. This is because it's now your priority to inform the user why some field is disabled.

If an input is truly disabled, meaning it's not focusable or doesn't send off a Msg, you actually lose your ability to help the user out! For those wary about accessibility this is a big problem.

Here are some alternatives to think about that don't involve explicitly disabling an input.

Disabled Buttons - Change the Msg it fires, the text that is rendered, and optionally set a Region.description which will be available to screen readers.

import Element.Input as Input
import Element.Region as Region

myButton ready =
    if ready then
        Input.button
            [ Background.color blue
            ]
            { onPress =
                Just SaveButtonPressed
            , label =
                text "Save blog post"
            }

    else
        Input.button
            [ Background.color grey
            , Region.description
                "A publish date is required before saving a blogpost."
            ]
            { onPress =
                Just DisabledSaveButtonPressed
            , label =
                text "Save Blog "
            }

Consider showing a hint if DisabledSaveButtonPressed is sent.

For other inputs such as Input.text, consider simply rendering it in a normal paragraph or el if it's not editable.

Alternatively, see if it's reasonable to not display an input if you'd normally disable it. Is there an option where it's only visible when it's editable?