Or: How to generate dynamic html tags inside lit-html templates? eval
is sometimes mixed up with evil
. We also hear sometimes that there are cases in which it is needed. This is one such case…
Notice: this article is for an old version of
lit
. I was notified in the comments by Justin that in the newlit
versions there is better support for static expressions.
As I mention in the article, it did not work at the time and I came up with this solution.
This solution is also not performant when compared to other solutions. It did leave our code cleaner and overall performance in the app was not harmed so this is what we used.
Since we are not usinglit
anymore invivid 3.x
this is considered legacy code in our project.
The lit
project is a framework for working with web components. In vivid 1.x and 2.x we relied heavily on lit
. While we are working hard on vivid 3.x, we still support and add features to 2.x
.
One such feature was to add an H tag around the expansion panel component. Because expansion panels can be used as headers (and mostly they do), we need to set them up semantically as headers.

We’ve decided the default H level to be 3 (<h3>
). That’s not enough because the expansion panel can also be a sub header of any other header level:
<h1>Is there alien life out there?</h1>
<expansion-panel>
<h2><button>What is life?</button></h2>
</expansion-panel>
<expansion-panel>
<h2><button>What is alien?</button></h2>
</expansion-panel>
So you see – setting up h3
in this case would be wrong. The same is true for expansion panels under h3
, h4
and h5
.
Some would say to use a div
with aria-level
and role=header
. While this would have worked, there’s this quote from MDN (and other places):

aria-level
Taking all this into account, we wanted to keep the MDN’s best practices while still allowing for dynamic tags.
Table of Contents
Trying to setup a dynamic tag with lit-html
How do lit templates work?
Let’s first explain how templating works in lit
without getting too much into details.
lit-html
is a utility function that turns our strings into live templates. The lit
render method then “magically” listens to changes in properties in the template and updates the view accordingly.
So you’ll have a render
function that looks kind of like this:

html
lit function.The panel header would be the content we’d like to designate as a header.
As I mentioned, we wanted to wrap it with h3
as default, so it might look something like this:

h3
tagHow to not dynamically change a tag name in lit-html?
The second requirement was to have the h tag
dynamic. That means, we’d like to have a user editable property that will change the header’s level. Looking at the template, this might seem an easy task. Just set the variable inside the template string near the h
:

${this.headerLevel}
near the h
tagHow wonderful it looks! And how spectacularly it fails š
You see – lit-html
and templating system does not work well with static data. I’ll spare you the creepy details, but the error that we get is that it fails to parse the template – and this happens in runtime! (ouch – imagine what would have happened if we didn’t have tests)
Trying to use unsafeHTML
You know what they say: “If you can’t fight them – use their methods”. A lit method called unsafeHTML
promises to do the following: Renders the result as HTML, rather than text.
Since the documentation was lacking, I turned to the official documentation – the unit tests:

unsafeHTML
unit test that shows how to use it. It seems we can actually parse anything as HTML.Applying that to our code looks like:

unsafeHTML
in our codeOk – build passed. Tests failed (but who looks at tests, right?). Why did they fail? Here’s how it looks like when using this solution:

unsafeHTML
. You can see all it did was generate an orphaned h3
tag instead of wrapping our button with h3
tag. That’s mostly why the tests failed – because they expected the button to be wrapped with h3
… post in the comments below if you’d like to see how I tdd
ed this one šI won’t go into details why this didn’t work – suffice to say that the way lit-html
renders its templates doesn’t allow for “loose” HTML tags ends.
We’re back to point zero. We tried something that didn’t work… time to move on!
Trying to use unsafe
Static
Our third attempt was to use another built in function of lit
called unsafeStatic
. Again the documentation isn’t too helpful, so… you know… unit tests:

unsafeHTML
does exactly what we need!Seems like our problem is solved. Let’s just implement this in our code:

unsafeStatic
to the mix just like in the docs and unit tests of the library.And then… we get this:

object object
!Hey Element – next time you yell
object
no one will believe you!Apparently, while their tests worked with the unit-tests
render, it did not work in the “real world”. Running render
on my own showed me the exact same thing.
So… unsafeStatic
doesn’t work. What’s next?
How to use Eval
in order to generate dynamic tags in lit-html templates
In our former failed attempt we tried to add the headerLevel
dynamically like this:
<h${this.headerLevel}>
${this.renderPanelHeader()}
</h${this.headerLevel}>
Let’s try to tackle this differently. Because we can call rendering functions that return TemplateResult
objects, we can create a function that returns an eval
ed TemplateResult
like this:

renderPanelHeader
method. It first verifies that our input is correct (we don’t want someone injecting stuff into our code). Then it returns the evaluated value of running the string inside the eval
call.Our solution now return a dynamic template with a dynamic tag name for our header:


Let’s bundle it all up
It seems like we succeeded in what we wanted. We have a dynamic tag inside our lit-html
– and we even used eval
…
We did find another “small” issue. While running our ui visual regression tests (that are being bundled by webpack) we got the following error:

html
function inside an eval
The same error showed up in our storybook (webpack, again…).
Oh no! Now our components are not usable in bundled applications (which are like… 99% of the applications consuming Vivid).
What to do? What to do? I know – let’s get depressed and give up!
Or… we could come up with another nasty solution!
If we just set the html
as a local variable and use the local variable, it should be available in runtime.
So adding const safeHTML = html;
at the top of the file and then using safeHTML
instead of html
solves our problem.
Actually – a variable might be problematic since an uglifier/minifier might change it. We could use a static method on the class itself to make sure it is more resilient. It depends how “aggressive” your uglifier/minifier is…
Not my problem at the moment š
Summary
Accessibility is an important matter. We don’t want to leave anyone behind, and make sure our apps are as accessible to as many people as possible.
Using correct semantics in your HTML is a big part of that. This is why the vivid
team is constantly working on adding accessibility features to our Design System’s components.
In our case we wanted to make sure the expansion panels
get the right semantics as headers. Because we wanted to allow the consumers to determine the header’s level, we wanted the h
tag to be dynamic.
lit-html
was not really cool with that, so after trying out some ways of doing that, we found that by using eval
we can create a dynamic tag inside lit-html
.
We then found another small problem: when bundling our component inside other apps (in our case, the ui visual regression tests and storybook), we found out that using eval
is tricky. It wouldn’t find the imported html
since the eval
expression is evaluated in runtime and bundlers tend to give imports their own names…
We solved it by creating a local property on the class and it did the trick. We had a dynamic tag that was also working with bundlers.
The operation was a success.
Thanks a lot to Rachel B. Tannenbaum and Yinon Oved for the inspiration and review of this article.
I would use static expressions for this instead: https://lit.dev/docs/templates/expressions/#static-expressions
Thanks!
It didn’t work at the time, and we have left
lit
in favor offast
so for us it is irrelevant anymore šThis would be beneficial for future readers who are lit users I guess š
I’ll update the article with your comment.