I-HTML, an inline-HTML import element
i-html, an inline-html import element

      
      
        i-html is a drop in
        tag that allows for dynamically importing html,
        inline. It's a bit like an <iframe>, except the
        html gets adopted into the page.
      
      
        You might have used something similar before, it might seem familiar to
        other techniques such as
        hotwired turbo or similar. You
        might have even used an element very close to this one, for example the
        popular
        include-fragment-element
        by GitHub. This element is a spiritual successor to that one (more on
        that below). But this one is <i-html>. Let's talk about
        it.
      
      
        Inside the box below is a demonstration of the element. The box is there
        to help you see it, but the element is inside. If JavaScript is enabled
        the contents of the box should read "Hello world!". The source page is
        just an HTML page. There's nothing special about it. Look, go see for
        yourself: hello.html.
      

              
  <i-html src="example-responses/hello.html">Loading...</i-html>
        
      

      
        Loading...
      

      
        The <i-html> tag can be placed on a page, as an empty
        container. It is completely unstyled (well almost, it has
        display:contents). Whenever the src= attribute
        changes, it will fetch the requested resource as
        text/html and replace its inner contents with the parsed
        contents of the response. That's essentially all it does. Kind of.
      

      
        Setting src= and leaving it alone does not really
        demonstrate the full utility, however, because it is far more capable of
        interesting things when utilised correctly. People do lots of novel
        things with <img> tags, and <i-html>
        should be no different.
      

      Getting started

      
        The easiest way to start is to include the minified script from the CDN:
      

              
  <script src="https://cdn.jsdelivr.net/npm/i-html-element/i-html.min.js" defer></script>
        
      

      
        or run npm i i-html-element and use the local module:
      

              
  <script type="module" src="node_modules/i-html-element/i-html.js"></script>
        
      

      Features

      Targeting with link

      
        Like an iframe, <i-html> respects the
        target= attribute on links. If an
        <a target=> points to the <i-html>, then
        the src= is switched with that of the link. A
        demonstration:
      

              
  <a href="example-responses/hello.html" target="link-target-example">
    Load hello.html
  </a>
  <a href="example-responses/how-are-you.html" target="link-target-example">
    Load how-are-you.html
  </a><br>
  <i-html id="link-target-example"></i-html>
        
      

      

      Targeting with form

      
        Also like an iframe, target= on forms is also respected.
        This means forms can submit to an <i-html> element which
        will then load in new content. Here's a dummy form, for example:
      

              
  <form action="example-responses/form-save.html" method="get" target="form-target-example">
    <label>
      A form label:
      <input type="search" />
    </label>
    <button type="submit">Submit</button>
  </form>

  <p>Submitting the form will change the content below:</p>

  <i-html id="form-target-example">Nothing yet</i-html>
        
      

      
        

        Submitting the form will change the content below:

        Nothing yet
      

      Wrapping a form

      
        Wrap a <form> tag in an <i-html> element
        and you'll have a form that replaces itself. Serve the same form back
        and you have an "AJAX style" form with no effort.
      
      
              
  <i-html id="ajax-form-target-example">
    <form action="example-responses/ajax-form.html" method="get" target="ajax-form-target-example">
      <label>
        A form label:
        <input type="search" />
      </label>
      <button type="submit">Submit</button>
    </form>

    <p>Submitting the form will change the whole form</p>
  </i-html>
        
      

      
        
          

          Submitting the form will change the whole form
        
      

      Form actions

      
        Of course because <forms> work, so does
        <button formaction=. Example:
      

              
  <form method="get" target="formaction-example">
    <button formaction="example-responses/form-delete.html" type="submit">
      Delete
    </button>
    <button formaction="example-responses/form-save.html" type="submit">
      Save
    </button>
  </form>

  <p>Submitting the form will change the content below:</p>

  <i-html id="formaction-example">Nothing yet</i-html>
        
      

      
        

        Submitting the form will change the content below:

        Nothing yet
      

      Targeting the response

      
        By default <i-html> will parse the response as HTML,
        and inject the <body> element's contents into itself.
        This can be changed by altering the target= attribute on
        <i-html> itself (which defaults to 'body>*'
        (or 'svg' if accept= is an SVG mime)). If you
        find you want to take a different element from the response body, set
        target= to any valid querySelector. If
        target= is an invalid querySelector it'll
        revert to 'body>*'. You can check if a
        target= select is valid by using JavaScript to set
        .target and then read the value back out, for example:
      

              
  myEl.target = '/not a valid selector/' // this won't set:
  console.assert(myEl.target === 'body')

  myEl.accept = '.this[is]:valid' // this will set:
  console.assert(myEl.target === '.this[is]:valid')
        
      

      
        In the below example, these two links fetch the same HTML, which has two
        paragraphs. However the two links target two different
        <i-html> elements, the first one with a
        target="p:first-child", the second with a
        target="p:last-child".
      
      
              
  <a href="example-responses/two-paragraphs.html" target="two-paragraphs-a">
    Load two-paragraphs.html into first target
  </a>
  <a href="example-responses/two-paragraphs.html" target="two-paragraphs-b">
    Load two-paragraphs.html into first target
  </a>
  <p><i-html id="two-paragraphs-a" target="p:first-child"></i-html></p>
  <p><i-html id="two-paragraphs-b" target="p:last-child"></i-html></p>
        
      

      

      Refetching

      
        If you want to refetch the contents there are several options. The
        simplest is to use an <a href> element, as clicking the
        link will cause <i-html> to load each time.
      

      
        That's not always the easiest though, and so another option is to use a
        <button command="--load" commandfor=".."> element,
        which will cause the <i-html> to load the
        src it has (regardless of the loading= value,
        see Deferring Loading for more on
        that).
      

      
        (For older browsers that don't support command/commandfor buttons)
        
          You'll need to drop in the "invokers-polyfill" package to polyfill
          this in older browsers.
        
        
          <script type="module" src="https://unpkg.com/invokers-polyfill@0.4.2/invoker.js">
          </script>
        
      

              
  <button command="--load" commandfor="command-load-example">Load</button>
  <i-html id="command-load-example" src="example-responses/hello.html"></i-html>
        
      

      

      
        Responses can indicate to the client when the content should refresh by
        adding a `Refresh` header (or a <meta http-equiv=refresh>
        meta tag), telling <i-html> to refresh after N seconds.
        This is opt-in though, and needs the allow=refresh
        attribute on the element.
      
      
        The `Refresh` header can also have a URL to traverse to. This is also
        respected, which can make for some interesting properties such as the
        example below. Lastly, this can be stopped with
        <button command="--stop":
      

              
  <button command="--stop" commandfor="refresh-example">Stop</button>
  <i-html allow="refresh" id="refresh-example" src="example-responses/refresh.html"></i-html>
        
      

      


      Content negotiation

      
        By default <i-html> will make a request using the header
        Accept: text/html. You can customise this by setting the
        accept= attribute, but it is limited to certain values:
      
      
        text/plain.
        text/html.
        image/svg+xml.
        application/xml.
      
      
        You can slightly customise these types by setting the "subtype combination"
        or "parameter values", and it'll be smart and make the request with those
        extras. This means application/xhtml+xml will work, and the
        expected return content-type must be either application/xml or
        application/xhtml+xml (but an entirely different sub-type like
        application/atom+xml will fail). Also more complex mime
        types like text/fragment+html; charset=utf-8 will work, and
        the server can respond with the generic text/html or the matching
        sub-type of text/fragment+html.
      
      
        Invalid mimes such as application/json will revert to
        text/html. (If you want to get JSON you don't need this element).
      
      
        You can check what type works by using JavaScript to set .accept
        and then read the value back out, for example:
      

              
  myEl.accept = 'application/json' // this won't set:
  console.assert(myEl.accept === 'text/html')

  myEl.accept = 'text/fragment+html; charset=utf-8' // this will set:
  console.assert(myEl.accept === 'text/fragment+html; charset=utf-8')
        
      

      
        The mime types text/html, image/svg+xml, and
        application/xml will all use DOMParser to
        parse the full response body. text/event-stream uses a
        different streaming mode...
      

              
  <a href="star-solid.svg" target="svg-example">
    <i-html id="svg-example" src="example-responses/star.svg" accept="image/svg+xml"></i-html>
  </a>
        
      

      


      Streaming Mode

      
        With the accept= attribute set to
        text/event-stream, it will use an
        EventSource to listen for
        Server-Side Events. A connection will be open and for each event fired, the response will
        be converted using DOMParser and replaced. This allows for
        many replacements with one request (this could be useful, for example,
        to show a notification count for a user). The connection will be kept
        open and replacing contents until either the server drops out, the
        browser closes the EventSource, the element is removed from
        the DOM, or the src= or accept= attributes
        change value.
      

      Insertion Mode

      
        Whether in streaming mode or one-shot mode, the default operation is to
        replace the contents of the element with the newly downloaded contents.
        While this is useful most of the time, some of the time it can
        be even more useful to switch to an append mode. Changing to
        insert=append will cause new content to be added after all
        the current children, and using insert=prepend will cause
        new content to be added before the current children:
      

              
  <a href="example-responses/prepend-list.html" target="prepend-example">
    Prepend to this list:
  </a>
  <ul>
    <i-html id="prepend-example" target="li" insert="prepend">
      <li>Milk</li>
      <li>Eggs</li>
    </i-html>
  </ul>
        
      

      

      Deferring Loading

      
        Just like <img> and <iframe> tags,
        <i-html> tags have a loading= attribute. By
        default they are loading=eager, but changing it to
        loading=lazy means it will wait until it's visible in the
        viewport until it makes the request. This also respects CSS styles, so
        if it's inside an element with display:none then
        loading=lazy can prevent it from loading until its
        container is display:block. This is really handy for
        components like <dialog>s.
      

      
        Changing to loading=none means it will never load,
        unless the loading= attribute is changed back to either
        loading=eager or loading=lazy. This gives you
        an opportunity to use JavaScript to determine when the loading should
        occur, for example on click.
      

      
        A <button command="--load" commandfor=".."> element
        pointing to a, <i-html> element will force it to load,
        regardless of the loading= value.
      

      
        
          ... lazy loading ...
        
      

      Security

      
        Injecting arbitrary HTML into a page can pose some security risks, so
        it's important that there's a defence in depth approach to
        mitigating the risk surface area. <i-html> has some
        security provisions in place but it's important to not only lean on
        these, and also apply additional security measures:
      

      
        
          Use a 
            Content Security Policy
          . A good minimum default is script-src self, which
          will prevent JavaScript from executing unless it's served by the same
          origin, and only from <script> tags, not inline
          attributes such as onclick.
        
        
          Consider how to 
            protect against CSRF
          . A good measure would be HTTP-only session cookies to protect
          sensitive resources.
        
        
          Fine tune your 
            CORS policies
           to protect against injecting pages you might not want
          <i-html> to embed.
        
      

      
        With that out of the way, let's talk about the protections
        <i-html> has in place. It uses fetch()
        under-the-hood and so must adhere to CORS policies. By default the
        credentials option is set to 'same-origin'
        meaning it will send cookies (and respect the Set-Cookie
        response header) for same-orign requests. To lock this down down further
        setting credentials="omit" will never send/recieve cookies,
        and setting credentials="include" will open it up to
        sending/receiving cookies in cross-origin requests.
      

      
        The default behaviour of <i-html> is restricted in some
        ways, and these restrictions can be lifted by adding keywords into the
        allow= attribute. This attribute takes a space-separated
        list of well-known keywords, and each one will allow a certain type of
        previously restricted behaviour to happen. The keywords can be combined,
        so for example allow="refresh media cross-origin" is a
        valid value.
      

      Cross Origin (allow="cross-origin")

      
        The default behaviour of <i-html> is to only
        fetch same-origin URLs. This means
        <i-html src="https://other-site"> won't actually do
        anything, as you'll need to allow cross-origin requests explicitly.
        This can be done with the allow="cross-origin" attribute.
      

      
        It is important to note that allow="cross-origin" only
        impacts the initial fetch. Without
        allow="cross-origin" a page may still have URLs or
        resources (such as images) to other origins, and these will be rendered
        regardless of this setting.
      

      Sanitization

      
        <i-html> will never explicitly append certain elements
        into the page, unless you opt in. For example if a response contains an
        <iframe> element, this will simply be deleted before
        the contents are injected. If you want <iframe>
        elements to be injected, you'll need to add the
        allow="iframe" attribute to the element.
        <iframe>s aren't the only elements that get sanitized.
        In fact, there are quite a few. Anything that fetches a sub-resource
        will be sanitized out by default, only to be opted in with an
        allow= attribute.
      

      
        allow="iframe"
        
          This allows the rendering of <iframe> elements in the
          response body.
        

        allow="i-html"
        
          This allows the rendering of <i-html> elements in the
          response body.
        

        allow="script"
        
          This allows the rendering of <script> elements in the
          response body.
        

        allow="style"
        
          This allows the rendering of <style> and
          <link rel=stylesheet elements in the response body.
        

        allow="media"
        
          This allows the rendering of <img>,
          <picture>, <video> 
          <audio>, and <object> elements in the
          response body.
        
      

              
  <i-html src="example-responses/sanitization.html" allow="media"></i-html>
        
      

      

              
  <a target="theme-switcher" href="example-responses/theme-pink.html">Switch to pink theme</a><br>
  <a target="theme-switcher" href="example-responses/theme-yellow.html">Switch to yellow theme</a><br>
  <a target="theme-switcher" href="example-responses/theme-gray.html">Switch to grayscale theme</a><br>
  <a target="theme-switcher" href="example-responses/empty.html">Reset theme</a><br>
  <i-html id="theme-switcher" allow="style"></i-html>
        
      

      


      Tuning off protections

      
        Of course, if you like to live dangerously, you can turn all of this off
        by setting allow="*". This is a very bad idea but can be
        useful during development.
      

      Declarative Shadow DOM

      
        Support for parsing fragments of HTML which include declarative shadow DOM (DSD) templates via Document.parseHTMLUnsafe recently entered Baseline 2024 status, so <i-html> will use that method of parsing if supported. Older browsers would need a polyfill to handle those cases, otherwise, <i-html> will fall back on new DOMParser().parseFromString which does not support DSD.
      

      
        Note: the "unsafe" nomenclature of this API simply means the browser won't perform any sanitization. (Coming soon to the web platform, there will be a `parseHTML` counterpart which does.) But as explained above, <i-html> will perform its own sanitizing process unless you choose to opt into the various `allow` directives.
      

      
        
          ... loading dsd ...
        
      

      Styling

      
        By default this element is display: contents so it won't
        effect layout, and it has role=presentation so the
        container itself has no impact on the accessibility tree. All of the
        content loaded in, however, will effect layout and the accessibility
        tree. There are 4 pseudo classes you can use to style it during various
        stages of its lifecycle, it will always be in one of these states, and
        never more than one. The states are loading,
        loaded, errored, and waiting.
        They can be styled like so:
      

              
          
        
      

      
        (For older browsers that don't support
        CSS CustomStateSet you'll need some additional CSS.)

        
          The element will fall back to using attributes like
          [state-waiting] in older browsers, such as Safari 17.3
          and below, Firefox 125 and below, and Chrome 89 and below.
        
        
          Chrome versions 90-124 used a different syntax for :state(),
          which used double dashes instead, like :--waiting. There's
          a small bit of support code to handle those versions and so it will fall
          back to using syntax like :--waiting if it needs to. If you need to
          support all of these you'll need something more like:
        

        
          
        
      

      
        Here are some details for what each of these states mean:
      

      
        
          
            waiting is the state used when the element is in the
            DOM, but either does not have a src= or has
            loading=lazy or loading=none and hasn't
            yet begun to load. Once an element has left the waiting
            state it'll never go back to it.
          
        
        
          
            loading is the state used when the element is in making
            a request, but the request hasn't yet completed. An individual
            element can enter and exit this state multiple times per session.
          
        
        
          
            streaming is the state used when the request was an
            text/event-stream type, and the connection has been
            opened. In this state events can stream in, and the
            loaded state is removed. When the connection closes it
            will transition to a loaded or error
            state.
          
        
        
          
            loaded is the state used when the element has
            successfully completed and closed the request and has inserted the
            content into the page, and has no more work to do for now. If the
            src= changes, it might move back to the
            loading state.
          
        
        
          
            error is the state used when the element has completed
            a request, but the request failed somehow. Unless in streaming mode,
            the element won't have inserted any content into the page. If the
            src= or accept= attributes change, it
            might move back to the loading state.
          
        
      

      Events

      
        There are a wealth of events that are fired for each stage of the
        elements lifecycle. Just like <img> and
        <iframe> elements, <i-html> elements
        dispatch loadstart, load,
        loadend, and error events to announce which
        stage of the loading process they're in. They also dispatch
        beforeinsert, and inserted events.
      

      
        
          
            loadstart is dispatched right before a request is
            started. This event also has a .request property that
            is the Request object that will be given to
            fetch(). You can re-assign .request or
            mutate some of its properties, and whatever changes you make to it
            will propagate to the fetch() call, so the
            loadstart event is really useful if you want to
            customise requests beyond the default capabilities, for example
            adding new headers. You can call .preventDefault() on
            this event to stop loading happening altogether, and no subsequent
            events will fire unless the request lifecycle is restarted (for
            example by changing src=).
          
        
        
          
            load is dispatched as soon as the request has completed
            successfully. When streaming, this will be dispatched upon a
            successful (non error) close of the EventSource. This
            event does not come with any additional properties. This event isn't
            fired if the network request had an error.
          
        
        
          
            error is dispatched if the network request failed for
            some reason, or if it was unable to be created for example due to
            bad Request data. This can happen when streaming if the connection
            errors, even after events have been sent. This event does not come
            with any additional properties.
          
        
        
          
            loadend is dispatched at the end of a request,
            regardless of the end state of the request. In other words this will
            always come directly after a load or error
            event. This event does not come with any additional properties.
          
        
        
          
            beforeinsert is dispatched right beforethe
            contents are about to be inserted into the page. In streaming mode
            this event could be fired many times. This comes with a
            .content property which is an array of all the child
            Nodes about to be inserted into the element. If you
            re-assign or otherwise mutate this array, then that is what will be
            inserted. This is useful for doing extra sanitization or simply
            removing elements with additional scripting. If you wan't to do
            something even more radical, you can call
            .preventDefault() and it will not
            insert the contents, leaving it up to you what to do next. Other
            events will continue to fire, and if in streaming mode, you may get
            new data coming in.
          
        
        
          
            inserted is dispatched right after the
            contents have been inserted into the page, provided that
            beforeinsert was not cancelled. In streaming mode this
            event could be fired many times. This comes with a
            .content property which is an array of all the child
            Nodes that were inserted - which may be different to
            the elements .childNodes. This event is not
            preventable, as the action has already happened.
          
        
      

      Questions you might have

      What about using <alternative>?

      
        There are many elements like this, but none that are quite like
        this. I think this is a culmination of the best features of the other
        elements.
      

      What about using htmx / htmz?

      
        htmx and
        htmz came as small inspiration
        for this library. htmx is a great project which demonstrates the power
        of leveraging HTML, and htmz is a great lightweight alternative that
        leans into the web platform even futher, but it also lacks some real
        niceties that you'd want from a component like this. This is why
        <i-html> exists - to take the concepts of htmz and
        expand them into a fully-fledged element, adding the missing features.
        If you want a fully featured and more popular library, try htmx.
        If you're after a really tiny codebase and leveraging as much of the
        web platform as possible, htmz looks to be a great choice. If you're
        after something a little bit in-between, I think <i-html>
        is the best next step.
      

      Why did you not extend include-fragment?

      
        The
        include-fragment-element
        is very similar to this, and I'm the current maintainer of both, so it
        is within my power (and hubris) to make changes to that element to make
        it more like this element. However, I think they are spiritually
        different elements. This element definitely builds upon the success of
        <include-fragment>, but it also changes some design
        decisions that aren't worth changing in
        <include-fragment>, given its large user base and the
        amount of churn it would take to make those changes. In that regard you
        might consider this element a "rewrite" or a "major version bump" of
        <include-fragment, but I think both have a valid use
        case and certainly both can co-exist, maybe even on the same website.
      

      
        What are the differences between this and include-fragment then?
      

      
        Aside from the obvious difference being the name,
        <inlude-fragment> has a smaller featureset, for example
        it doesn't support streaming mode, it doesn't support custom CSS states,
        it doesn't support preventing loadstart events. Those
        things could be added, but then it also has some big differences that
        are design decisions, and would be breaking changes.
        <include-fragment> replaces itself on the page,
        so a fully loaded fragment no longer exists on the page and the loaded
        HTML stands in its place. This is different to
        <i-html> which remains in the DOM, and can re-fetch
        contents again and again. This design change gives the two elements very
        different use cases, for example <i-html> respects link
        and form target= attributes which would seem pointless to
        do in <include-fragment> with this design choice in
        mind.
      
    