HSX can be written pretty much like normal HTML. You can write an HSX expression inside your Haskell code by wrapping it with [hsx|YOUR HSX CODE|]. HSX expressions are just a syntax for BlazeHtml and thus are automatically escaped as described in the blaze documentation.

Because the HSX is parsed, you will get a syntax error when you type in invalid HTML.

Inline Haskell

HSX can access Haskell variables wrapped with {} like this:

    x :: Text = "World"
    [hsx|Hello {x}!|]

If the variable is another HSX expression, a blaze HTML element, a text or string: it is just included as you would expect.

If the variable is any other custom Haskell data structure: it will first be converted to a string representation by calling show on it. You can add a custom ToHtml (import it from IHP.HSX.ToHtml) instance, to customize rendering a data structure.

You can also write more complex code like:

    items :: [Int] = [ 0, 1, 2 ]
    renderItem n = [hsx|Hello {n}!|]
    [hsx|Complex demo: {forEach items renderItem}!|]

As the HSX expressions are compiled to Haskell code at compile-time, type errors inside these {} expressions will be reported to you by the compiler.

Dynamic Attributes

The variable syntax can also be used in attribute values:

    inputValue = "Hello World" :: Text
    [hsx|<input type="text" value={inputValue}/>|]

Boolean Attribute Values

HSX has special handling for Boolean values to make it easy to deal with HTML Boolean attributes like disabled, readonly, checked, etc.

You can write

<input disabled={True} />

as a short form for:

<input disabled="disabled" />

Writing False:

<input disabled={False} />

This will not render the attribute, as specified in the HTML standard:

<input />

Boolean data attributes

This behavior of omiting a attribute when it’s set to False does not apply to data- attributes.

You can write

<form data-disable-javascript-submission={True}/>

and it will render like this:

<form data-disable-javascript-submission="true"/>

When set to False, like this:

<form data-disable-javascript-submission={False}/>

the output HTML will keep the attribute and set it to "false":

<form data-disable-javascript-submission="false"/>

Maybe Attribute Values / Optional Attributes

HSX has special handling for Maybe values to make it easy to deal with optional attributes.

You can write

    target :: Maybe Text
    target = Just "_blank"
    [hsx|<a target={target} />|]

and it will render to:

<a target="_blank" />

Using Nothing results in the target attribute not being in the output HTML:

    target :: Maybe Text
    target = Nothing
    [hsx|<a target={target} />|]

This will render to:

<a />

Spread Values

For dynamic use cases you can use {...attributeList}:

<div { ...[ ("data-my-attribute" :: Text, "Hello World!" :: Text) ] } />
<div { ...[ ("data-user-" <> tshow userId, tshow userFirstname) ] } />
<div { ...someVariable } />

Note the <> concatenation operator.

Special Elements: <script> and <style>

For <script> and <style> tags HSX applies some special handling. This only applies to tags with inline scripts or styles:

<!-- No Special Handling: -->

<script src="/hello.js"></script>
<link rel="stylesheet" href="layout.css" />

<!-- Special Handling applies: -->

    h1 {
        color: blue;

Inside those tags using a Haskell expression will not work:


This will just literally output the string {myHaskellExpr} without evaluating the Haskell expression itself. This is because JavaScript usually uses {} for object expressions like { a: "hello" }. The same applies to inline CSS inside <style> elements.

So using {haskellVariables} inside your JavaScript like this will not work:

    var apiKey = "{apiKey}";

Instead use a data- attribute to solve this:

<script data-api-key={apiKey}>
    var apiKey = document.currentScript.dataset.apiKey;

Additionally, HSX will not do the usual escaping for style and script bodies, as this will make e.g. the JavaScript unusable.

Syntax Rules

While most HTML is also valid HSX, there are some differences you need to be aware of:

Closing Tags

Tags always need to have a closing tag or be self-closing: so instead of <br> you need to write <br/>.

JSX Differences

In JSX you have the restriction that you always have to have a single root tag. In HSX this restriction does not apply, so this is valid HSX (but not valid JSX):



Spaces and newline characters are removed where possible at HSX parse time.


HTML Comments are supported and can be used like this:

    <!-- Comment will be shown on HTML -->

We also support a “no render” comments, that can be used for adding comments inside hsx, but without being rendered to HTML.

        {- Comment will not be shown on HTML -}

Will render to:


Empty Attributes

HSX allows you to write empty attributes like these:

    <input disabled/>

The underlying HTML library blaze currently does not support an empty HTML attribute. Therefore empty attributes are implemented by setting the attribute value to the attribute name. This is valid HTML supported by all browsers.. Therefore the generated HTML looks like this:

<input disabled="disabled" />

Unescaped Strings

If you use HTML entities, such as &nbsp; for a non-breaking space, you will notice they appear exactly like that. To output directly (i.e. unescaped) use the method preEscapedToMarkup from Text.Blaze.Html5.

Common HSX Patterns

Dealing with Maybe Values

When dealing with Maybe values you sometimes want to conditionally render something. E.g. in react to render a tag with JSX you would do it like this:

<p>Hi {user.name}</p>
{user.country && <p><small>from {user.country}</small></p>}

In HSX you usually write it like this:

render user = [hsx|
    <p>Hi {user.name}</p>
        renderCountry = case user.country of
            Just country -> [hsx|<p><small>{country}</small></p>|]
            Nothing -> [hsx||]

What about if the country is a empty string? A simple solution could look like this:

renderCountry = case user.country of
    Just "" -> [hsx||]
    Just country -> [hsx|<p>from {country}!</p>|]
    Nothing -> [hsx||]

That code doesn’t feel right. It’s better to deal with the problem at create time of the User record already. Use emptyValueToNothing to transform an empty Just "" to Nothing before inserting it into the db:

action CreateUserAction = do
    newRecord @User
        |> fill '["name", "country"]
        |> emptyValueToNothing #country
        |> createRecord

Now when the country input field is empty when creating the user, the country field will be set to Nothing instead of Just "".

Displaying HTML without Escaping

In HSX all variables and expressions are automatically escaped to avoid XSS.

Let’s say we have variable myVariable = "<script>alert(1)</script>". This is how the HSX will behave:


-- RENDERS to this HTML:

Sometimes you want to insert some actual HTML code to your HSX using a variable. E.g. when your app is rendering markdown, the generated HTML by the markdown library should not be escaped, the markdown library typically already takes care of that.

In these cases you can use preEscapedToHtml to mark a text as already escaped:

<div>{markdownHtml |> preEscapedToHtml}</div>

-- Let's define `markdownHtml = "<p>hello</p>"`

-- This will render the following HTML:

Be careful when using preEscapedToHtml as it can easily introduce security issues.

The preEscapedToHtml function can also be used to output HTML code that is not supported by HSX: The preEscapedToHtml function can also be used to output HTML code that is not supported by HSX:

{"<!--[if IE]> Internet Explorer Conditional Comments <![endif]-->" |> preEscapedToHtml}

HTML Attributes without Escaping

Variable attributes are also escaped:

html = [hsx|
    <div class={style}>
        Hello World
        style = "someClassName&" :: Text

The above code will generate the following HTML, where the & symbol has been replaced with it’s escpaed form &amp;:

<div class="someClassName&amp;">Hello World</div>

You might want to have this & character not escaped. E.g. to use Tailwind Arbitrary Variants. In that case wrap the attribute value with a call to preEscapedTextValue:

html = [hsx|
    <div class={style}>
        Hello World
        style = preEscapedTextValue "someClassName&"

The HTML will now render without escaping the attribute:

<div class="someClassName&">Hello World</div>

Keep in mind that you’re now responsible for making sure that there’s no bad input inside the string passed to preEscapedTextValue. You might accidentally open the door for XSS.

A List of Html Elements

It’s possible that one of your functions would need to get a list of Html values. The following code will results with an error

render :: [Html] -> Html
render items = -- ...
Illegal qualified type:
    (?context::ControllerContext) => Blaze.Html
• In the expansion of type synonym 'HtmlWithContext'
  In the expansion of type synonym 'Html'
  In the type signature:

To overcome it, we would write it like this. Inside the function, we can concatenate the list into a single Html value.

import qualified Text.Blaze.Html as Blaze

render :: [Blaze.Html] -> Html
render items =
    [hsx|Items are: {items |> mconcat}|]

Example: HSX and the equivalent BlazeHtml

The following code using HSX:

instance View EditView where
    html EditView { .. } = [hsx|
            <ol class="breadcrumb">
                <li class="breadcrumb-item"><a href={PostsAction}>Posts</a></li>
                <li class="breadcrumb-item active">Edit Post</li>
        <h1>Edit Post</h1>
        {renderForm post}

is roughly equivalent to the following BlazeHTML code:

instance View EditView where
    html EditView { .. } = do
        H.nav $ do
            H.ol ! A.class_ "breadcrumb" $ do
                H.li ! A.class_ "breadcrumb-item" $ do
                    H.a ! A.href (fromString (P.show PostsAction)) $ do
                H.li ! A.class_ "breadcrumb-item active" $ do
                    "Edit Post"
        H.h1 "Edit Post"
        renderForm post

given the following imports:

module Web.View.Posts.Edit where
import Web.View.Prelude

import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A

import Prelude as P