---
area: "Learn"
parent: "Links chapter"
url: "https://unpoly.com/learn/faux-interactive-elements"
---
<nav aria-label="Unpoly docs">[All docs](https://unpoly.com/index.md) · [Links chapter](https://unpoly.com/learn/links.md)</nav>

# Elements that act like links or buttons

Unpoly can make any element act like a link or a button: a table row that follows the link in it, a `<span>` that navigates, or a card that emits an event when clicked. Unlike a naive `click` listener, these elements stay usable with the keyboard and with screen readers.

<nav aria-label="Contents">

#### Contents

- [Making a row or card follow its link](#expanding)
- [Acting like a hyperlink](#acting-like-a-hyperlink)
- [Emitting an event on click](#emitting-an-event-on-click)
- [Arbitrary click effects](#arbitrary-click-effects)
- [Controls inside clickable elements](#controls)
- [Acting on press](#instant)
- [Accessibility](#accessibility)

</nav>

## Making a row or card follow its link {#expanding}

A table row or a card often contains a link to a details page. Set an [`[up-expand]`](https://unpoly.com/api/up-expand.md) attribute to make a click anywhere in the row follow that link:

```html
<tr up-expand>
  <td><a href="/users/5" up-follow>Alice</a></td>
  <td>alice@example.com</td>
  <td>Admin</td>
</tr>
```

A click on the email address does what a click on _Alice_ does. It follows the link to `/users/5`.

HTML doesn't let you wrap a `<tr>` in an `<a>` element, so [`[up-expand]`](https://unpoly.com/api/up-expand.md) is the way to make a whole row clickable. For a card, you can also wrap the whole card in an `<a>` element. [It's OK to put block elements inside a link](https://makandracards.com/makandra/43549-it-s-ok-to-put-block-elements-inside-an-a-tag), and only a real link lets users open the card in a new tab with a middle click or from the context menu. In an [`[up-expand]`](https://unpoly.com/api/up-expand.md) area, only the link itself offers that.

### Choosing the link {#choosing-the-link}

By default the area follows its first link. When a row contains several links, set [`[up-expand]`](https://unpoly.com/api/up-expand.md) to a selector for the one to follow:

```html
<tr up-expand=".details">
  <td><a href="/companies/3" up-follow>Acme Inc.</a></td>
  <td><a href="/users/5" class="details" up-follow>Alice</a></td>
  <td>alice@example.com</td>
</tr>
```

A click on _Acme Inc._ follows that link. A click anywhere else in the row follows _Alice_.

### Links that Unpoly doesn't follow {#links-that-unpoly-doesnt-follow}

When Unpoly doesn't follow the link, a click on the area clicks the link. A plain link makes a full page load, a `[download]` link downloads its file, and a `[target=_blank]` link opens a new tab:

```html
<tr up-expand>
  <td><a href="/invoices/7.pdf" download>Invoice #7</a></td>
  <td>€ 120.00</td>
</tr>
```

A click on the amount downloads the PDF.

To update a fragment instead, give the link an [`[up-follow]`](https://unpoly.com/api/up-follow.md) attribute, or [follow all links](https://unpoly.com/learn/handling-all-links.md).

### The area takes over the link's attributes {#the-area-takes-over-the-links-attributes}

When Unpoly follows the link, the area takes over the link's Unpoly attributes:

```html
<tr up-expand>
  <td><a href="/users/5" up-layer="new" up-preload>Alice</a></td>
  <td>alice@example.com</td>
</tr>
```

A click anywhere in this row opens Alice in an [overlay](https://unpoly.com/learn/overlays.md), and hovering over the row [preloads](https://unpoly.com/learn/preloading.md) her page. Attributes that your own [macros](https://unpoly.com/api/up.macro.md) set on the link are taken over as well.

A click on the area follows the link itself. Events like [`up:link:follow`](https://unpoly.com/api/up:link:follow.md) are emitted on the link, and the link is the [origin](https://unpoly.com/learn/targeting-fragments.md#referring-to-the-origin-element) of the fragment update. The link's `[data-method]` and `[data-confirm]` attributes from Rails also apply, e.g. for a delete link that asks for confirmation.

## Acting like a hyperlink {#acting-like-a-hyperlink}

When there is no link to expand, set an [`[up-follow]`](https://unpoly.com/api/up-follow.md) attribute on any element to make it behave like a link. Set the destination URL as an [`[up-href]`](https://unpoly.com/api/up-href.md) attribute:

```html
<span up-follow up-href="/details">Read more</span>
```

A click on the `<span>` [follows](https://unpoly.com/learn/following-links.md) the URL `/details`. The element works with any link feature, like `[up-target]`, [`[up-instant]`](https://unpoly.com/api/up-instant.md), [`[up-preload]`](https://unpoly.com/api/up-preload.md) or [`up.follow()`](https://unpoly.com/api/up.follow.md).

Prefer a real `<a>` element where you can, or an [`[up-expand]`](https://unpoly.com/api/up-expand.md) area around one. Only a real link lets users open its destination in a new tab, works without JavaScript and is followed by search engines. Use a faux link when you want to avoid exactly that, e.g. to keep a link from being opened in a new tab or from being followed by crawlers.

## Emitting an event on click {#emitting-an-event-on-click}

Set an [`[up-emit]`](https://unpoly.com/api/up-emit.md) attribute on any element to have it emit an event when clicked:

```html
<span up-emit="user:select" up-emit-props="{ id: 5 }">
  Alice
</span>
```

This can be a good solution for letting widely distributed elements communicate with each other.

Like a faux link, the [`[up-emit]`](https://unpoly.com/api/up-emit.md) element works with the keyboard and with screen readers. See [Accessibility](#accessibility).

## Arbitrary click effects {#arbitrary-click-effects}

Set an [`[up-clickable]`](https://unpoly.com/api/up-clickable.md) attribute on any non-interactive element to make it behave like a button:

```html
<span id="faux-button" up-clickable>Click me</span>
```

It's up to you to make the element appear interactive visually, e.g. by assigning a `.button` class from your design system.

To react to the element being activated, handle the [`up:click`](https://unpoly.com/api/up:click.md) event:

```js
let button = document.querySelector('#faux-button')

button.addEventListener('up:click', function(event) {
  console.log('Click on faux button!')
})
```

To make elements clickable without an explicit [`[up-clickable]`](https://unpoly.com/api/up-clickable.md) attribute, configure [`up.link.config.clickableSelectors`](https://unpoly.com/api/up.link.config.md#config.clickableSelectors).

## Controls inside clickable elements {#controls}

A clickable row often has controls of its own, like a checkbox to select the row or a button to archive it. Inside an [`[up-expand]`](https://unpoly.com/api/up-expand.md) area or an `[up-follow][up-href]` element, these controls keep their clicks:

```html
<tr up-expand>
  <td><input type="checkbox" name="user_ids[]" value="5"></td>
  <td><a href="/users/5" up-follow>Alice</a></td>
  <td>
    <form method="post" action="/users/5/archive" up-submit>
      <button>Archive</button>
    </form>
  </td>
</tr>
```

A click on the checkbox toggles it, and a click on _Archive_ submits the form. Only a click elsewhere in the row follows the link to `/users/5`. The same goes for other links, form fields, labels and `<summary>` elements within the area.

Elements with [`[up-emit]`](https://unpoly.com/api/up-emit.md) or [`[up-clickable]`](https://unpoly.com/api/up-clickable.md) don't make this distinction. They react to a click anywhere within them.

### Your own widgets {#your-own-widgets}

When an element in the area handles clicks with your own JavaScript, prevent its [`up:click`](https://unpoly.com/api/up:click.md) event, so the area doesn't also follow its link. This `<check-cell>` element toggles its checkbox when the user clicks anywhere in the cell:

```html
<tr up-expand>
  <td><check-cell><input type="checkbox" name="user_ids[]" value="5"></check-cell></td>
  <td><a href="/users/5" up-follow>Alice</a></td>
</tr>
```

```js
class CheckCell extends HTMLElement {
  constructor() {
    super()
    this.addEventListener('up:click', (event) => {
      let checkbox = this.querySelector('input[type=checkbox]')
      // A click on the checkbox already toggles it
      if (event.target === checkbox) return
      event.preventDefault()
      checkbox.click()
    })
  }
}

customElements.define('check-cell', CheckCell)
```

Preventing [`up:click`](https://unpoly.com/api/up:click.md) also prevents the underlying `click` event. That's why the listener leaves clicks on the checkbox itself alone.

### Inside a link {#inside-a-link}

Inside an `<a>` element, buttons and labels belong to the link, so a click on them follows it. HTML doesn't allow controls within a link anyway. When a clickable area needs controls, use [`[up-expand]`](https://unpoly.com/api/up-expand.md) instead.

## Acting on press {#instant}

Set an [`[up-instant]`](https://unpoly.com/api/up-instant.md) attribute to activate an element on `mousedown` instead of `click`:

```html
<span up-emit="menu:open" up-instant>Menu</span>
```

This works for faux links, [`[up-emit]`](https://unpoly.com/api/up-emit.md) and [`[up-clickable]`](https://unpoly.com/api/up-clickable.md) elements, and for [`[up-expand]`](https://unpoly.com/api/up-expand.md) areas, which copy [`[up-instant]`](https://unpoly.com/api/up-instant.md) from their link.

A press on a control within the element, like a checkbox, still waits for the `click`, so the control works as usual. The same goes for a press whose `mousedown` event was prevented by another script, e.g. to keep the focus in a search field. In both cases Unpoly emits [`up:click`](https://unpoly.com/api/up:click.md) on `click` instead of `mousedown`.

See [Acting on press](https://unpoly.com/learn/following-links.md#instant) for when [`[up-instant]`](https://unpoly.com/api/up-instant.md) makes sense.

## Accessibility {#accessibility}

Naively adding a `click` listener to a non-interactive element causes [accessibility issues](https://keepinguptodate.com/pages/2019/04/accessible-javascript-click-handlers/): the element cannot be focused, keyboard users cannot activate it, and screen readers won't announce it as interactive.

To prevent these issues, Unpoly adds the following behaviors to elements with `[up-follow][up-href]`, [`[up-emit]`](https://unpoly.com/api/up-emit.md) or [`[up-clickable]`](https://unpoly.com/api/up-clickable.md):

- The element is given a [`[role=link]`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/link_role) or [`[role=button]`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/button_role) attribute so screen readers announce it as an interactive element. To override Unpoly's choice of role, set a `[role]` attribute manually.
- Link-like elements show a [`pointer`](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor) cursor when hovered over.
- The element gets a [`[tabindex=0]`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) attribute so it can be focused with the keyboard. An existing `[tabindex]` attribute will be preserved.
- Pressing Enter activates the element. An element with a button role also activates with the Space key.
- The element emits an [`up:click`](https://unpoly.com/api/up:click.md) event when activated, by mouse or by keyboard.

> #### Note
>
> No additional behaviors will be added to `<a>` or `<button>` elements, as these are accessible by default.

An [`[up-expand]`](https://unpoly.com/api/up-expand.md) area only gets a pointer cursor. It gets no role and no `[tabindex]`, since the link inside it is already focusable and announced as a link. Keyboard and screen reader users use that link, and making the area a link as well would announce the same destination twice.

<nav aria-label="Reading order">

- [Previous: Navigation bars](https://unpoly.com/learn/navigation-bars.md)
- [Next: Forms](https://unpoly.com/learn/forms.md)

</nav>

