consdata.com
Blog techniczny Blog biznesowy Dział HR
EN
angular

Czy wiesz, jak stworzyć klikalną kartę (Card component) zgodnie z WCAG?

author Mariusz Bartosik
13 maja 2026

Budowa karty, która jest w całości klikalna, a jednocześnie zawiera w sobie dodatkowe akcje (jak przycisk „Ulubione”), to jedno z częstych wyzwań UX/UI.

Opisywany komponent karty

Klikalny komoponent karty

Na czym polega problem?

Zgodnie ze standardami HTML i WCAG, nie wolno umieszczać elementów interaktywnych wewnątrz innych elementów interaktywnych.

Taka konstrukcja prowadzi do błędów w drzewie dostępności, utrudniając poprawną nawigację przy użyciu czytników ekranowych.

Co chcemy osiągnąć?

Naszym celem jest rozdzielenie warstwy wizualnej od semantyki:

Dla użytkownika:

  • cała karta wygląda jak klikalna
  • kursor zmienia się na „łapkę” na całej powierzchni

Dla technologii asystujących:

  • linkiem jest tylko konkretny element (np. tytuł)
  • przyciski są osobnymi, niezależnymi kontrolkami

Rozwiązanie: stretched link

Zamiast owijać całą kartę w <a>, traktujemy:

  • link
  • przyciski

jako oddzielne elementy.

Kluczowa idea

Link umieszczamy tam, gdzie ma on sens semantyczny, najczęściej w tytule. Następnie:

  • Rozciągamy link: za pomocą ::after sprawiamy, że link z nagłówka pokrywa całą powierzchnię karty.
  • Warstwujemy akcje: przyciski (np. „Ulubione”) wyciągamy na wierzch przez z-index, by pozostały niezależne i klikalne.
@Component({
  selector: 'app-accessible-card',
  template: `
    <article class="card">
      <div class="content">
        <header>
          <span class="badge"></span>
          <h3 class="title">
            <a [href]="linkUrl()" class="overlay-link"></a>
          </h3>
           
          <button
            type="button"
            class="fav-btn"
            [attr.aria-label]="isFavorite() ? 'Usuń z ulubionych' : 'Dodaj do ulubionych'"
            [class.is-fav]="isFavorite()"
            (click)="toggleFavorite($event)">
            <span aria-hidden="true">❤</span>
          </button>
        </header>
 
        <p class="desc"></p>
 
        <footer>
          <button type="button" class="btn-primary" (click)="onActionClick($event)">
            Szczegóły
          </button>
        </footer>
      </div>
    </article>
  `,
  styleUrl: './accessible-card.component.css'
})
export class AccessibleCardComponent {
  title = input.required<string>();
  description = input.required<string>();
  category = input<string>('Artykuł');
  linkUrl = input.required<string>();
  isFavorite = input<boolean>(false);
 
  favoriteToggled = output<boolean>();
  actionTriggered = output<void>();
 
  toggleFavorite(event: Event) {
    event.stopPropagation();            // Zapobiega aktywacji linku głównego
    this.favoriteToggled.emit(!this.isFavorite());
  }
 
  onActionClick(event: Event) {
    event.stopPropagation();            // Zapobiega aktywacji linku głównego
    this.actionTriggered.emit();
  }
}

Kluczowe fragmenty CSS

.card {
  position: relative;
}
 
.overlay-link {
 
  &::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 1;             /* Warstwa podstawowa */
  }
 
  /* Wyraźny focus klawiatury widoczny na obrysie całej karty */
  &:focus-visible::after {
    outline: 3px solid #3b82f6;
    outline-offset: 4px;
  }
}
 
/* Przyciski interaktywne wyciągnięte "NAD" link */
.fav-btn,
.btn-primary {
  position: relative;
  z-index: 2;               /* Przebija się przez warstwę linku głównego */
}

Co wyróżnia to podejście?

  • Prawidłowa semantyka: czytnik ekranu ogłasza tylko krótki, konkretny tytuł jako link.
  • Niezależne akcje: możemy dodać przycisk „Ulubione”, „Koszyk” czy „Tagi”, m.in. dzięki z-index: 2.
  • Zgodność z HTML5: unikamy błędu zagnieżdżania przycisku wewnątrz linku.

Demo

Demo dostępne pod linkiem: StackBlitz

Źródła

  • Bootstrap - stretched-link
  • Piccalilli - Accessible faux-nested interactive controls
Najnowsze wpisy

  • Czy wiesz, jak stworzyć klikalną kartę (Card component) zgodnie z WCAG?
  • Czy wiesz, że Angular 21 rozszerza API formularzy o Signal Forms?
  • Pułapki adnotacji @Transactional
Dołącz do nas

  • SENIOR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 14 900 - 20 590 PLN brutto
    B2B 19 680 - 27 220 PLN netto
  • REGULAR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 11 300 - 15 900 PLN brutto
    B2B 14 950 - 21 000 PLN netto
  • ZOBACZ WSZYSTKIE OGŁOSZENIA

Podobne wpisy

post-image
angular

Czy wiesz, jak stworzyć klikalną kartę (Card component) zgodnie z WCAG?

Budowa karty, która jest w całości klikalna, a jednocześnie zawiera w sobie dodatkowe akcje (jak przycisk „Ulubione”), to jedno z częstych wyzwań UX/UI.

author
Mariusz Bartosik 13 maj 2026
post-image
angular

Czy wiesz, że Angular 21 rozszerza API formularzy o Signal Forms?

Wraz z publikacją Angulara w wersji 21 opracowano nowy system definicji formularzy za pomocą sygnałów.

author
Wojciech Kulczak 24 kwi 2026
post-image
java

Pułapki adnotacji @Transactional

Najczęstsze pułapki związane z @Transactional w Springu i Hibernate - od proxy i self-invocation, przez dirty checking, po cache Hibernate.

author
Kamil Dudek 10 kwi 2026
Dołącz do nas

  • SENIOR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 14 900 - 20 590 PLN brutto
    B2B 19 680 - 27 220 PLN netto
  • REGULAR FULLSTACK DEVELOPER (JAVA + ANGULAR) Poznań (hybrydowo) lub zdalnie UoP 11 300 - 15 900 PLN brutto
    B2B 14 950 - 21 000 PLN netto
  • ZOBACZ WSZYSTKIE OGŁOSZENIA
consdata.com
  • Kontakt

    • sales@consdata.com
    • +48 61 41 51 000

  • Biuro

    • K9Office
      Krysiewicza 9/14
      61-825 Poznań
      Polska

  • Rozwiązania

    • Eximee
    • Kouncil
  • Blog Dołącz do nas
Copyright © 2024 Consdata. All rights reserved. Privacy Policy & Cookies
Chcemy używać plików cookie oraz skryptów podmiotów trzecich do polepszania funkcjonowania tej strony Zgadzam się