关于 ui-builder
ui-builder 是使用 React、Tailwind CSS 和 Shadcn/ui 等组件库构建响应式、易访问界面的技能,提供布局、组件、动画、性能和暗色模式的模式与检查清单。
主要功能
- 01构建移动优先的响应式布局
- 02应用语义化与易访问界面模式
- 03创建常见界面组件
- 04添加过渡与动画
- 05实现加载状态和暗色模式
- 06检查性能与易访问性
适用场景
- 01构建页面和界面组件
- 02让表单与导航适配不同屏幕
- 03添加易访问交互与键盘支持
- 04优化加载状态和界面性能
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ jmsktm/ claude-settings --skill ui-builder安装后在 Claude Code 中输入/ui-builder确认已加载。
详细介绍
UI/UX 构建技能
概述
此技能可帮助你使用现代 React 模式、Tailwind CSS 和 Shadcn/ui 等组件库,构建美观、易于访问且响应迅速的用户界面。
核心原则
1. 移动端优先设计
- 从移动端布局开始
- 使用响应式断点:sm(640px)、md(768px)、lg(1024px)、xl(1280px)
- 在多种屏幕尺寸下进行测试
2. 无障碍访问
- 使用语义化 HTML
- 在需要时添加 ARIA 标签
- 确保键盘导航正常工作
- 保持颜色对比度
- 支持屏幕阅读器
3. 性能
- 延迟加载大型组件
- 优化图片
- 尽可能使用 CSS 动画代替 JS
- 尽量减少重新渲染
4. 一致性
- 使用设计令牌(颜色、间距、排版)
- 遵循既定模式
- 保持视觉层级
Tailwind CSS 模式
布局
// Flex layouts
<div className="flex items-center justify-between gap-4">
<div>Left</div>
<div>Right</div>
</div>
// Grid layouts
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div>Card 1</div>
<div>Card 2</div>
<div>Card 3</div>
</div>
// Container
<div className="container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl">
Content
</div>
// Centering
<div className="flex items-center justify-center min-h-screen">
<div>Centered</div>
</div>
响应式设计
// Mobile first
<div className="text-sm md:text-base lg:text-lg">
Responsive text
</div>
// Hide/show by breakpoint
<div className="hidden md:block">
Desktop only
</div>
<div className="block md:hidden">
Mobile only
</div>
// Responsive padding/margin
<div className="p-4 md:p-6 lg:p-8">
Responsive padding
</div>
颜色和主题
// Background colors
<div className="bg-white dark:bg-gray-900">
Content
</div>
// Text colors
<p className="text-gray-900 dark:text-white">
Text
</p>
// Hover states
<button className="bg-blue-500 hover:bg-blue-600 transition-colors">
Hover me
</button>
// Focus states
<input className="border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200" />
间距
// Margin/Padding scale: 0, 1(4px), 2(8px), 4(16px), 6(24px), 8(32px), 12(48px), 16(64px)
<div className="p-4">Padding 16px</div>
<div className="mt-8 mb-4">Margin top 32px, bottom 16px</div>
<div className="space-y-4">Vertical spacing between children</div>
<div className="gap-6">Gap between flex/grid items</div>
常见组件模式
按钮
// components/ui/button.tsx
import { cn } from '@/lib/utils'
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'outline' | 'ghost' | 'destructive'
size?: 'sm' | 'md' | 'lg'
}
export function Button({
className,
variant = 'default',
size = 'md',
...props
}: ButtonProps) {
return (
<button
className={cn(
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
'disabled:opacity-50 disabled:pointer-events-none',
{
'bg-blue-600 text-white hover:bg-blue-700': variant === 'default',
'border border-gray-300 bg-white hover:bg-gray-50': variant === 'outline',
'hover:bg-gray-100': variant === 'ghost',
'bg-red-600 text-white hover:bg-red-700': variant === 'destructive',
},
{
'h-8 px-3 text-sm': size === 'sm',
'h-10 px-4': size === 'md',
'h-12 px-6 text-lg': size === 'lg',
},
className
)}
{...props}
/>
)
}
卡片
// components/ui/card.tsx
export function Card({ children, className }: { children: React.ReactNode; className?: string }) {
return (
<div className={cn('rounded-lg border bg-white p-6 shadow-sm', className)}>
{children}
</div>
)
}
export function CardHeader({ children }: { children: React.ReactNode }) {
return <div className="mb-4">{children}</div>
}
export function CardTitle({ children }: { children: React.ReactNode }) {
return <h3 className="text-lg font-semibold">{children}</h3>
}
export function CardContent({ children }: { children: React.ReactNode }) {
return <div>{children}</div>
}
输入框
// components/ui/input.tsx
export function Input({ className, ...props }: React.InputHTMLAttributes<HTMLInputElement>) {
return (
<input
className={cn(
'flex h-10 w-full rounded-md border border-gray-300 bg-white px-3 py-2',
'text-sm placeholder:text-gray-400',
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent',
'disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
/>
)
}
模态框/对话框
'use client'
import { useEffect } from 'react'
import { X } from 'lucide-react'
interface DialogProps {
open: boolean
onClose: () => void
title?: string
children: React.ReactNode
}
export function Dialog({ open, onClose, title, children }: DialogProps) {
useEffect(() => {
if (open) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = 'unset'
}
return () => {
document.body.style.overflow = 'unset'
}
}, [open])
if (!open) return null
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={onClose}
/>
{/* Dialog */}
<div className="relative z-50 w-full max-w-lg rounded-lg bg-white p-6 shadow-xl">
{title && (
<div className="mb-4 flex items-center justify-between">
<h2 className="text-xl font-semibold">{title}</h2>
<button
onClick={onClose}
className="rounded-md p-1 hover:bg-gray-100"
>
<X className="h-5 w-5" />
</button>
</div>
)}
{children}
</div>
</div>
)
}
下拉菜单
'use client'
import { useState, useRef, useEffect } from 'react'
export function DropdownMenu({ trigger, children }: {
trigger: React.ReactNode
children: React.ReactNode
}) {
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (ref.current && !ref.current.contains(event.target as Node)) {
setOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
return (
<div className="relative" ref={ref}>
<div onClick={() => setOpen(!open)}>{trigger}</div>
{open && (
<div className="absolute right-0 mt-2 w-56 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5">
<div className="py-1">{children}</div>
</div>
)}
</div>
)
}
export function DropdownMenuItem({ children, onClick }: {
children: React.ReactNode
onClick?: () => void
}) {
return (
<button
className="block w-full px-4 py-2 text-left text-sm hover:bg-gray-100"
onClick={onClick}
>
{children}
</button>
)
}
表单
// components/forms/contact-form.tsx
'use client'
import { useState } from 'react'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
export function ContactForm() {
const [loading, setLoading] = useState(false)
const [errors, setErrors] = useState<Record<string, string>>({})
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
setLoading(true)
setErrors({})
const formData = new FormData(e.currentTarget)
const email = formData.get('email') as string
const message = formData.get('message') as string
// Validation
const newErrors: Record<string, string> = {}
if (!email) newErrors.email = 'Email is required'
if (!message) newErrors.message = 'Message is required'
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors)
setLoading(false)
return
}
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, message }),
})
if (!response.ok) throw new Error('Failed to send')
// Success
e.currentTarget.reset()
alert('Message sent!')
} catch (error) {
setErrors({ submit: 'Failed to send message' })
} finally {
setLoading(false)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium mb-1">
Email
</label>
<Input
id="email"
name="email"
type="email"
placeholder="[email protected]"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
)}
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium mb-1">
Message
</label>
<textarea
id="message"
name="message"
rows={4}
className="w-full rounded-md border border-gray-300 px-3 py-2"
placeholder="Your message..."
/>
{errors.message && (
<p className="mt-1 text-sm text-red-600">{errors.message}</p>
)}
</div>
{errors.submit && (
<p className="text-sm text-red-600">{errors.submit}</p>
)}
<Button type="submit" disabled={loading}>
{loading ? 'Sending...' : 'Send Message'}
</Button>
</form>
)
}
动画
Tailwind 过渡效果
// Hover effects
<div className="transition-all duration-200 hover:scale-105">
Hover to scale
</div>
// Opacity fade
<div className="transition-opacity duration-300 opacity-0 hover:opacity-100">
Fade in
</div>
// Transform
<div className="transition-transform duration-200 hover:translate-x-2">
Slide on hover
</div>
Framer Motion
'use client'
import { motion } from 'framer-motion'
// Fade in
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
Fades in
</motion.div>
// Slide in
<motion.div
initial={{ x: -100, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ duration: 0.5 }}
>
Slides in
</motion.div>
// Stagger children
<motion.div
initial="hidden"
animate="visible"
variants={{
visible: {
transition: {
staggerChildren: 0.1
}
}
}}
>
{items.map(item => (
<motion.div
key={item.id}
variants={{
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
}}
>
{item.title}
</motion.div>
))}
</motion.div>
图标
Lucide React
import { Search, User, Settings, ChevronRight } from 'lucide-react'
// Basic usage
<Search className="h-5 w-5" />
// With color
<User className="h-5 w-5 text-blue-600" />
// In button
<button className="flex items-center gap-2">
<Settings className="h-4 w-4" />
Settings
</button>
加载状态
骨架屏
export function Skeleton({ className }: { className?: string }) {
return (
<div className={cn('animate-pulse rounded-md bg-gray-200', className)} />
)
}
// Usage
<div className="space-y-4">
<Skeleton className="h-12 w-full" />
<Skeleton className="h-12 w-full" />
<Skeleton className="h-12 w-3/4" />
</div>
加载指示器
export function Spinner({ className }: { className?: string }) {
return (
<div
className={cn(
'animate-spin rounded-full border-2 border-gray-300 border-t-blue-600',
'h-8 w-8',
className
)}
/>
)
}
深色模式
设置
// app/providers.tsx
'use client'
import { ThemeProvider } from 'next-themes'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
)
}
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
主题切换
'use client'
import { useTheme } from 'next-themes'
import { Moon, Sun } from 'lucide-react'
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className="rounded-md p-2 hover:bg-gray-100 dark:hover:bg-gray-800"
>
<Sun className="h-5 w-5 dark:hidden" />
<Moon className="hidden h-5 w-5 dark:block" />
</button>
)
}
深色模式样式
// Tailwind dark mode
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
Dark mode aware
</div>
// tailwind.config.js
module.exports = {
darkMode: 'class',
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
}
}
}
}
最佳实践
无障碍访问检查清单
- 使用语义化 HTML(header、nav、main、footer、article、section)
- 为图片添加替代文本
- 确保颜色对比度充足(普通文本为 4.5:1)
- 支持键盘导航(Tab、Enter、Escape)
- 在需要时添加 ARIA 标签
- 使用 focus-visible 设置键盘焦点样式
- 使用屏幕阅读器进行测试
性能检查清单
- 使用 next/image 处理图片
- 延迟加载视口中不可见的组件
- 尽量减少客户端 JavaScript
- 使用 CSS 动画代替 JS
- 实现加载骨架屏
- 优化字体加载
- 使用适当的图片格式(WebP、AVIF)
响应式设计检查清单
- 采用移动端优先的设计
- 在多种屏幕尺寸下进行测试
- 使用响应式图片
- 实现便于触控的点击目标(最小为 44x44px)
- 测试横屏和竖屏
- 处理移动设备上的安全区域
- 考虑折叠屏设备
实用函数
// lib/utils.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
常见布局
仪表盘布局
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex h-screen">
{/* Sidebar */}
<aside className="w-64 border-r bg-gray-50">
<nav className="p-4">Sidebar</nav>
</aside>
{/* Main content */}
<div className="flex-1 overflow-auto">
<header className="border-b bg-white px-6 py-4">
Header
</header>
<main className="p-6">{children}</main>
</div>
</div>
)
}
落地页主视觉区
export function Hero() {
return (
<section className="flex min-h-screen items-center justify-center bg-gradient-to-b from-blue-50 to-white">
<div className="container px-4 text-center">
<h1 className="mb-6 text-5xl font-bold tracking-tight md:text-6xl lg:text-7xl">
Your Amazing Product
</h1>
<p className="mx-auto mb-8 max-w-2xl text-lg text-gray-600 md:text-xl">
A compelling description that explains what your product does
</p>
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
<Button size="lg">Get Started</Button>
<Button size="lg" variant="outline">Learn More</Button>
</div>
</div>
</section>
)
}
何时使用此技能
在以下情况下调用此技能:
- 构建新的 UI 组件
- 创建布局和页面结构
- 实现响应式设计
- 添加动画和过渡效果
- 处理表单和输入框
- 实现深色模式
- 确保无障碍访问
- 优化 UI 性能
- 创建加载状态
- 构建导航组件
评论
支持 Markdown