<h1>My first web component</h1>

<!-- Three Media Objects -->
<media-object data-avatar="round">
  <img slot="img" src="https://assets.codepen.io/15542/mia.jpg" alt="Miriam speaking" height="300" width="300" />
  <h2 slot="title">Miriam Suzanne</h2>
  <p slot="content">
    I was looking for the fastest way to create a web component,
    with the least custom JS required.
    This is my first attempt,
    using
    <a href="https://twitter.com/WestbrookJ/status/1369350640019922948">
      JS snippet provided by @WestbrookJ
    </a>
    on Twitter.
  </p>
  <p slot="content">
    It took a bit to figure out the exact interplay
    of shadow elements, attributes, slots, and selectors
    to get my styling right.
    I'm decently happy with the results,
    but do still have a few issues.
  </p>
</media-object>

<media-object data-theme="dark">Hello World</media-object>

<media-object class="grumpy">
  <img slot="img" src="https://assets.codepen.io/15542/sad.jpg" alt="little girl frowning" />
  <h2 slot="title">My main complaints:</h2>
  <ul slot="content">
    <li>
      I can't establish attributes/semantics on the host element
      from within the template
      (at least using declarative syntax).
    </li>
    <li>
      In the template, a <code>slot</code> wraps other elements,
      but in usage, a <code>slot</code> <em>is an element</em>.
      That makes it hard to style both the slot-default
      and slotted content in a consistent way.
    </li>
    <li>
      I'd love a declarative way to provide default semantics/attributes
      on both the template & slots,
      and have those "merge" with semantics/attrs
      defined in the outer document.
    </li>
  </ul>
</media-object>

<!-- The Source Template -->
<template id="media-object-template">
  <style>
    :host {
      display: contents;
      --media-grid--default: 'img text'auto / minmax(5em, 30%) 1fr;
      --media-grid--flip: 'text img'auto / 1fr minmax(5em, 30%);
    }

    :host([data-grid="flip"]) {
      --media-grid: var(--media-grid--flip);
    }

    :host([data-avatar="round"]) {
      --media-avatar: 100%;
    }

    @media (max-width: 60ch) {
      :host {
        --media-grid--override: 'img'minmax(4em, 1fr) 'text'auto / 100%;
      }
    }

    article {
      background-color: var(--media-bg, var(--bg, papayawhip));
      color: var(--media-text, var(--text, black));
      display: grid;
      grid-auto-flow: dense;
      grid-gap: var(--media-gutter, var(--gutter, 1em));
      grid-template: var(--media-grid--override, var(--media-grid, var(--media-grid--default)));
      padding: var(--media-gutter, var(--gutter, 1em));
    }

    [part='avatar'] {
      align-self: var(--media-align, start);
      display: flex;
      justify-self: center;
      grid-column: img;
    }

    [data-default=title],
    ::slotted([slot=title]) {
      color: var(--highlight, maroon);
      margin-bottom: 0;
    }

    /* I wish there was a way to select both in one */
    [data-default=img],
    ::slotted([slot=img]) {
      border-radius: var(--media-avatar, 0);
      max-width: 100%;
      max-height: 100%;
      height: auto;
      overflow: hidden;
      object-fit: var(--media-fit, cover);
    }

    [data-default=img],
    ::slotted([slot=img]:not([alt])) {
      outline: medium dotted tomato;
    }

    [data-default=content] {
      font-style: italic;
      opacity: 0.5;
    }
  </style>
  <!-- I don't see a clear way to add semantics to the template itself -->
  <article>
    <div part="avatar">
      <slot name="img">
        <img data-default="img" src="https://placeimg.com/300/300/any" height="300" width="300" alt="random person placeholder" />
      </slot>
    </div>
    <div part="content">
      <slot name="title">
        <!-- use slot=title to replace the full title -->
        <!-- or use the unnamed slot to just change the text -->
        <h2 data-default="title">
          <slot>Odd McBird</slot>
        </h2>
      </slot>
      <slot name="content">
        <p data-default="content">(empty)</p>
      </slot>
    </div>
  </article>
</template>
/* external style, via props */
media-object:nth-of-type(odd) {
  --media-grid: var(--media-grid--flip);
}

.grumpy {
  --media-align: stretch;
  --media-fit: cover;
  --media-gutter: 0.5em 1em;
}

/* external style, targeting slots */
.grumpy [slot="img"] {
  outline: medium dashed green;
}

/* external style, using global patterns */
[data-theme] {
  background: var(--bg, white);
  color: var(--text, black);
}

[data-theme="dark"] {
  --bg: maroon;
  --text: white;
  --highlight: papayawhip;
}

/* page layout */
body {
  --gutter: 0.5em;
  display: grid;
  grid-template-columns: minmax(min-content, 70ch);
  grid-gap: var(--gutter);
  justify-content: center;
  padding: var(--gutter);
}
// copypasta from https://twitter.com/WestbrookJ/status/1369350640019922948
customElements.define(
  "media-object",
  class extends HTMLElement {
    constructor() {
      super();
      let t = document.getElementById("media-object-template");
      let content = t.content;
      const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(
        content.cloneNode(true)
      );
    }
  }
);
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.