watercolor-ui
Version:
A modern minimalist cross-framework component library
538 lines (448 loc) • 15.7 kB
Markdown
<div align="center">

A modern minimalist cross-framework component library supporting Vue 3 and React 18+, perfect alternative to Material-UI.
[](https://badge.fury.io/js/watercolor-ui)
[](https://lbesson.mit-license.org/)
[](https://vuejs.org/)
[](https://reactjs.org/)
[](https://www.typescriptlang.org/)
[](https://storybook.js.org/)
[🚀 Quick Start](
</div>
- 🎨 **Watercolor Design Language** - Modern watercolor-style design, soft and elegant
- 🌙 **Dark Mode Support** - Complete dark mode support with automatic switching
- 🎯 **Cross-Framework Compatible** - Supports both Vue 3 and React 18+
- 🔄 **Material-UI Compatibility Layer** - Complete Material-UI API compatibility for zero-cost migration
- 🎭 **Theme System** - Fully customizable color system and theme presets
- 🛠️ **TypeScript First** - Complete type support and IntelliSense
- 📚 **Storybook Documentation** - Complete component documentation and interactive demos
- 🚀 **Zero-Config Migration** - Seamless migration from Material-UI with minimal code changes
- ⚡ **Performance Optimized** - Tree-shaking optimization with excellent runtime performance
- 🌐 **Internationalization** - Built-in multi-language support
- 🎁 **Rich Components** - 60+ high-quality components covering all common use cases
- 🎪 **Flat Design** - Ultra-minimal flat design without shadows or borders, pure HTML+CSS
```bash
npm install watercolor-ui
yarn add watercolor-ui
pnpm add watercolor-ui
```
Make sure to import styles in your project entry file:
```js
// main.js or main.ts
import 'watercolor-ui/dist/style.css'
```
The simplest migration approach is using our compatibility layer with minimal code changes:
```javascript
// Replace all Material-UI imports with Watercolor compatibility layer
// import { Button, TextField, Card, Container, Typography } from '@mui/material'
import { Button, TextField, Card, Container, Typography } from 'watercolor-ui/mui-compat'
// No other code changes needed, API is fully compatible!
```
```vue
<template>
<div class="app">
<!-- Button Component -->
<ButtonVue
variant="primary"
size="md"
:loading="isLoading"
@click="handleClick"
>
{{ isLoading ? 'Loading...' : 'Click Button' }}
</ButtonVue>
<!-- Input Component -->
<InputVue
v-model="form.username"
label="Username"
placeholder="Enter username"
:required="true"
:error="errors.username"
helper-text="Username must be at least 3 characters"
/>
<!-- Card Component -->
<CardVue
title="User Information"
variant="elevated"
padding="lg"
>
<p>{{ form.username || 'No username yet' }}</p>
<template
<div class="flex gap-2">
<ButtonVue variant="secondary" size="sm">Cancel</ButtonVue>
<ButtonVue variant="primary" size="sm">Save</ButtonVue>
</div>
</template>
</CardVue>
<!-- Table Component -->
<TableVue :data="tableData" :columns="tableColumns" />
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import {
ButtonVue,
InputVue,
CardVue,
TableVue
} from 'watercolor-ui'
const isLoading = ref(false)
const form = reactive({
username: ''
})
const errors = reactive({
username: ''
})
const tableData = ref([
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
])
const tableColumns = [
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' }
]
const handleClick = async () => {
isLoading.value = true
// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 2000))
isLoading.value = false
}
</script>
```
```jsx
import React, { useState } from 'react'
import {
ButtonReact,
InputReact,
CardReact,
TableReact,
themeVUE,
useToast
} from 'watercolor-ui'
function App() {
const [isLoading, setIsLoading] = useState(false)
const [form, setForm] = useState({ username: '' })
const [errors, setErrors] = useState({})
const theme = themeVUE()
const toast = useToast()
const tableData = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
]
const tableColumns = [
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' }
]
const handleClick = async () => {
setIsLoading(true)
try {
// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 2000))
toast.success('Operation successful!')
} catch (error) {
toast.error('Operation failed: ' + error.message)
} finally {
setIsLoading(false)
}
}
const handleInputChange = (e) => {
const { name, value } = e.target
setForm(prev => ({ ...prev, [name]: value }))
// Simple validation
if (name === 'username' && value.length < 3) {
setErrors(prev => ({ ...prev, username: 'Username must be at least 3 characters' }))
} else {
setErrors(prev => ({ ...prev, [name]: '' }))
}
}
return (
<div className="app p-6 space-y-6">
{/* Button Component */}
<ButtonReact
variant="primary"
size="md"
loading={isLoading}
onClick={handleClick}
>
{isLoading ? 'Loading...' : 'Click Button'}
</ButtonReact>
{/* Input Component */}
<InputReact
name="username"
value={form.username}
onChange={handleInputChange}
label="Username"
placeholder="Enter username"
required
error={errors.username}
helperText="Username must be at least 3 characters"
/>
{/* Card Component */}
<CardReact
title="User Information"
variant="elevated"
padding="lg"
footer={
<div className="flex gap-2">
<ButtonReact variant="secondary" size="sm">Cancel</ButtonReact>
<ButtonReact variant="primary" size="sm">Save</ButtonReact>
</div>
}
>
<p>{form.username || 'No username yet'}</p>
</CardReact>
{/* Table Component */}
<TableReact data={tableData} columns={tableColumns} />
</div>
)
}
export default App
```
```js
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx,vue}",
"./node_modules/watercolor-ui/**/*.{js,ts,jsx,tsx,vue}",
],
theme: {
extend: {
colors: {
// Watercolor UI theme color variables
primary: {
50: 'rgb(var(--wc-primary-50) / <alpha-value>)',
100: 'rgb(var(--wc-primary-100) / <alpha-value>)',
200: 'rgb(var(--wc-primary-200) / <alpha-value>)',
300: 'rgb(var(--wc-primary-300) / <alpha-value>)',
400: 'rgb(var(--wc-primary-400) / <alpha-value>)',
500: 'rgb(var(--wc-primary-500) / <alpha-value>)',
600: 'rgb(var(--wc-primary-600) / <alpha-value>)',
700: 'rgb(var(--wc-primary-700) / <alpha-value>)',
800: 'rgb(var(--wc-primary-800) / <alpha-value>)',
900: 'rgb(var(--wc-primary-900) / <alpha-value>)',
950: 'rgb(var(--wc-primary-950) / <alpha-value>)',
},
secondary: {
50: 'rgb(var(--wc-secondary-50) / <alpha-value>)',
100: 'rgb(var(--wc-secondary-100) / <alpha-value>)',
200: 'rgb(var(--wc-secondary-200) / <alpha-value>)',
300: 'rgb(var(--wc-secondary-300) / <alpha-value>)',
400: 'rgb(var(--wc-secondary-400) / <alpha-value>)',
500: 'rgb(var(--wc-secondary-500) / <alpha-value>)',
600: 'rgb(var(--wc-secondary-600) / <alpha-value>)',
700: 'rgb(var(--wc-secondary-700) / <alpha-value>)',
800: 'rgb(var(--wc-secondary-800) / <alpha-value>)',
900: 'rgb(var(--wc-secondary-900) / <alpha-value>)',
950: 'rgb(var(--wc-secondary-950) / <alpha-value>)',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
}
},
},
plugins: [],
darkMode: 'class',
}
```
Watercolor UI now uses a **file-based theme system** for maximum flexibility. Create a `theme.config.json` file in your project root:
```json
{
"primary": {
"50": "#eff6ff",
"100": "#dbeafe",
"200": "#bfdbfe",
"300": "#93c5fd",
"400": "#60a5fa",
"500": "#3b82f6",
"600": "#2563eb",
"700": "#1d4ed8",
"800": "#1e40af",
"900": "#1e3a8a"
},
"secondary": {
"50": "#f3f4ff",
"100": "#e5e7ff",
"200": "#c7d2fe",
"300": "#a5b4fc",
"400": "#818cf8",
"500": "#6366f1",
"600": "#4f46e5",
"700": "#4338ca",
"800": "#3730a3",
"900": "#312e81"
},
"fonts": {
"chinese": "Noto Sans SC",
"english": "Inter",
"fallback": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
}
}
```
```js
import { setTheme, loadThemeConfig } from 'watercolor-ui'
// Method 1: Load from file (recommended)
await loadThemeConfig() // Loads /theme.config.json
await loadThemeConfig('/custom-theme.json') // Custom path
// Method 2: Direct theme setting
setTheme({
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9', // Primary color
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e'
},
secondary: {
50: '#fafaf9',
100: '#f5f5f4',
200: '#e7e5e4',
300: '#d6d3d1',
400: '#a8a29e',
500: '#78716c', // Secondary color
600: '#57534e',
700: '#44403c',
800: '#292524',
900: '#1c1917'
},
fonts: {
chinese: 'PingFang SC',
english: 'SF Pro Display',
fallback: 'system-ui, sans-serif'
}
})
```
```js
// Switch between different theme files
await loadThemeConfig('/themes/blue.json') // Blue theme
await loadThemeConfig('/themes/green.json') // Green theme
await loadThemeConfig('/themes/orange.json') // Orange theme
await loadThemeConfig('/themes/brand.json') // Brand theme
```
```js
import {
toggleDarkMode,
isDarkMode,
setDarkMode,
useDarkMode // React Hook
} from 'watercolor-ui'
// Toggle dark mode
toggleDarkMode()
// Set dark mode
setDarkMode(true) // Enable dark mode
setDarkMode(false) // Disable dark mode
// Check current mode
const darkModeEnabled = isDarkMode()
// React Hook usage
function ThemeToggle() {
const [isDark, setIsDark] = useDarkMode()
return (
<ButtonReact onClick={() => setIsDark(!isDark)}>
{isDark ? '🌞 Light Mode' : '🌙 Dark Mode'}
</ButtonReact>
)
}
```
Watercolor UI provides **60+** high-quality components, completely covering all modern web application use cases:
| Component | Vue | React | Material-UI Compatible | Description |
|------|:---:|:-----:|:----------------:|------|
| Button | ✅ | ✅ | ✅ | Button component with multiple variants and states |
| IconButton | ✅ | ✅ | ✅ | Icon button |
| Fab | ✅ | ✅ | ✅ | Floating action button |
| TextField | ✅ | ✅ | ✅ | Text input field |
| Input | ✅ | ✅ | ✅ | Basic input component |
| Select | ✅ | ✅ | ✅ | Select dropdown |
| Checkbox | ✅ | ✅ | ✅ | Checkbox |
| Radio | ✅ | ✅ | ✅ | Radio button |
| RadioGroup | ✅ | ✅ | ✅ | Radio button group |
| Switch | ✅ | ✅ | ✅ | Toggle switch |
| Slider | ✅ | ✅ | ✅ | Slider |
| FileInput | ✅ | ✅ | ⭕ | File upload |
| DatePicker | ✅ | ✅ | ⭕ | Date picker |
| ColorPicker | ✅ | ✅ | ⭕ | Color picker |
| VerificationCodeInput | ✅ | ✅ | ⭕ | Verification code input |
| Component | Vue | React | Material-UI Compatible | Description |
|------|:---:|:-----:|:----------------:|------|
| Container | ✅ | ✅ | ✅ | Container component |
| Box | ✅ | ✅ | ✅ | Box layout |
| Grid | ✅ | ✅ | ✅ | Grid layout |
| Paper | ✅ | ✅ | ✅ | Paper container |
| Component | Vue | React | Material-UI Compatible | Description |
|------|:---:|:-----:|:----------------:|------|
| Alert | ✅ | ✅ | ✅ | Alert message |
| Snackbar | ✅ | ✅ | ✅ | Toast notification |
| Tooltip | ✅ | ✅ | ✅ | Tooltip |
| Progress | ✅ | ✅ | ✅ | Progress indicator |
| Skeleton | ✅ | ✅ | ✅ | Loading skeleton |
| Spinner | ✅ | ✅ | ⭕ | Loading spinner |
| Component | Vue | React | Material-UI Compatible | Description |
|------|:---:|:-----:|:----------------:|------|
| Table | ✅ | ✅ | ✅ | Data table |
| List | ✅ | ✅ | ✅ | List |
| Card | ✅ | ✅ | ✅ | Card container |
| Avatar | ✅ | ✅ | ✅ | Avatar |
| Badge | ✅ | ✅ | ✅ | Badge |
| Chip | ✅ | ✅ | ✅ | Chip |
| Typography | ✅ | ✅ | ✅ | Typography |
| Rating | ✅ | ✅ | ⭕ | Star rating |
Watercolor UI follows an **ultra-minimal flat design** philosophy:
- **No Shadows**: Pure flat design without any drop shadows or elevations
- **No Borders**: Clean interfaces using background colors for separation
- **Pure CSS**: All styling done with pure HTML+CSS, no complex animations
- **Theme-Driven**: Complete reliance on CSS variables for theming
- **Accessibility First**: Full keyboard navigation and screen reader support
```bash
npm install
npm run dev
npm run build
npm run storybook
npm test
npm run lint
```
MIT License - see [LICENSE](LICENSE) file for details.
We welcome all contributions! Please read our [Contributing Guide](CONTRIBUTING.md) for details.
- [ ] More component variants
- [ ] Advanced theme system
- [ ] Better accessibility
- [ ] More framework support (Angular, Svelte)
- [ ] Component playground
- [ ] Design tokens
---
Made with ❤️ by the Watercolor UI team