| name | style-ui |
| description | Style and UI patterns for DaisyUI Emerald theme. Use when styling forms, cards, buttons, tables, and other UI components. Includes fancy card designs, color schemes, and visual effects. |
Style and UI Patterns
Overview
This skill documents the UI styling patterns used in this project, focusing on DaisyUI with the Emerald theme. It covers fancy form cards, color schemes, visual effects, and consistent styling conventions.
Theme Configuration
Emerald Theme Setup
The project uses DaisyUI's Emerald theme as the default:
/* In App.css */
@plugin 'daisyui' {
themes: emerald --default, dark;
}
/* In rsbuild.config.ts */
html: {
htmlAttrs: {
'data-theme': 'emerald',
},
},
Emerald Theme Colors
| Color | Usage | Tailwind Class |
|---|---|---|
| Primary | Main actions, links | text-primary, bg-primary, border-primary |
| Secondary | Alternative actions | text-secondary, bg-secondary |
| Accent | Highlights, tags | text-accent, bg-accent |
| Info | Information, previews | text-info, bg-info |
| Success | Published, confirmed | text-success, bg-success |
| Warning | Warnings, content | text-warning, bg-warning |
| Error | Errors, delete | text-error, bg-error |
Fancy Card Pattern
Card Structure
<div className="card bg-base-100 shadow-lg border-t-4 border-t-primary hover:shadow-xl transition-shadow duration-300">
<div className="card-body">
{/* Card Header */}
<div className="flex items-start gap-4">
<div className="bg-primary/10 p-3 rounded-xl">
<IconComponent className="w-6 h-6 text-primary" />
</div>
<div className="flex-1">
<h2 className="card-title text-lg">Card Title</h2>
<p className="text-sm text-base-content/60">
Description text here
</p>
</div>
</div>
<div className="divider my-2"></div>
{/* Card Content */}
<div className="space-y-4">
{/* Content goes here */}
</div>
</div>
</div>
Card Color Variations
Use different border colors to distinguish sections:
{/* Primary - Basic Information */}
<div className="card bg-base-100 shadow-lg border-t-4 border-t-primary hover:shadow-xl transition-shadow duration-300">
<div className="bg-primary/10 p-3 rounded-xl">
<FolderOpen className="w-6 h-6 text-primary" />
</div>
</div>
{/* Secondary - Thumbnails/Media */}
<div className="card bg-base-100 shadow-lg border-t-4 border-t-secondary hover:shadow-xl transition-shadow duration-300">
<div className="bg-secondary/10 p-3 rounded-xl">
<ImagePlus className="w-6 h-6 text-secondary" />
</div>
</div>
{/* Accent - Tags */}
<div className="card bg-base-100 shadow-lg border-t-4 border-t-accent hover:shadow-xl transition-shadow duration-300">
<div className="bg-accent/10 p-3 rounded-xl">
<Tag className="w-6 h-6 text-accent" />
</div>
</div>
{/* Info - Preview Content */}
<div className="card bg-base-100 shadow-lg border-t-4 border-t-info hover:shadow-xl transition-shadow duration-300">
<div className="bg-info/10 p-3 rounded-xl">
<BookOpen className="w-6 h-6 text-info" />
</div>
</div>
{/* Warning - Full Content */}
<div className="card bg-base-100 shadow-lg border-t-4 border-t-warning hover:shadow-xl transition-shadow duration-300">
<div className="bg-warning/10 p-3 rounded-xl">
<FileText className="w-6 h-6 text-warning" />
</div>
</div>
Icon Badge Pattern
<div className="bg-{color}/10 p-3 rounded-xl">
<IconComponent className="w-6 h-6 text-{color}" />
</div>
Replace {color} with: primary, secondary, accent, info, warning, success, error
Form Input Styling
Basic Input with Focus State
<label className="form-control w-full">
<div className="label">
<span className="label-text font-medium">Label Text</span>
</div>
<input
type="text"
className={`input input-bordered w-full focus:input-primary ${errors.field ? 'input-error' : ''}`}
placeholder="Placeholder text"
disabled={isLoading}
/>
{errors.field && (
<div className="label">
<span className="label-text-alt text-error">{errors.field.message}</span>
</div>
)}
</label>
Select with Focus State
<select
className={`select select-bordered w-full focus:select-primary ${errors.field ? 'select-error' : ''}`}
disabled={isLoading}
>
<option value="" disabled>Select option</option>
{options.map((opt) => (
<option key={opt.id} value={opt.id}>{opt.name}</option>
))}
</select>
Textarea with Focus State
<textarea
className={`textarea textarea-bordered w-full min-h-28 focus:textarea-info ${errors.field ? 'textarea-error' : ''}`}
placeholder="Enter text..."
disabled={isLoading}
/>
Multi-Chip Input Container
<div className="flex flex-wrap border-2 border-base-300 rounded-xl p-3 min-h-[52px] bg-base-100 focus-within:border-accent transition-colors">
{/* Chips content */}
</div>
Toggle/Switch Pattern
Published Status Toggle
<Controller
name="published"
control={control}
render={({ field }) => (
<div
className={`flex items-center gap-3 border-2 rounded-xl px-4 h-12 transition-all duration-200 ${
field.value
? 'border-success bg-success/5'
: 'border-base-300 bg-base-100'
}`}
>
<input
type="checkbox"
className={`toggle ${field.value ? 'toggle-success' : ''}`}
checked={field.value}
onChange={field.onChange}
disabled={isLoading}
/>
<span className={`font-medium ${field.value ? 'text-success' : 'text-base-content/60'}`}>
{field.value ? 'Published' : 'Draft'}
</span>
{field.value && <Sparkles className="w-4 h-4 text-success ml-auto" />}
</div>
)}
/>
Badge Patterns
Optional Field Badge
<div className="flex items-center justify-between">
<h2 className="card-title text-lg">Section Title</h2>
<span className="badge badge-accent badge-outline">Optional</span>
</div>
Status Badge
{/* Published/Draft */}
<span className={`badge badge-sm ${item.published ? 'badge-success' : 'badge-ghost'}`}>
{item.published ? 'Published' : 'Draft'}
</span>
{/* Category Type */}
<span className={`badge badge-sm ${
item.categoryType === 'Blog' ? 'badge-primary' : 'badge-secondary'
}`}>
{item.categoryType}
</span>
Tag Badges
<div className="flex flex-wrap gap-1">
{tags.slice(0, 3).map((tag) => (
<span key={tag.id} className="badge badge-ghost badge-sm">
{tag.name}
</span>
))}
{tags.length > 3 && (
<span className="badge badge-ghost badge-sm">
+{tags.length - 3}
</span>
)}
</div>
Button Patterns
Primary Action Button
<button
type="submit"
className="btn btn-primary flex-1 gap-2 shadow-lg hover:shadow-primary/25"
disabled={isLoading}
>
{isSubmitting ? (
<>
<span className="loading loading-spinner loading-sm"></span>
{id ? 'Updating...' : 'Creating...'}
</>
) : (
<>
<Save className="w-4 h-4" />
{id ? 'Update' : 'Create'}
</>
)}
</button>
Ghost/Cancel Button
<button
type="button"
className="btn btn-ghost gap-2 hover:bg-base-200"
onClick={() => navigate('/admin/list')}
disabled={isLoading}
>
<ArrowLeft className="w-4 h-4" />
Cancel
</button>
Action Button Layout
<div className="flex flex-col-reverse sm:flex-row gap-3 pt-4">
<button className="btn btn-ghost gap-2">Cancel</button>
<button className="btn btn-primary flex-1 gap-2">Submit</button>
</div>
Small Outline Button
<button className="btn btn-sm btn-secondary btn-outline gap-1">
<Plus className="w-4 h-4" />
Add Item
</button>
Delete Button (Ghost with Error Color)
<button
className="btn btn-ghost btn-sm btn-square text-error hover:bg-error/10"
onClick={handleDelete}
title="Delete"
>
<Trash2 className="w-4 h-4" />
</button>
Empty State Pattern
Form Empty State
<div className="text-center py-10 border-2 border-dashed border-base-300 rounded-xl bg-base-200/30">
<div className="bg-secondary/10 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-3">
<Languages className="w-8 h-8 text-secondary/50" />
</div>
<p className="text-base-content/50 text-sm mb-3">No items added yet</p>
<button
type="button"
className="btn btn-sm btn-ghost text-secondary"
onClick={handleAdd}
>
<Plus className="w-4 h-4" />
Add first item
</button>
</div>
List Empty State
<div className="flex flex-col items-center justify-center py-16 px-4">
<div className="bg-base-200 rounded-full p-4 mb-4">
<FileText className="w-8 h-8 text-base-content/40" />
</div>
<h3 className="text-lg font-semibold mb-1">
{hasActiveFilters ? 'No items found' : 'No items yet'}
</h3>
<p className="text-base-content/60 text-center mb-4">
{hasActiveFilters
? 'Try adjusting your search or filter criteria'
: 'Get started by creating your first item'}
</p>
{hasActiveFilters ? (
<button className="btn btn-ghost btn-sm" onClick={clearFilters}>
Clear filters
</button>
) : (
<Link to="/create" className="btn btn-primary btn-sm gap-2">
<Plus className="w-4 h-4" />
Create Item
</Link>
)}
</div>
Table Styling
Modern Table
<div className="card bg-base-100 shadow-sm">
<div className="overflow-x-auto">
<table className="table">
<thead>
<tr className="bg-base-200/50">
<th className="cursor-pointer hover:bg-base-200 transition-colors">
Column Name
<SortIcon columnKey="field" />
</th>
</tr>
</thead>
<tbody>
{items.map((item) => (
<tr key={item.id} className="hover:bg-base-200/30 transition-colors">
<td className="font-medium">{item.name}</td>
<td>
<code className="text-xs bg-base-200 px-2 py-1 rounded">
{item.slug}
</code>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
Sort Icon Component
const SortIcon = ({ columnKey }: { columnKey: string }) => {
if (sortConfig?.key !== columnKey) return null;
return sortConfig.direction === 'asc' ? (
<ChevronUp className="inline w-4 h-4 ml-1" />
) : (
<ChevronDown className="inline w-4 h-4 ml-1" />
);
};
Filter UI Pattern
Collapsible Filter
<details className="group bg-base-100 shadow-sm rounded-lg">
<summary className="flex items-center justify-between cursor-pointer list-none p-4 font-medium">
<div className="flex items-center gap-2">
<span>Filters</span>
{hasActiveFilters && (
<span className="badge badge-primary badge-sm">
{filteredItems.length} / {items.length}
</span>
)}
</div>
<ChevronDown className="w-4 h-4 transition-transform group-open:rotate-180" />
</summary>
<div className="px-4 pb-4">
<div className="flex flex-col sm:flex-row gap-3">
{/* Filter inputs */}
</div>
</div>
</details>
Search Input with Clear
<label className="input input-bordered flex items-center gap-2 flex-1">
<Search className="w-4 h-4 opacity-50" />
<input
type="text"
placeholder="Search..."
className="grow"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
{searchTerm && (
<button
type="button"
className="btn btn-ghost btn-xs btn-circle"
onClick={() => setSearchTerm('')}
>
<X className="w-3 h-3" />
</button>
)}
</label>
Tab Pattern
Boxed Tabs with Active State
<div className="tabs tabs-boxed bg-base-200 p-1 rounded-xl mb-4">
{tabs.map((tab, index) => (
<button
key={tab.id}
type="button"
className={`tab gap-2 transition-all ${
activeTab === index ? 'tab-active bg-secondary text-secondary-content' : ''
}`}
onClick={() => setActiveTab(index)}
>
<Globe className="w-3 h-3" />
{tab.label}
</button>
))}
</div>
Tab Content Container
<div className={`space-y-4 p-4 bg-base-200/30 rounded-xl ${activeTab === index ? '' : 'hidden'}`}>
{/* Tab content */}
</div>
Pagination Pattern
<div className="card-body border-t border-base-200 py-3">
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
<span className="text-sm text-base-content/60">
Showing {startIndex + 1} to {endIndex} of {total} items
</span>
<div className="join">
<button
className="join-item btn btn-sm"
disabled={currentPage === 1}
onClick={() => setCurrentPage((p) => p - 1)}
>
Previous
</button>
{/* Page numbers */}
<button
className="join-item btn btn-sm"
disabled={currentPage === totalPages}
onClick={() => setCurrentPage((p) => p + 1)}
>
Next
</button>
</div>
</div>
</div>
Modal Pattern
DaisyUI Dialog Modal
<dialog id="modal_id" className={`modal ${isOpen ? 'modal-open' : ''}`}>
<div className="modal-box">
<h3 className="font-bold text-lg">Modal Title</h3>
<p className="py-4">Modal content goes here.</p>
<div className="modal-action">
<button className="btn btn-ghost" onClick={handleCancel}>
Cancel
</button>
<button className="btn btn-error" onClick={handleConfirm}>
{isLoading ? (
<>
<span className="loading loading-spinner loading-sm"></span>
Processing...
</>
) : (
'Confirm'
)}
</button>
</div>
</div>
<form method="dialog" className="modal-backdrop" onClick={handleCancel}>
<button>close</button>
</form>
</dialog>
Icon Usage
Icon Sizes
// Small (buttons, inline)
<Icon className="w-4 h-4" />
// Medium (card headers)
<Icon className="w-5 h-5" />
// Large (card icon badges)
<Icon className="w-6 h-6" />
// Extra Large (empty states)
<Icon className="w-8 h-8" />
Common Icons
import {
// Navigation
Home, ArrowLeft, ChevronDown, ChevronUp,
// Actions
Plus, Save, Pencil, Trash2, X, Search,
// Content Types
FileText, FolderOpen, ImagePlus, BookOpen,
// Features
Tag, Globe, Languages, Sparkles,
// Status
Check, AlertCircle, Info,
} from 'lucide-react';
Opacity/Transparency Patterns
Text Opacity
text-base-content/60 // Muted text (descriptions)
text-base-content/50 // More muted (help text)
text-base-content/40 // Very muted (placeholders, empty states)
Background Opacity
bg-primary/10 // Light tinted background (icon badges)
bg-base-200/30 // Subtle background (tab content)
bg-success/5 // Very light tint (active toggle)
hover:bg-error/10 // Hover state for error actions
Transition Patterns
Shadow Transition (Cards)
className="shadow-lg hover:shadow-xl transition-shadow duration-300"
Color Transition (Inputs)
className="border-base-300 focus-within:border-accent transition-colors"
Transform Transition (Icons)
className="transition-transform group-open:rotate-180"
All Transitions (Toggle States)
className="transition-all duration-200"
Responsive Patterns
Form Grid
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Form fields */}
</div>
Button Layout
<div className="flex flex-col-reverse sm:flex-row gap-3">
{/* Buttons */}
</div>
Filter Layout
<div className="flex flex-col sm:flex-row gap-3">
{/* Filter inputs */}
</div>
Best Practices
- Consistent Color Scheme: Use the same border color and icon color for each card section
- Hover Effects: Add
hover:shadow-xl transition-shadow duration-300to interactive cards - Focus States: Use
focus:input-{color}for matching focus colors - Dividers: Use
<div className="divider my-2"></div>between card header and content - Icon Badges: Wrap icons in colored rounded containers for visual hierarchy
- Optional Labels: Use
badge badge-{color} badge-outlinefor optional field indicators - Empty States: Include icon, message, and action button
- Loading States: Use DaisyUI loading spinners with appropriate sizes
- Error States: Use
text-errorandinput-errorclasses consistently - Full Width Forms: Remove max-width constraints for full-width layouts