Create a link inside a link

Yesterday I needed to display a table row that linked to a detailed view of a dataset, and inside a cell of this table I needed to link to an ’edit’ view of this dataset. My table consisted of divs inside a bigger div which itself was the body of an outer a. This is what it looked like:

<a href="/foo">  
  <div class="row">
    <div class="cell">
      <!-- cell content here -->
    </div>
    <div class="cell">
      <!-- cell content here -->
    </div>
    <div class="cell">
      <!-- cell content here -->
      <a href="/bar">Edit this</a>
    </div>
  </div>
</a>  

Perhaps you are already thinking: "Wait. This is not possible!"; And you would be right. The spec explicitly disallows links inside links, because of possible constructs like this
<a href="/foo">outer <a href="/bar">inner link</a> link</a>. There would be ambiguity on how the links (esp. the "link" word for the outer link) should be styled. That's why browsers usually close the opened (outer) link themselves before opening the other (inner) link element. Which leaves you with a word "link" (from the outer link) that does not link to anything.

Now if you search the web for this topic you can find some solutions. The ’easiest’ might be to use JavaScript for the actual linking. And this is also how I solved this. But there are a few things worth thinking about:

If you remove the <a> from the markup and do the linking yourself, some things happen:

  • The browser no longer recognizes this element as a link.
  • The person navigating your site will no longer recognize it as a link.
  • The browser cannot use its status bar to show you the “link” destination.
  • Screen readers and other assistive technologies won’t be able to tell their users that this is a link.
  • Some devices and some users do not support or use JavaScript.

You wont be able to solve all this issues. If people don’t enable JavaScript for your website, there is nothing you can do about it. So here’s what you can and should do:

  • Set the role attribute: <button role="link">. This will help assistive technologies to mark the element as a link. Furthermore you should set a attribute tabindex. This enables navigating highlighting the element by using alt-TAB for your users.
  • Set up a specific class for this element. Something like class="link", so you can define hover effects and style it with text-attributes. Do anything you need and can do, to make the element appear like other links on your site.
  • You have to accept the fact, that the link destination won’t appear in the browsers status bar. As far as I know there is nothing you can do about this. (If YOU do know, please let me know!)

Here’s some markup how this could look:

<button title="link to a cool site"  
      class="btn bg-blue"
      href="https://5minutenpause.com"
      role="link"
      target="_self"
      onkeydown="navigateLink(event)"
      onclick="navigateLink(event)"
>Click me, I am a link!</button>

Notice the onclick and onkeydown event handlers. These are necessary to have the JavaScript take over and make it a link.

The JavaScript to make it work

Now you have your ’link’ but other than look good, it doesn’t do much yet. Below I have written some JavaScript that’s adapted from Mozilla’s docs about using the role="link" attribute. Most importantly, I made retrieving the href and target attributes a recursive function. I noticed with my first implementation that, depending on what elements where placed inside my custom link, the event.target wouldn’t have the desired attributes. But a parent element would have these. So I just traverse the DOM upwards until I find the attributes and then return those.

The base implementation comes from Mozilla’s docs and supports the navigation via keyboard. That's why we act onkeydown also and check for the keycodes for ENTER and SPACE.

// handles clicks and keydowns on the link

function navigateLink(evt) {  
  const sap = { ui: { keycodes: { SPACE:32, ENTER:13 } } };
  if (evt.type === 'click' || evt.keyCode === sap.ui.keycodes.SPACE ||
    evt.keyCode === sap.ui.keycodes.ENTER) {
    evt.preventDefault();
    const ref = evt.target !== null ? evt.target : evt.srcElement,
          attrs;

    if (ref) {
      attrs = getHref(ref);
      window.open(attrs['href'], attrs['target']);
    }
  }
}

function getHref(el) {  
  const href = el.getAttribute('href'),
        target = el.getAttribute('target');

  return href !== null ? { href: href, target: target } : getHref(el.parentElement);
}

If you styled the custom link with a nice cursor: pointer; you now should be set to go live with this.

Let me know how it works for you.

Edit: I changed the occurrences of <span> to <button>, because it's semantically more correct. Thanks to my friend Tino for this tip.

Resources:
- https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIATechniques/Usingthelinkrole