Radix UI Primitives กับ Progressive Delivery —
Radix UI Primitives

Radix UI Primitives เป็น UI Component Library สำหรับ React ที่ออกแบบมาให้เป็น Unstyled Components ให้เฉพาะ Behavior และ Accessibility (WAI-ARIA) ที่ถูกต้อง Developer สามารถ Style เองได้อิสระ 100% ใช้ร่วมกับ Tailwind CSS, CSS Modules หรือ Styled Components
เนื้อหาเกี่ยวข้อง — บทความที่เกี่ยวข้อง: Strength แปลว่าอะไร
เมื่อรวมกับ Progressive Delivery Strategy ใช้ Feature Flags ควบคุมการปล่อย Components ใหม่ให้ผู้ใช้ทีละกลุ่ม Canary Deployment ทดสอบกับผู้ใช้ส่วันนี้อยก่อน A/B Testing เปรียบเทียบ Design ต่างๆ
เนื้อหาเกี่ยวข้อง — แนะนำให้อ่าน แท็ก html คืออะไรใช้งานอย่างไร
Radix UI Components พร้อม Tailwind CSS
// === Radix UI + Tailwind CSS Components ===
// npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu
// npm install @radix-ui/react-tabs @radix-ui/react-tooltip
// npm install @radix-ui/react-switch @radix-ui/react-select
// npm install tailwindcss class-variance-authority clsx
// components/Dialog.tsx — Accessible Modal Dialog
import * as Dialog from "@radix-ui/react-dialog";
import { X } from "lucide-react";
interface ModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
description?: string;
children: React.ReactNode;
}
export function Modal({ open, onOpenChange, title, description, children }: ModalProps) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm
data-[state=open]:animate-in data-[state=closed]:animate-out
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
<Dialog.Content className="fixed left-1/2 top-1/2 -translate-x-1/2
-translate-y-1/2 bg-white rounded-xl shadow-xl p-6 w-full max-w-md
data-[state=open]:animate-in data-[state=closed]:animate-out
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95">
<Dialog.Title className="text-lg font-semibold text-gray-900">
{title}
</Dialog.Title>
{description && (
<Dialog.Description className="mt-2 text-sm text-gray-500">
{description}
</Dialog.Description>
)}
<div className="mt-4">{children}</div>
<Dialog.Close className="absolute right-4 top-4 rounded-sm
opacity-70 hover:opacity-100 transition-opacity">
<X className="h-4 w-4" />
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
// components/Tabs.tsx — Accessible Tabs
import * as Tabs from "@radix-ui/react-tabs";
interface TabItem {
value: string;
label: string;
content: React.ReactNode;
}
export function TabGroup({ tabs, defaultValue }: { tabs: TabItem[]; defaultValue?: string }) {
return (
<Tabs.Root defaultValue={defaultValue || tabs[0]?.value}>
<Tabs.List className="flex border-b border-gray-200">
{tabs.map((tab) => (
<Tabs.Trigger key={tab.value} value={tab.value}
className="px-4 py-2 text-sm font-medium text-gray-500
hover:text-gray-700 border-b-2 border-transparent
data-[state=active]:border-blue-500
data-[state=active]:text-blue-600 transition-colors">
{tab.label}
</Tabs.Trigger>
))}
</Tabs.List>
{tabs.map((tab) => (
<Tabs.Content key={tab.value} value={tab.value} className="py-4">
{tab.content}
</Tabs.Content>
))}
</Tabs.Root>
);
}
// components/DropdownMenu.tsx — Accessible Dropdown
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
interface MenuItem {
label: string;
onClick: () => void;
icon?: React.ReactNode;
destructive?: boolean;
}
export function Dropdown({ trigger, items }: { trigger: React.ReactNode; items: MenuItem[] }) {
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>{trigger}</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content className="min-w-[180px] bg-white rounded-lg
shadow-lg border border-gray-200 p-1 animate-in fade-in-0 zoom-in-95">
{items.map((item, i) => (
<DropdownMenu.Item key={i} onClick={item.onClick}
className={`flex items-center gap-2 px-3 py-2 text-sm rounded-md
cursor-pointer outline-none
`}>
{item.icon} {item.label}
</DropdownMenu.Item>
))}
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
);
}
Feature Flags สำหรับ Progressive Delivery

# feature_flags.py — Feature Flag System สำหรับ Progressive Delivery
import hashlib
import json
import time
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Any
from enum import Enum
class RolloutStrategy(Enum):
ALL = "all"
PERCENTAGE = "percentage"
USER_LIST = "user_list"
GRADUAL = "gradual"
@dataclass
class FeatureFlag:
name: str
enabled: bool
strategy: RolloutStrategy
percentage: float = 0
user_list: List[str] = field(default_factory=list)
gradual_schedule: Dict[str, float] = field(default_factory=dict)
description: str = ""
created_at: float = field(default_factory=time.time)
class FeatureFlagService:
"""Feature Flag Service สำหรับ Progressive Delivery"""
def __init__(self):
self.flags: Dict[str, FeatureFlag] = {}
self.evaluations: Dict[str, int] = {}
def create_flag(self, name, strategy=RolloutStrategy.ALL,
percentage=0, user_list=None, description=""):
flag = FeatureFlag(
name=name, enabled=True, strategy=strategy,
percentage=percentage,
user_list=user_list or [],
description=description,
)
self.flags[name] = flag
return flag
def is_enabled(self, flag_name, user_id=None, attributes=None):
"""ตรวจสอบว่า Feature เปิดสำหรับ User นี้หรือไม่"""
flag = self.flags.get(flag_name)
if not flag or not flag.enabled:
return False
# Track evaluations
self.evaluations[flag_name] = self.evaluations.get(flag_name, 0) + 1
if flag.strategy == RolloutStrategy.ALL:
return True
elif flag.strategy == RolloutStrategy.PERCENTAGE:
if not user_id:
return False
hash_val = int(hashlib.md5(
f"{flag_name}:{user_id}".encode()
).hexdigest(), 16)
return (hash_val % 100) < flag.percentage
elif flag.strategy == RolloutStrategy.USER_LIST:
return user_id in flag.user_list
elif flag.strategy == RolloutStrategy.GRADUAL:
current_pct = self._get_current_percentage(flag)
if not user_id:
return False
hash_val = int(hashlib.md5(
f"{flag_name}:{user_id}".encode()
).hexdigest(), 16)
return (hash_val % 100) < current_pct
return False
def _get_current_percentage(self, flag):
"""คำนวณ Percentage ปัจจุบันตาม Schedule"""
now = time.time()
current_pct = 0
for ts_str, pct in sorted(flag.gradual_schedule.items()):
if float(ts_str) <= now:
current_pct = pct
return current_pct
def set_percentage(self, flag_name, percentage):
"""ปรับ Percentage"""
if flag_name in self.flags:
self.flags[flag_name].percentage = percentage
def kill_switch(self, flag_name):
"""ปิด Feature ทันที"""
if flag_name in self.flags:
self.flags[flag_name].enabled = False
def dashboard(self):
"""แสดง Feature Flags Dashboard"""
print(f"\n{'='*60}")
print(f"Feature Flags Dashboard")
print(f"{'='*60}")
for name, flag in self.flags.items():
status = "ON" if flag.enabled else "OFF"
evals = self.evaluations.get(name, 0)
print(f"\n [{status:>3}] {name}")
print(f" Strategy: {flag.strategy.value}")
if flag.strategy == RolloutStrategy.PERCENTAGE:
print(f" Percentage: {flag.percentage}%")
elif flag.strategy == RolloutStrategy.USER_LIST:
print(f" Users: {len(flag.user_list)}")
print(f" Evaluations: {evals}")
if flag.description:
print(f" Description: {flag.description}")
# === ตัวอย่าง ===
ff = FeatureFlagService()
# Canary: เปิดให้ 5% ก่อน
ff.create_flag("new_search_ui", RolloutStrategy.PERCENTAGE,
percentage=5, description="New Search UI with Radix Components")
# Beta Users
ff.create_flag("dark_mode", RolloutStrategy.USER_LIST,
user_list=["user_001", "user_002", "user_003"],
description="Dark Mode Feature")
# ทดสอบ
for i in range(100):
user = f"user_{i:03d}"
ff.is_enabled("new_search_ui", user)
ff.is_enabled("dark_mode", user)
ff.dashboard()
# ค่อยๆเพิ่ม: 5% -> 25% -> 50% -> 100%
ff.set_percentage("new_search_ui", 25)
print("\nIncreased new_search_ui to 25%")
CI/CD Pipeline สำหรับ Progressive Delivery
# === GitHub Actions — Progressive Delivery Pipeline ===
# .github/workflows/progressive-delivery.yml
name: Progressive Delivery
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: myapp-frontend
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run type-check
- run: npm run test -- --coverage
# Accessibility Tests
- run: npm run test:a11y
- run: npx playwright test
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t $REGISTRY/$IMAGE_NAME:} .
- run: docker push $REGISTRY/$IMAGE_NAME:}
canary:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy Canary (5%)
run: |
kubectl set image deployment/frontend-canary \
frontend=$REGISTRY/$IMAGE_NAME:} \
-n production
# Istio Traffic Split: 5% canary
kubectl apply -f - << 'EOF'
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: frontend
namespace: production
spec:
hosts: ["www.example.com"]
http:
- route:
- destination:
host: frontend-stable
weight: 95
- destination:
host: frontend-canary
weight: 5
EOF
- name: Monitor Canary (5 min)
run: |
sleep 300
ERROR_RATE=$(curl -s "http://prometheus:9090/api/v1/query" \
--data-urlencode 'query=rate(http_requests_total{status=~"5.."}[5m])' \
| jq '.data.result[0].value[1]')
if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
echo "Canary failed! Error rate: $ERROR_RATE"
exit 1
fi
- name: Promote to 50%
run: |
# Update traffic split to 50/50
echo "Promoting canary to 50%"
- name: Promote to 100%
run: |
kubectl set image deployment/frontend-stable \
frontend=$REGISTRY/$IMAGE_NAME:} \
-n production
echo "Full rollout complete"
Best Practices
- Unstyled Components: ใช้ Radix UI Primitives เป็นฐาน Style เองด้วย Tailwind CSS
- Accessibility First: Radix UI มี WAI-ARIA ในตัว ทดสอบด้วย axe-core
- Feature Flags: ใช้ Feature Flags ควบคุม Component ใหม่ เปิดปิดโดยไม่ต้อง Deploy
- Canary Release: ปล่อย Feature ให้ผู้ใช้ 5% ก่อน Monitor Error Rate แล้วค่อยขยาย
- Kill Switch: ทุก Feature ต้องมี Kill Switch ปิดได้ทันทีถ้ามีปัญหา
- A/B Testing: ใช้ Feature Flags ทำ A/B Test เปรียบเทียบ UI ต่างๆ
Radix UI Primitives คืออะไร
Open-source UI Component Library สำหรับ React เน้น Accessibility Unstyled Components Composability ให้ Behavior WAI-ARIA ที่ถูกต้อง Developer Style เองได้อิสระ ใช้กับ Tailwind CSS ได้
แนะนำเพิ่มเติม — XM Signal
เนื้อหาเกี่ยวข้อง — LlamaIndex RAG Low Code No Code





