Tips & Tricks

Cara Menyederhanakan Accessibility dengan Claude Code

Pelajari cara menyederhanakan accessibility menggunakan Claude Code. Dilengkapi contoh kode praktis dan panduan langkah demi langkah.

Keuntungan Menggunakan Claude Code untuk Accessibility

Web accessibility sangat penting, tetapi memahami standar WCAG dan penggunaan atribut ARIA yang benar cukup kompleks. Claude Code dapat melakukan code generation dengan mempertimbangkan best practice accessibility dan audit kode yang sudah ada secara efisien.

Audit Accessibility Kode yang Sudah Ada

> Review semua component di bawah src/components/ dari perspektif accessibility.
> Periksa berdasarkan standar WCAG 2.1 AA, dan buat daftar masalah beserta solusinya.

Claude Code mendeteksi masalah-masalah berikut:

  • Atribut alt pada gambar tidak ada
  • Elemen form tidak terhubung dengan elemen label
  • Rasio kontras warna tidak memadai
  • Elemen interaktif tidak bisa dioperasikan dengan keyboard
  • Atribut ARIA digunakan dengan tidak tepat

Membuat Component yang Accessible

> Buat modal dialog yang accessible.
> Implementasikan focus trap, Close dengan ESC key, dan background scroll lock.
import { useEffect, useRef, useCallback } from 'react';

interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  title: string;
  children: React.ReactNode;
}

function Modal({ isOpen, onClose, title, children }: ModalProps) {
  const modalRef = useRef<HTMLDivElement>(null);
  const previousFocus = useRef<HTMLElement | null>(null);

  // Focus trap
  const trapFocus = useCallback((e: KeyboardEvent) => {
    if (!modalRef.current) return;

    const focusable = modalRef.current.querySelectorAll<HTMLElement>(
      'a[href], button:not([disabled]), input:not([disabled]), select, textarea, [tabindex]:not([tabindex="-1"])'
    );

    const first = focusable[0];
    const last = focusable[focusable.length - 1];

    if (e.key === 'Tab') {
      if (e.shiftKey && document.activeElement === first) {
        e.preventDefault();
        last.focus();
      } else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault();
        first.focus();
      }
    }

    if (e.key === 'Escape') onClose();
  }, [onClose]);

  useEffect(() => {
    if (isOpen) {
      previousFocus.current = document.activeElement as HTMLElement;
      document.body.style.overflow = 'hidden';
      document.addEventListener('keydown', trapFocus);

      // Fokus ke elemen focusable pertama di dalam modal
      setTimeout(() => {
        modalRef.current?.querySelector<HTMLElement>('[autofocus], button')?.focus();
      }, 0);
    }

    return () => {
      document.body.style.overflow = '';
      document.removeEventListener('keydown', trapFocus);
      previousFocus.current?.focus();
    };
  }, [isOpen, trapFocus]);

  if (!isOpen) return null;

  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center"
      role="presentation"
    >
      <div
        className="fixed inset-0 bg-black/50"
        aria-hidden="true"
        onClick={onClose}
      />
      <div
        ref={modalRef}
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        className="relative z-10 mx-4 w-full max-w-lg rounded-lg bg-white p-6 shadow-xl dark:bg-gray-800"
      >
        <h2 id="modal-title" className="text-xl font-bold mb-4">
          {title}
        </h2>
        {children}
        <button
          onClick={onClose}
          aria-label="Close"
          className="absolute right-4 top-4 rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-700"
        >
          <svg aria-hidden="true" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
            <path d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" />
          </svg>
        </button>
      </div>
    </div>
  );
}
function DropdownMenu({ label, items }: { label: string; items: MenuItem[] }) {
  const [isOpen, setIsOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);
  const menuRef = useRef<HTMLUListElement>(null);

  const handleKeyDown = (e: React.KeyboardEvent) => {
    switch (e.key) {
      case 'ArrowDown':
        e.preventDefault();
        setActiveIndex(prev => Math.min(prev + 1, items.length - 1));
        break;
      case 'ArrowUp':
        e.preventDefault();
        setActiveIndex(prev => Math.max(prev - 1, 0));
        break;
      case 'Enter':
      case ' ':
        e.preventDefault();
        if (activeIndex >= 0) items[activeIndex].onClick();
        setIsOpen(false);
        break;
      case 'Escape':
        setIsOpen(false);
        break;
    }
  };

  return (
    <div className="relative" onKeyDown={handleKeyDown}>
      <button
        aria-haspopup="true"
        aria-expanded={isOpen}
        onClick={() => setIsOpen(!isOpen)}
      >
        {label}
      </button>
      {isOpen && (
        <ul ref={menuRef} role="menu" className="absolute mt-1 rounded-md border bg-white shadow-lg dark:bg-gray-800">
          {items.map((item, i) => (
            <li
              key={i}
              role="menuitem"
              tabIndex={-1}
              className={`cursor-pointer px-4 py-2 ${i === activeIndex ? 'bg-blue-100 dark:bg-blue-900' : ''}`}
              onClick={item.onClick}
            >
              {item.label}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

Accessibility Form

> Buat ulang form kontak agar accessible.
> Error validasi juga harus tersampaikan ke screen reader.
function ContactForm() {
  const [errors, setErrors] = useState<Record<string, string>>({});

  return (
    <form aria-label="Form kontak" noValidate>
      <div className="mb-4">
        <label htmlFor="name" className="block font-medium mb-1">
          Nama <span aria-label="required">*</span>
        </label>
        <input
          id="name"
          type="text"
          required
          aria-required="true"
          aria-invalid={!!errors.name}
          aria-describedby={errors.name ? 'name-error' : undefined}
          className="w-full rounded border p-2"
        />
        {errors.name && (
          <p id="name-error" role="alert" className="mt-1 text-sm text-red-600">
            {errors.name}
          </p>
        )}
      </div>

      <div className="mb-4">
        <label htmlFor="email" className="block font-medium mb-1">
          Email <span aria-label="required">*</span>
        </label>
        <input
          id="email"
          type="email"
          required
          aria-required="true"
          aria-invalid={!!errors.email}
          aria-describedby="email-hint email-error"
          className="w-full rounded border p-2"
        />
        <p id="email-hint" className="mt-1 text-xs text-gray-500">
          e.g., [email protected]
        </p>
        {errors.email && (
          <p id="email-error" role="alert" className="mt-1 text-sm text-red-600">
            {errors.email}
          </p>
        )}
      </div>

      <button type="submit" className="rounded bg-blue-600 px-4 py-2 text-white">
        Kirim
      </button>
    </form>
  );
}

Menerapkan Automated Testing

> Tambahkan accessibility test menggunakan jest-axe.
import { render } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

describe('Modal accessibility', () => {
  it('should have no accessibility violations', async () => {
    const { container } = render(
      <Modal isOpen={true} onClose={() => {}} title="Test Modal">
        <p>Content</p>
      </Modal>
    );
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });
});

Summary

Dengan Claude Code, kamu bisa secara efisien membuat component yang accessible sesuai standar WCAG hingga mengaudit kode yang sudah ada. Pengaturan untuk menjalankan accessibility test secara otomatis dengan fitur hook juga efektif. Untuk detail, lihat Panduan Fitur Hook. Tips untuk mengintegrasikan ke alur kerja pengembangan sehari-hari dibahas di Tips Melipatgandakan Produktivitas 3x.

Untuk detail Claude Code, lihat Dokumentasi Resmi Anthropic. Untuk detail panduan WCAG, lihat W3C WCAG 2.1.

#Claude Code #accessibility #WCAG #a11y #React