A Web Component for Swapping Text between Text, HTML, and Markdown

A couple of re-aligns ago on this website, my “biography” (a bit of text about myself) had some interactive controls on it. You could swap it between:

  • Short, Medium, and Long
  • First-Person and Third-Person
  • Text, HTML, and Markdown

The big idea was to make it self-serve when someone asked me for this, say to accompany and interview or for an introduction at a conference. You never know what format they want it in, what the length limit is, or which tense. So I just did them all!

You’d think I would have done something clever to make that work, right?

I didn’t.

That’s why I don’t have it anymore. I created each possible permutation (3 Ă— 2 Ă— 3 = 18) separately, then just swapped between them depending on the combination of radio input values. Editing the bio was far too hard.

Couldn’t it be automated, though? The “which type of output” I always thought would be the easiest. So the other day I farted around with that and got this far:

I made it a Web Component named <bio-machine>, so it could be portable if needed. It’s not published anywhere except this Pen, so you can’t really just import and and use it, but maybe someday. It would need work.

The idea is that you only provide the Markdown, and the other formats are automatically created and the controls are added.

<bio-machine>
<div># Hello, World!

I'm Chris.</div>
</bio-machine>Code language: HTML, XML (xml)

All the rest of the code you see there is just setting up the Web Component.

I used a beta version of Tram-Lite. I like the idea that it requires no build step at all and the entire component is instantiated in a big block of HTML. I ultimately needed the help of Jesse Jurman, the creator of Tram-Lite, because the event binding stuff they built into v4 is just award with radio controls (apparently a generally weird problem).

Notes on the overall experience:

  • I couldn’t just do this.innerText to grab all the text from the Light DOM. Notice the awkward <div> wrapper which allows for this.querySelector("div").innerText. Feels like there should be a better way.
  • I don’t know that Tram-Lite lends itself well to packaging a component and letting people import and use it from a CDN like many other Web Components. I like the HTML focus though. Is that what this kind of future syntax is about? import { MyComponent } from "./component.html" with { type: 'html' }; I honestly don’t know.
  • I also couldn’t use <script type="module"> within Tram-Lite, so the other dependencies (the Markdown converter, the Syntax Highlighter, etc) had to be just global scripts, not imported. Again, I like the approach and philosophy of Tram-Lite, but it probably makes more sense to use Lit or something for this. I would have been more comfortable just attaching onChange handles to all the radios anyway.
  • How would I approach allowing styles in? Certainly I could use ::part and whatnot to set the main background and text colors, but not with styling links, because you can’t make each link a part and that would be ridiculous anyway. And because you can’t do like ::part(bio) a because you can’t use the cascade, that’s off. So you’d have to send through a --link-color custom property or something, which is fine, but that’s potentially mix-and-matching styling approaches which feels silly.
  • I probably haven’t done the accessibility correctly here, in regards to the changing content. Should I do something like <div role="region" aria-live="polite"> for the bio area?
Thoughts? Email me or comment below. Also CodePen PRO is quite a deal. 🙏

2 responses to “A Web Component for Swapping Text between Text, HTML, and Markdown”

  1. Jesse Jurman says:

    Nice article! Thanks for the shoutout and links to Tram-Lite, and more importantly, the feedback on Tram-Lite! Here’s some off-the-cuff thoughts:

    I couldn’t just do this.innerText to grab all the text from the Light DOM.

    In this case, it looks like you can do this.textContenttextContent has noted differences from innerText (see https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#differences_from_innertext), but none of them directly reference slots or web-components. This may be noted in a spec, or a browser quirk, but given that there are known differences in how these are processed, I’m guessing this is something that is expected, and you could probably use going forward

    I don’t know that Tram-Lite lends itself well to packaging a component and letting people import and use it from a CDN like many other Web Components.

    Yep, totally agree! It’s something I definitely want to improve, especially since the whole appeal of web-components is their portability. It’s the next big focus, most likely after the v4 release

    I also couldn’t use script type=”module” within Tram-Lite, so the other dependencies (the Markdown converter, the Syntax Highlighter, etc) had to be just global scripts, not imported.

    Candidly, my experience with type=”module” is pretty limited, so this hadn’t stood out as a use-case before you brought it up. It’s not on the top of the list to support, but the fact that this behavior is inconsistent is something that’ll probably warrant future work (hopefully not another major version bump )

    How would I approach allowing styles in?

    I haven’t played around with this too much, but potentially you could have a prop for styles (similar to react’s styled-components). Alternatively, since you are already using oklch, you could have all the styles with a fixed lightness and chroma, and have the hue be a prop that you pass into the component (defaulted to “220”, but dynamic for any other color scheme that you might want to fit into)

    Thanks again for playing around with this, it’s been super awesome to get other people’s detailed feedback here

  2. Chris Coyier says:

    Accessibility advice from Curtis Wilcox:

    Neat. Regarding its accessibility, maybe add a <legend> to the <fieldset> to make it more clear what happens when you select a radio, like “Change bio format.”

    Definitely don’t make the bio div an ARIA live region, a screen reader would read all its contents when a radio is selected. The screen reader says when and what radio is selected so if there’s a legend explaining what will happen, there’s no need for a live region also announcing what happened.

    role="region" would make the bio div a landmark for navigation purposes (more generic than the header, footer, main, nav landmarks). It probably isn’t needed, a heading that includes “Bio” would be better. If the role is used, the element would also have to have a name (using aria-label or aria-labelledby). <section> with a name also implicitly has the “region” role.

Leave a Reply to Chris Coyier Cancel reply

Your email address will not be published. Required fields are marked *