diff --git a/Guide/hsx.markdown b/Guide/hsx.markdown
index 08be852e8..0ff71ce0c 100644
--- a/Guide/hsx.markdown
+++ b/Guide/hsx.markdown
@@ -338,6 +338,47 @@ The [`preEscapedToHtml`](https://ihp.digitallyinduced.com/api-docs/IHP-ViewPrelu
{"" |> preEscapedToHtml}
```
+#### HTML Attributes without Escaping
+
+Variable attributes are also escaped:
+
+```haskell
+html = [hsx|
+
+ Hello World
+
+|]
+ where
+ style = "someClassName&" :: Text
+```
+
+The above code will generate the following HTML, where the `&` symbol has been replaced with it's escpaed form `&`:
+
+```html
+Hello World
+```
+
+You might want to have this `&` character not escaped. E.g. to use [Tailwind Arbitrary Variants](https://tailwindcss.com/blog/tailwindcss-v3-1#arbitrary-values-but-for-variants). In that case wrap the attribute value with a call to `preEscapedTextValue`:
+
+
+```haskell
+html = [hsx|
+
+ Hello World
+
+|]
+ where
+ style = preEscapedTextValue "someClassName&"
+```
+
+The HTML will now render without escaping the attribute:
+
+```html
+Hello World
+```
+
+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.
+
## Example: HSX and the equivalent BlazeHtml
The following code using HSX:
diff --git a/IHP/ViewPrelude.hs b/IHP/ViewPrelude.hs
index d372abba3..b4d3c7d06 100644
--- a/IHP/ViewPrelude.hs
+++ b/IHP/ViewPrelude.hs
@@ -13,6 +13,7 @@ module IHP.ViewPrelude (
hsx,
toHtml,
preEscapedToHtml,
+ preEscapedTextValue,
module IHP.ValidationSupport,
pathTo,
urlTo,
@@ -41,7 +42,7 @@ import IHP.Prelude
import IHP.ViewErrorMessages ()
import IHP.ViewSupport
import Text.Blaze (preEscapedText, stringValue, text, (!))
-import Text.Blaze.Html5 (preEscapedToHtml)
+import Text.Blaze.Html5 (preEscapedToHtml, preEscapedTextValue)
import IHP.View.Form
import IHP.HSX.QQ (hsx)
import IHP.HSX.ToHtml
diff --git a/Test/HSX/QQSpec.hs b/Test/HSX/QQSpec.hs
index cd0670e7a..d42be1f22 100644
--- a/Test/HSX/QQSpec.hs
+++ b/Test/HSX/QQSpec.hs
@@ -8,6 +8,7 @@ import Test.Hspec
import IHP.Prelude
import IHP.HSX.QQ
import qualified Text.Blaze.Renderer.Text as Blaze
+import Text.Blaze (preEscapedTextValue)
tests = do
describe "HSX" do
@@ -163,6 +164,12 @@ tests = do
-- See https://github.com/digitallyinduced/ihp/issues/1226
[hsx||] `shouldBeHtml` ""
+
+ it "should support pre escaped class names" do
+ -- See https://github.com/digitallyinduced/ihp/issues/1527
+
+ let className = preEscapedTextValue "a&"
+ [hsx||] `shouldBeHtml` ""
data Project = Project { name :: Text }
diff --git a/ihp-hsx/IHP/HSX/ConvertibleStrings.hs b/ihp-hsx/IHP/HSX/ConvertibleStrings.hs
index 47449500b..f81bc4065 100644
--- a/ihp-hsx/IHP/HSX/ConvertibleStrings.hs
+++ b/ihp-hsx/IHP/HSX/ConvertibleStrings.hs
@@ -36,4 +36,8 @@ instance ConvertibleStrings LBS.ByteString Html5.AttributeValue where
instance ConvertibleStrings Text Html5.Html where
{-# INLINE convertString #-}
- convertString = Html5.text
\ No newline at end of file
+ convertString = Html5.text
+
+instance ConvertibleStrings Html5.AttributeValue Html5.AttributeValue where
+ {-# INLINE convertString #-}
+ convertString value = value