SiamCafe.net Blog
Cybersecurity

Radix UI Primitives Compliance Automation

radix ui primitives compliance automation
Radix UI Primitives Compliance Automation | SiamCafe Blog
2026-03-09· อ. บอม — SiamCafe.net· 11,762 คำ

Radix UI Primitives คืออะไร

Radix UI Primitives เป็น Low-level, Unstyled Component Library สำหรับ React ที่ให้ Behavior, Accessibility และ Interaction Patterns มาพร้อมใช้งาน โดยไม่บังคับ Style ทำให้สามารถ Customize UI ได้เต็มที่ด้วย CSS Framework ที่ต้องการ เช่น Tailwind CSS, CSS Modules หรือ Styled Components

จุดเด่นคือทุก Component ผ่านมาตรฐาน WAI-ARIA มีการจัดการ Keyboard Navigation, Focus Management, Screen Reader Support อย่างถูกต้อง เป็นพื้นฐานของ shadcn/ui ที่นิยมมากในปัจจุบัน การใช้ Radix UI ช่วยให้ผ่าน Accessibility Compliance ได้ง่ายขึ้นมาก

ติดตั้งและใช้งาน Radix UI

# ติดตั้ง Radix UI Primitives
npm install @radix-ui/react-dialog
npm install @radix-ui/react-dropdown-menu
npm install @radix-ui/react-tabs
npm install @radix-ui/react-tooltip
npm install @radix-ui/react-accordion
npm install @radix-ui/react-select
npm install @radix-ui/react-toast

# หรือติดตั้งทั้งหมดผ่าน shadcn/ui
npx shadcn-ui@latest init
npx shadcn-ui@latest add dialog dropdown-menu tabs tooltip

# --- ตัวอย่าง Accessible Dialog ---
// components/accessible-dialog.tsx
import * as Dialog from '@radix-ui/react-dialog';
import { X } from 'lucide-react';

export function AccessibleDialog({ trigger, title, description, children }) {
  return (
    <Dialog.Root>
      <Dialog.Trigger asChild>
        {trigger}
      </Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Overlay className="fixed inset-0 bg-black/50
          data-[state=open]:animate-fadeIn" />
        <Dialog.Content
          className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
            bg-white dark:bg-zinc-900 rounded-lg p-6 w-full max-w-md
            shadow-xl focus:outline-none
            data-[state=open]:animate-slideIn"
          aria-describedby={description ? 'dialog-desc' : undefined}
        >
          <Dialog.Title className="text-lg font-semibold">
            {title}
          </Dialog.Title>
          {description && (
            <Dialog.Description id="dialog-desc" className="text-sm
              text-zinc-500 mt-2">
              {description}
            </Dialog.Description>
          )}
          <div className="mt-4">{children}</div>
          <Dialog.Close asChild>
            <button
              className="absolute top-3 right-3 p-1 rounded-full
                hover:bg-zinc-100 dark:hover:bg-zinc-800"
              aria-label="Close dialog"
            >
              <X size={16} />
            </button>
          </Dialog.Close>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

// Radix UI จัดการให้อัตโนมัติ:
// - Focus Trap (Tab ไม่หลุดออกจาก Dialog)
// - ESC ปิด Dialog
// - Click Outside ปิด Dialog
// - aria-modal="true"
// - role="dialog"
// - Focus กลับไปที่ Trigger เมื่อปิด
// - Screen Reader ประกาศ Title และ Description

# --- ตัวอย่าง Accessible Dropdown Menu ---
// components/accessible-dropdown.tsx
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';

export function UserMenu({ user }) {
  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger asChild>
        <button className="flex items-center gap-2 px-3 py-2 rounded-lg
          hover:bg-zinc-100 dark:hover:bg-zinc-800"
          aria-label={`User menu for `}>
          <img src={user.avatar} alt="" className="w-8 h-8 rounded-full" />
          <span>{user.name}</span>
        </button>
      </DropdownMenu.Trigger>

      <DropdownMenu.Portal>
        <DropdownMenu.Content className="bg-white dark:bg-zinc-900
          rounded-lg shadow-lg p-1 min-w-[200px]
          animate-in fade-in slide-in-from-top-2"
          sideOffset={5}>

          <DropdownMenu.Label className="px-3 py-1.5 text-xs
            text-zinc-500">
            Account
          </DropdownMenu.Label>

          <DropdownMenu.Item className="px-3 py-2 rounded cursor-pointer
            hover:bg-zinc-100 dark:hover:bg-zinc-800 outline-none
            data-[highlighted]:bg-zinc-100">
            Profile Settings
          </DropdownMenu.Item>

          <DropdownMenu.Item className="px-3 py-2 rounded cursor-pointer
            hover:bg-zinc-100 outline-none
            data-[highlighted]:bg-zinc-100">
            Billing
          </DropdownMenu.Item>

          <DropdownMenu.Separator className="h-px bg-zinc-200
            dark:bg-zinc-700 my-1" />

          <DropdownMenu.Item className="px-3 py-2 rounded cursor-pointer
            text-red-500 outline-none
            data-[highlighted]:bg-red-50">
            Sign Out
          </DropdownMenu.Item>

          <DropdownMenu.Arrow className="fill-white dark:fill-zinc-900" />
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  );
}

// Radix UI จัดการ Keyboard Navigation:
// - Arrow Up/Down เลื่อนระหว่าง Items
// - Enter/Space เลือก Item
// - ESC ปิด Menu
// - Type-ahead search

Compliance Automation Pipeline

# .github/workflows/accessibility-compliance.yml
# GitHub Actions — Accessibility Compliance Check

name: Accessibility Compliance
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  axe-scan:
    name: Axe Accessibility Scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install Dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Start Server
        run: npm run preview &
        env:
          PORT: 3000

      - name: Wait for Server
        run: npx wait-on http://localhost:3000 --timeout 30000

      - name: Run Axe Accessibility Tests
        run: |
          npx @axe-core/cli http://localhost:3000 \
            --tags wcag2a, wcag2aa, wcag21aa \
            --exit \
            --save results/axe-report.json

      - name: Upload Report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: accessibility-report
          path: results/

  playwright-a11y:
    name: Playwright Accessibility Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install Dependencies
        run: |
          npm ci
          npx playwright install --with-deps chromium

      - name: Run Accessibility Tests
        run: npx playwright test tests/accessibility/

      - name: Upload Report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-a11y-report
          path: playwright-report/

  lighthouse:
    name: Lighthouse Accessibility Audit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Build
        run: npm ci && npm run build

      - name: Lighthouse CI
        uses: treosh/lighthouse-ci-action@v11
        with:
          configPath: .lighthouserc.json
          uploadArtifacts: true

---
# .lighthouserc.json
{
  "ci": {
    "assert": {
      "assertions": {
        "categories:accessibility": ["error", {"minScore": 0.95}],
        "categories:performance": ["warn", {"minScore": 0.90}],
        "categories:best-practices": ["warn", {"minScore": 0.90}]
      }
    },
    "collect": {
      "startServerCommand": "npm run preview",
      "url": ["http://localhost:3000", "http://localhost:3000/about"]
    }
  }
}

Playwright Accessibility Tests

// tests/accessibility/dialog.spec.ts
// Playwright Tests สำหรับ Accessible Dialog
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('Dialog Accessibility', () => {

  test('should pass axe accessibility scan', async ({ page }) => {
    await page.goto('/');
    await page.click('[data-testid="open-dialog"]');
    await page.waitForSelector('[role="dialog"]');

    const results = await new AxeBuilder({ page })
      .withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
      .analyze();

    expect(results.violations).toEqual([]);
  });

  test('should trap focus inside dialog', async ({ page }) => {
    await page.goto('/');
    await page.click('[data-testid="open-dialog"]');

    // Tab through all focusable elements
    const dialog = page.locator('[role="dialog"]');
    await expect(dialog).toBeFocused();

    // Tab forward
    await page.keyboard.press('Tab');
    const firstFocusable = page.locator('[role="dialog"] :focus');
    await expect(firstFocusable).toBeVisible();

    // Tab should cycle within dialog
    for (let i = 0; i < 20; i++) {
      await page.keyboard.press('Tab');
      const focused = await page.evaluate(
        () => document.activeElement?.closest('[role="dialog"]') !== null
      );
      expect(focused).toBe(true);
    }
  });

  test('should close on ESC', async ({ page }) => {
    await page.goto('/');
    await page.click('[data-testid="open-dialog"]');
    await expect(page.locator('[role="dialog"]')).toBeVisible();

    await page.keyboard.press('Escape');
    await expect(page.locator('[role="dialog"]')).not.toBeVisible();
  });

  test('should return focus to trigger on close', async ({ page }) => {
    await page.goto('/');
    const trigger = page.locator('[data-testid="open-dialog"]');
    await trigger.click();
    await page.keyboard.press('Escape');
    await expect(trigger).toBeFocused();
  });

  test('should have correct ARIA attributes', async ({ page }) => {
    await page.goto('/');
    await page.click('[data-testid="open-dialog"]');

    const dialog = page.locator('[role="dialog"]');
    await expect(dialog).toHaveAttribute('aria-modal', 'true');
    await expect(dialog).toHaveAttribute('aria-labelledby', /.+/);
  });

  test('should announce to screen readers', async ({ page }) => {
    await page.goto('/');
    await page.click('[data-testid="open-dialog"]');

    // Check aria-labelledby points to title
    const labelledBy = await page.locator('[role="dialog"]')
      .getAttribute('aria-labelledby');
    const title = page.locator(`#`);
    await expect(title).toHaveText(/.+/);
  });
});

test.describe('Color Contrast', () => {
  test('all pages pass contrast requirements', async ({ page }) => {
    const pages = ['/', '/about', '/contact', '/dashboard'];

    for (const url of pages) {
      await page.goto(url);
      const results = await new AxeBuilder({ page })
        .withTags(['wcag2aa'])
        .withRules(['color-contrast'])
        .analyze();

      expect(results.violations, `Contrast issues on `).toEqual([]);
    }
  });
});

WCAG 2.1 Checklist

Radix UI Primitives คืออะไร

Radix UI เป็น Unstyled Accessible Component Library สำหรับ React ให้ Behavior และ Accessibility มาพร้อม เช่น Keyboard Navigation, Focus Management, ARIA Attributes Customize Style ได้เต็มที่ เป็นพื้นฐานของ shadcn/ui

Compliance Automation คืออะไร

การใช้เครื่องมืออัตโนมัติตรวจสอบว่าซอฟต์แวร์เป็นไปตามมาตรฐาน เช่น WCAG 2.1 สำหรับ Accessibility รวมเข้า CI/CD Pipeline ตรวจทุก Commit ใช้ Axe, Playwright, Lighthouse ตรวจสอบอัตโนมัติ Fail Build เมื่อไม่ผ่าน

WCAG 2.1 คืออะไร

มาตรฐานสากลสำหรับ Web Accessibility 4 หลักคือ Perceivable, Operable, Understandable, Robust 3 ระดับ A, AA, AAA ส่วนใหญ่ตั้งเป้า AA ครอบคลุม Contrast Ratio, Keyboard Navigation, Screen Reader Support หลายประเทศบังคับใช้ตามกฎหมาย

ทำไมต้องใช้ Radix UI แทนการเขียนเอง

Accessibility ซับซ้อนมาก Dialog, Dropdown, Tooltip ต้องจัดการ Focus Trap, Keyboard Events, Screen Reader Radix UI จัดการทั้งหมดให้ถูกต้องตาม WAI-ARIA ลด Bug ผ่าน Compliance ง่ายขึ้น ประหยัดเวลาพัฒนาหลายสัปดาห์

สรุป

Radix UI Primitives ช่วยให้สร้าง Accessible UI ได้ง่ายและถูกต้องตาม WCAG 2.1 เมื่อรวมกับ Compliance Automation ใน CI/CD Pipeline ใช้ Axe, Playwright และ Lighthouse ตรวจสอบทุก Commit อัตโนมัติ ทำให้มั่นใจว่า UI ผ่านมาตรฐาน Accessibility ตลอดเวลา ไม่ต้องตรวจสอบ Manual ทุกครั้ง

📖 บทความที่เกี่ยวข้อง

Radix UI Primitives Blue Green Canary Deployอ่านบทความ → Radix UI Primitives Serverless Architectureอ่านบทความ → Qwik Resumability Compliance Automationอ่านบทความ → Radix UI Primitives Capacity Planningอ่านบทความ →

📚 ดูบทความทั้งหมด →