Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <!-- html structure
following a heading include paragraph elements with the custom element instead of the default anchor link
-->
<main>
    <h1>Custom Tooltip <span role="img">💬</span></h1>
    <!--
        each anchor link benefits from the following attributes
        - href for the referenced URL
        - tip for the text shown on hover
        - corner for the position of the tooltip (starting at 0 with the top left corner and going clockwise)

        <custom-link
            href="#"
            tip="You really should"
            corner="0">
            Go to page
        <custom-link>

    -->
    <p>
        Hello there. This is meant to be an entry for the
        <custom-link
            href="https://www.florin-pop.com/blog/2019/03/weekly-coding-challenge/"
            tip="Check it out!"
            corner="0">
            weekly coding challenge
        </custom-link>.

        It is also a perfect excuse to practice with the
        <custom-link
            href="https://developer.mozilla.org/en-US/docs/Web/Web_Components"
            tip="Plenty to learn"
            corner="0">
            Web Component API
        </custom-link>.
    </p>

    <p>
        What's so special about it? Well, turns out anchor links on this page are actually

        <custom-link
            href="https://developers.google.com/web/fundamentals/web-components/customelements"
            tip="Handcrafted with care"
            corner="0">
            custom elements
        </custom-link>,

        equipped with a rather

        <custom-link
            href="https://www.merriam-webster.com/dictionary/cutesy"
            tip="Hope that's a word"
            corner="3">
            cutsy
        </custom-link>

        tooltip.
    </p>
    <p>
        Hover or focus on them at will.
    </p>

    <p>
        Here's

        <custom-link
            href="#"
            tip="Right here!"
            corner="1">
            one
        </custom-link>

        , here's

        <custom-link
            href="#"
            tip="Oh well..."
            corner="1">
            another
        </custom-link>.

        Getting old quickly?

        <custom-link
            href="#"
            tip="Hope not"
            corner="2">
            Nah
        </custom-link>.
    </p>


    <!--
        reference the pen showcasing a single custom tooltip,
        built without the web components API
    -->
    <p>
        If you are on Edge, you might enjoy
        <custom-link
            href="https://codepen.io/borntofrappe/full/mYaPwg"
            tip="Not supported yet..."
            corner="0">
            this proof of concept
        </custom-link>.
    </p>

</main>

              
            
!

CSS

              
                @import url("https://fonts.googleapis.com/css?family=Lato|Poppins:300&display=swap");

* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}

body {
  font-family: "Open Sans", sans-serif;
  background: #fff;
  color: #1d1e22;
  background: #111214;
  font-family: "Lato", sans-serif;
}
/* horizontally center the main container in the viewport */
main {
  max-width: 400px;
  margin: 0 auto;
  padding: 2rem 1.5rem;
  line-height: 1.5;
  min-height: 100vh;
  background: #fff;
  box-shadow: 0 0 5px -4px currentColor;
}
/* create noticeable whitespace around the elements of the page */
h1 {
  font-family: "Poppins", sans-serif;
  font-weight: 300;
  margin-bottom: 1.25rem;
}

p {
  margin: 1rem 0;
  line-height: 2.5;
}

/* reduce the size of the last paragraph, used as a final note */
p:last-of-type {
  margin-top: 3rem;
  font-size: 0.8rem;
  text-align: right;
}

              
            
!

JS

              
                // create a reusable element through a template tag
const template = document.createElement('template');

/* element structure
<a>
  text shown on page
  <div>text shown on hover/focus</div>
</a>

by default the div container includes three dots
*/
template.innerHTML = `
<style>
  a {
    text-decoration: none;
    color: inherit;
    font-weight: bold;
    position: relative;
    line-height: 1.5;
    font-size: 1em;
  }

  a .tooltip {
    position: absolute;
    font-size: 0.9em;
    font-weight: normal;
    padding: 0.25rem 0.75rem;
    white-space: nowrap;
    background: #0580ed;
    color: #fff;
    box-shadow: 0 0 5px -3px #000;

    transition: all 0.2s ease-out;
    transform: scale(0);
  }

  a:hover .tooltip,
  a:focus .tooltip {
    transform: scale(1);
  }

  a .tooltip span.tool {
    display: inline-block;
  }

  a:hover .tooltip span.tool,
  a:focus .tooltip span.tool {
    animation: wait 0.75s 0.3s ease-out 2;
  }
  a .tooltip span.tool:nth-of-type(2) {
    animation-delay: 0.45s;
  }
  a .tooltip span.tool:nth-of-type(3) {
    animation-delay: 0.6s;
  }

  @keyframes wait {
    25% {
      transform: translateY(-3px);
    }
    50% {
      transform: translateY(3px);
    }
    75% {
      transform: translateY(0);
    }
  }
</style>

<a>
  <slot></slot>
  <div class="tooltip"><span class="tool">•</span><span class="tool">•</span><span class="tool">•</span></div>
</a>
`;

// class describing the custom element
class CustomLinkComponent extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });

    // boolean to manage the state of the tooltip
    this.isShowing = false;

    // functions updating the tooltip's appearance and markup
    this.updateTooltip = this.updateTooltip.bind(this);
    this.showTooltip = this.showTooltip.bind(this);
    this.hideTooltip = this.hideTooltip.bind(this);
  }

  /* function called to update the tooltip
  - when the three dots have done animating the function updates the tooltip with the informative text and scales the tooltip up
  - when the tooltip is scaled down following the blur/mouseout event the function resets the tooltip's html and removes the inline transform property
  */
  updateTooltip() {
    const { isShowing } = this;
    const tooltip = this.shadowRoot.querySelector('a .tooltip');
    tooltip.innerHTML = isShowing ? this.getAttribute('tip') : Array(3).fill('').map(dot => '<span class="tool">•</span>').join('');
    tooltip.style.transform = isShowing ? 'scale(1)' : '';

    // remove the event listener to be able to fire it once more through the showTooltip/hideTooltip functions
    tooltip.removeEventListener('transitionend', this.updateTooltip);
  }

  // function called following the mouseenter/focus events
  showTooltip() {
    // proceed only if the tooltip is not in the process of being shown
    const { isShowing } = this;
    if (!isShowing) {
      this.isShowing = true;
      // target the .tooltip container, the nested span elements
      const tooltip = this.shadowRoot.querySelector('a .tooltip');
      const spans = tooltip.querySelectorAll('span');
      // identify the last span (last to be animated)
      const lastSpan = spans[spans.length - 1];

      // when the last span has done animating, scale the tooltip back to 0
      lastSpan.addEventListener('animationend', () => {
        tooltip.style.transform = 'scale(0)';
        // as the tooltip finishes its transition, call the update function to show the tooltip's text
        tooltip.addEventListener('transitionend', this.updateTooltip);
      });
    }
  }

  // function called following the mouseout/blur events
  hideTooltip() {
  // ! proceed only if the tooltip is in the process of being shown
    const { isShowing } = this;
    if (isShowing) {
      this.isShowing = false;
      // target the tooltip
      const tooltip = this.shadowRoot.querySelector('a .tooltip');
      // scale the tooltip to 0
      tooltip.style.transform = 'scale(0)';
      // as the tooltip disappears from sight call the update function to reset the tooltip's markup
      tooltip.addEventListener('transitionend', this.updateTooltip);
    }
  }

  // when instantiated create a copy of the template
  connectedCallback() {
    this.shadowRoot.appendChild(template.content.cloneNode(true));
    // target the elements in the template which need updating
    const link = this.shadowRoot.querySelector('a');
    const tooltip = link.querySelector('.tooltip');

    // set the href attribute through the href attribute of the custom element itself
    const href = this.getAttribute('href');
    link.setAttribute('href', href);
    // if the link references an actual page open the page in another tab
    if (href !== '#') {
      link.setAttribute('target', '_blank');
    }

    // retrieve the corner to modify the appearance of the tooltip box
    const corner = Number.parseInt(this.getAttribute('corner'), 10);

    /* based on the corner value update the following properties
    -
    -
    */
    // border-radius to have 20px for every corner except the one closes to the anchor link element
    const borderZero = (corner + 2) % 4;
    const borderRadius = Array(4).fill('20px');
    borderRadius[borderZero] = '0';
    tooltip.style.borderRadius = borderRadius.join(' ');

    // top, right, bottom, left to position the tooltip
    if (corner < 2) {
      tooltip.style.bottom = '100%';
    } else {
      tooltip.style.top = '100%';
    }
    if (corner % 3 === 0) {
      tooltip.style.right = '100%';
    } else {
      tooltip.style.left = '100%';
    }

    // transform-origin to determine from where the tooltip should spawn
    const originX = corner % 3 === 0 ? '100%' : '0%';
    const originY = corner < 2 ? '100%' : '0%';
    tooltip.style.transformOrigin = `${originX} ${originY}`;

    // attach event listeners on the anchor link to show/hide the tooltip as needed
    link.addEventListener('focus', this.showTooltip);
    link.addEventListener('mouseenter', this.showTooltip);

    link.addEventListener('blur', this.hideTooltip);
    link.addEventListener('mouseout', this.hideTooltip);
  }
}

// define the custom-link element referencing the custom element
window.customElements.define('custom-link', CustomLinkComponent);

              
            
!
999px

Console