I was reading this article by Chris where he talks about block links — you know, like wrapping an entire card element inside an anchor — being a bad idea. It’s bad accessibility because of how it affects screen readers. And it’s bad UX because it prevents simple user tasks, like selecting text.
But maybe there’s something else at play. Maybe it’s less an issue with the pattern than the implementation of it. That led me to believe that this is the time to write follow-up article to see if we can address some of the problems Chris pointed out.
Throughout this post, I’ll use the term “card” to describe a component using the block link pattern. Here’s what we mean by that.
Let’s see how we want our Card Components to work:
- The whole thing should be linked and clickable.
- It should be able to contain more than one link.
- Content should be semantic so assistive tech can understand it.
- The text should be selectable, like regular links.
- Things like right-click and keyboard shortcuts should work with it
- Its elements should be focusable when tabbing.
That’s a long list! And since we don’t have any standard card widget provided by the browser, we don’t have any standard guidelines to build it.
Like most things on the web, there’s more than one way to make a card component. However, I haven’t found something that checks all the requirements we just covered. In this article, we will try to hit all of them. That’s what we’re going to do now!
<a>
Method 1: Wrap everything an This is the most common and the easiest way to make a linked card. Take the HTML for the card and wrap the entire thing in an anchor tag.
<a href="/">
<!-- Card markup -->
</a>
Here’s what that gives us:
- It’s clickable.
- It works with right-click and keyboard shortcuts.
Well, not great. We still can’t:
- Put another link inside the card because the entire thing is a single link
- Use it with a screen reader — the content is not semantic, so assistive technology will announce everything inside the card, starting from the time stamp
- Select text
That’s enough 👎 that we probably shouldn’t use it. Let’s move onto the next technique.
Method 2: Just link what needs linking
This is a nice compromise that sacrifices a little UX for improved accessibility.
With this pattern we achieve most of our goals:
- We can put as many links as we want.
- Content is semantic.
- We can select the text from Card.
- Right Click and keyboard shortcuts work.
- The focus is in proper order when tabbing.
But it is missing the main feature we want in a card: the whole thing should be clickable! Looks like we need to try some other way.
Method 3: The good ol’ ::before pseudo element
In this one, we add a ::before
or ::after
element, place it above the card with absolute positioning and stretch it over the entire width and height of the card so it’s clickable.
But now:
- We still can’t add more than one link because anything else that’s linked is under the pseudo element layer. We can try to put all the text above the pseudo element, but card link itself won’t work when clicking on top of the text.
- We still can’t select the text. Again, we could swap layers, but then we’re back to the clickable link issue all over again.
Let’s try to actually check all the boxes here in our final technique.
Method 4: Sprinkle JavaScript on the second method
Let’s build off the second method. Recall that’s what where we link up everything we want to be a link:
<article class="card">
<time datetime="2020-03-20">Mar 20, 2020</time>
<h2><a href="https://css-tricks.com/a-complete-guide-to-calc-in-css/" class="main-link">A Complete Guide to calc() in CSS</a></h2>
<p>
In this guide, let’s cover just about everything there is to know about this very useful function.
</p>
<a class="author-name" href="https://css-tricks.com/author/chriscoyier/" target="_blank">Chris Coyier</a>
<div class="tags">
<a class="tag" href="https://css-tricks.com/tag/calc/" >calc</a>
</div>
</article>
So how do we make the whole card clickable? We could use JavaScript as a progressive enhancement to do that. We’ll start by adding a click
event listener to the card and trigger the click on the main link when it is triggered.
const card = document.querySelector(".card")
const mainLink = document.querySelector('.main-link')
card.addEventListener("click", handleClick)
function handleClick(event) {
mainLink.click();
}
Temporarily, this introduces the problem that we can’t select the text, which we’ve been trying to fix this whole time. Here’s the trick: we’ll use the relatively less-known web API window.getSelection
. From MDN:
The
Window.getSelection()
method returns aSelection
object representing the range of text selected by the user or the current position of the caret.
Although, this method returns an Object, we can convert it to a string with toString()
.
const isTextSelected = window.getSelection().toString()
With one line and no complicated kung-fu tricks with event listeners, we know if the user has selected text. Let’s use that in our handleClick
function.
const card = document.querySelector(".card")
const mainLink = document.querySelector('.main-link')
card.addEventListener("click", handleClick)
function handleClick(event) {
const isTextSelected = window.getSelection().toString();
if (!isTextSelected) {
mainLink.click();
}
}
This way, the main link can be clicked when no text selected, and all it took was a few lines of JavaScript. This satisfies our requirements:
- The whole thing is linked and clickable.
- It is able to contain more than one link.
- This content is semantic so assistive tech can understand it.
- The text should be selectable, like regular links.
- Things like right-click and keyboard shortcuts should work with it
- Its elements should be focusable when tabbing.
We have satisfied all the requirements but there are still some gotchas, like double event triggering on clickable elements like links and buttons in the card. We can fix this by adding a click event listener on all of them and stopping the propagation of event.
// You might want to add common class like 'clickable' on all elements and use that for the query selector.
const clickableElements = Array.from(card.querySelectorAll("a"));
clickableElements.forEach((ele) =>
ele.addEventListener("click", (e) => e.stopPropagation())
);
Here’s the final demo with all the JavaScript code we have added:
I think we’ve done it! Now you know how to make a perfect clickable card component.
What about other patterns? For example, what if the card contains the excerpt of a blog post followed by a “Read More’ link? Where should that go? Does that become the “main” link? What about image?
For those questions and more, here’s some further reading on the topic:
- Cards by Heydon Pickering
- Block Links, Cards, Clickable Regions, Rows, Etc. by Adrian Roselli
- Block Links Are a Pain (and Maybe Just a Bad Idea) by Chris Coyier
- Pitfalls of Card UIs by Dave Rupert
You might want to test this in Firefox. Two new tabs open in Firefox when clicking on a button or link within the card (instead of only one). When I click on the “math” button, for example, two different pages are opened in new tabs.
Yeah I can confirm that in Firefox as well right now. And Chrome opens the unexpected link there.
Probably need to test
event.target
and have some slightly different handling when the click is on a link that isn’t amain-link
.Could use
.contains()
on click check to see if the clicked element is contained within the parent node.Alternatively, go in reverse and use
.closest()
to check from the child node to see if it’s contained within a parent node.Also, to make this more robust, you could uses these to include tests to make sure that there’s no
onclick
,href
or form elements in the chain.I have found a fix and will update the blog soon. Meanwhile you can check the pen https://codepen.io/vikas-parashar/pen/qBOwMWj for the proposed fix.
I messed around with method 3 and css pointer-events to get a non-js solution that gets you most of the way. The only thing missing is text selection. https://codepen.io/dillonbheadley/pen/BaogrGm
Using
display: grid
you can add z-index to the child elements without affecting the absolutely positioned pseudo element. This allows you move the content above the pseudo link. Then usingpointer-events: none
on anything not an anchor prevents anything from blocking the click anywhere on the card.I’d argue that Method 3 with a tiny tweak is the best solution. Simply use z-index to bring forward the extra links within the card. No need for JS, which the user may be blocking anyway and it’s better for performance.
We can bring the links forward with z-index but we still can’t select the text. Now, it is alright depending on use case where you don’t mind the lack of it.
We can make the text selectable by also bringing the text forward using z-index but now there are areas where clicking doesn’t work.
As regarding the use of JS, this will work like method 2, whole card won’t be clickable but it will progressive degradation, which IMO, is okay for few cases where JS is disabled.
True Vikas, I’d rushed through your list of requirements and missed the one where while everything needs to be clickable, the text also needs to be selectable. You’re not going to be able to get around that without some JS voodoo like the example you provided. Usually I’d consider “everything needs to be clickable” and “text needs to be selectable” to be contradictions, so that’s an interesting workaround. I’m honestly struggling to think of a use-case for this. If a user is particularly interested in the content present on the linked card, I feel like they should click the card and potentially see even more information they’re interested in. It is, after all, meant to be a quick blurb about a page of content they’re being enticed to read.
I was thinking the exact thing :)
Cool demo! Now do it with no JS
it will work just like method 2 with JS disabled :)
However Method 2 has its flaws, like Chris mentioned! A no-JS version of the final method might be a fun challenge.
For method #3 you can play with
z-index
to put the other links in front of the pseudo element, but yeah still no selectable text.This is nice – we’ve struggled with this one over at ABC. One minor thing I noticed is I don’t get the URL preview in the bottom left unless I hover directly over the
<a>
. But even so, this beats our attempt!Yeah, URL preview is still visible when hovered on actual links and that is one of the compromise which comes with final solution but it is preferred since it improves the usability(text selection, whole card clickable) and accessibility.
One thing is still missing with this technique: You hover, but you can’t see where you go when you click (browsers display a tooltip on the bottom left of a page)
Can’t tell how many users actually know/use that feature apart from Devs, but still.
yeah that is one of the compromise which comes with the final solution.
Method 3. Yes, we can add more links inside.
Just add:
.author-name, .tag {
position: relative;
z-index: 2;
}
Yeah, we can do this way.
Here is a fixed version which works in Firefox also:
Nice solution but it doesn’t work when the link has child elements(like Chris’s avatar) in that case again two events are triggered. I have found a fix and will update the blog soon. Meanwhile you can check the pen https://codepen.io/vikas-parashar/pen/qBOwMWj for the proposed fix.
Could you not simply use an absolute positioned link within the card? Put a visually hidden span in there to keep it accessible. Since the link is positioned absolute the other links in the card will still work. Use z-index to bring them forward.
To me this seems like this is the best solution because you don’t have to fight JS at all. I’ve encountered JS-based solutions for this problem on sites I’ve worked on it’s always been a hassle to update because the JS makes it harder to debug when you need to make edits etc.
This doesn’t account for text selection. That might be totally fine for you, but the point of this article is making a strict set of requirements for it to be “perfect” (or as close as we can get) and trying to shoot for that.
Sweet. I also went this way the last time I had to build something like this.
But there are two things which are still missing and I think are worse UX than method 2 alone:
– url-preview (mentioned in other comments)
– open in new tab when clicking the whole card. I think it’s not clear for the user why he/she sometimes gets the correct context menu and sometimes not.
I’m very sceptical about the “let’s link the whole card” stuff. Personally, I prefer method 2. But you can spend hours discussing about this ;)
Nice article though!
Andy Bell explored some of the same ground in this article: https://hankchizljaw.com/wrote/create-a-semantic-breakout-button-to-make-an-entire-element-clickable/
However, he makes the assumption, which seems to be a good one, that there needs only to be a single link in a card component.
I don’t really understand this desire for selectable text on links.
When you have regular link, you can’t select the part of it when you start your selection inside of this link – you have to start before or after the link toi be able to select it. It works here exactly the same.
This is a classic frontend conundrum, but to me it seems like an odd UX requirement to have a “second” link inside a card that should be entirely clickable.
I see why – specifically for the Author’s name in this case – it might be useful, but what I think that basically boils down to, the whole card should not be clickable if we need secondary links in the card.
If the card is all one link, it seems to make sense to use an anchor to wrap it, otherwise marking up inner links for each component (title, image, author) seems to make the most sense.
You can use the Apple’s site solution. It’s a div with class .unit-wrapper and the main anchor with .unit-link class. So everything inside (except anchors) get the property “pointer-events:none”.
// Turn an area into a link block
// While maintaining interation with other links inside
// add class .unit-link to the main anchor
a.unit-link {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 3;
}
// add class .unit-wrapper to the parent el
<
div>
.unit-wrapper {
position: relative;
overflow: hidden;
}
.unit-wrapper > div {
position: relative;
z-index: 4;
pointer-events: none;
}
.unit-wrapper a:not(.unit-link) {
z-index: 4;
pointer-events: all;
}
Sweet. I also went this way the last time I had to build something like this.
But there are two things which are still missing and I think are worse UX than method 2 alone:
– url-preview (mentioned in other comments)
– open in new tab when clicking the whole card. I think it’s not clear for the user why he/she sometimes gets the correct context menu and sometimes not.
I’m very sceptical about the “let’s link the whole card” stuff. Personally, I prefer method 2. But you can spend hours discussing about this ;)
Nice article though!
And has this been tested with multiple cards? Only the first card in the batch behaves as intended. The other cards are ignored – meaning the entire card isn’t clickable.
I have found this issue also. Needs to work for multiple cards
Hi Vikas,
I really liked your article, your solution sounds like a good mid-point that works for most use cases.
I tried making a bit of a web component inspired on this (the encapsulation works great for this)
https://webcomponents.dev/preview/dyflJofod72crJWUDyyo
Basically it replaces the top level container and you can set which selector to use to get the main link using the
main-link
attribute.The code would look something like this:
And since all the content is in a slot you can style the elements pretty much as you would usually.
It’s still a super basic implementation, if you don’t mind I could brush it up (mainly docs and a11y-wise) and publish to npm (obviously crediting you and your article in any way you wish)
Yeah, go ahead! maybe polish up it a bit and include the other links which I have linked in the post for the reference. You can link this article and my twitter(@vicode_in) in credits if its alright.
Sure, I will add the articles quoted here together with this one
I’ll ping you on twitter once I’ve polished the component up a bit more
How do you get this to work with more than one item? As you can see, only the first article is entirely clickable.
I have the same question.
@Leone, I ended up using Sara Soueidan’s implementation which works rather well and no JS required. https://css-tricks.com/breakout-buttons/
Thanks for letting me know!
I tried Sara Soueidan’s implementation, but for some reason I can’t get it work on mobile. When I click on the card the hover effect on the button (which I didn’t click) gets triggered, but the page doesn’t navigate to the url.
So I had to fix our original problem, that only the first card was working, with some jQuery:
@Leone – your solution in this comment https://css-tricks.com/block-links-the-search-for-a-perfect-solution/#comment-1769257 worked a treat for me.
Thanks
I’m not a big fan of making the whole card clickable, especially where using multiple links in the card. As a user, would you expect that clicking the ‘Chris Coyier’ link would go to one link, and then clicking just outside of it a smidge would go to another link?