From capacitor-ui
Guides styling Capacitor mobile apps with Tailwind CSS, covering mobile-first design, safe areas, touch targets, dark mode, and performance optimization.
npx claudepluginhub cap-go/capgo-skills --plugin capacitor-uiThis skill uses the workspace's default tool permissions.
Build beautiful mobile apps with Tailwind CSS and Capacitor.
Guides integrating web frameworks like Next.js, React, Vue, Angular, Svelte with Capacitor for mobile apps via static exports. Use when converting web apps to hybrid native mobile.
Guides Ionic Framework component usage, theming, platform-specific styling, and best practices for native-looking Capacitor mobile apps using React, Vue, or Angular.
Enforces mobile UI accessibility in React Native, iOS/Android/Flutter apps: 44pt touch targets, WCAG contrast, visible button boundaries, accessibility labels.
Share bugs, ideas, or general feedback.
Build beautiful mobile apps with Tailwind CSS and Capacitor.
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx,vue,svelte}',
],
theme: {
extend: {
// Mobile-first spacing
spacing: {
'safe-top': 'env(safe-area-inset-top)',
'safe-bottom': 'env(safe-area-inset-bottom)',
'safe-left': 'env(safe-area-inset-left)',
'safe-right': 'env(safe-area-inset-right)',
},
// Minimum touch targets (44px)
minHeight: {
'touch': '44px',
},
minWidth: {
'touch': '44px',
},
},
},
plugins: [],
};
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Mobile-specific base styles */
@layer base {
html {
/* Prevent text size adjustment on orientation change */
-webkit-text-size-adjust: 100%;
/* Smooth scrolling */
scroll-behavior: smooth;
/* Prevent pull-to-refresh on overscroll */
overscroll-behavior: none;
}
body {
/* Prevent text selection on long press */
-webkit-user-select: none;
user-select: none;
/* Disable callout on long press */
-webkit-touch-callout: none;
/* Prevent elastic scrolling on iOS */
position: fixed;
width: 100%;
height: 100%;
overflow: hidden;
}
/* Enable text selection in inputs */
input, textarea {
-webkit-user-select: text;
user-select: text;
}
}
// tailwind.config.js
theme: {
extend: {
padding: {
'safe': 'env(safe-area-inset-bottom)',
'safe-t': 'env(safe-area-inset-top)',
'safe-b': 'env(safe-area-inset-bottom)',
'safe-l': 'env(safe-area-inset-left)',
'safe-r': 'env(safe-area-inset-right)',
},
margin: {
'safe': 'env(safe-area-inset-bottom)',
'safe-t': 'env(safe-area-inset-top)',
'safe-b': 'env(safe-area-inset-bottom)',
},
},
},
// Header with safe area
function Header() {
return (
<header className="
fixed top-0 left-0 right-0
pt-safe-t /* Padding for notch */
bg-white dark:bg-gray-900
border-b border-gray-200
">
<div className="px-4 h-14 flex items-center">
<h1 className="font-semibold">App Title</h1>
</div>
</header>
);
}
// Footer with safe area
function Footer() {
return (
<footer className="
fixed bottom-0 left-0 right-0
pb-safe-b /* Padding for home indicator */
bg-white dark:bg-gray-900
border-t border-gray-200
">
<div className="px-4 h-14 flex items-center justify-around">
<button className="min-h-touch min-w-touch">Home</button>
<button className="min-h-touch min-w-touch">Search</button>
<button className="min-h-touch min-w-touch">Profile</button>
</div>
</footer>
);
}
// Main content
function Main() {
return (
<main className="
pt-safe-t /* Account for header + notch */
pb-safe-b /* Account for footer + home indicator */
h-screen
overflow-y-auto
overscroll-none
">
{/* Content */}
</main>
);
}
// Apple HIG recommends 44x44pt minimum
function TouchableButton() {
return (
<button className="
min-h-[44px] min-w-[44px]
px-4 py-3
flex items-center justify-center
active:bg-gray-100
rounded-lg
">
Tap Me
</button>
);
}
// Icon button with proper touch target
function IconButton() {
return (
<button className="
h-11 w-11 /* 44px */
flex items-center justify-center
rounded-full
active:bg-gray-100
">
<svg className="w-6 h-6" /> {/* Icon smaller than touch area */}
</button>
);
}
/* Add to index.css */
@layer utilities {
.touch-feedback {
@apply transition-colors duration-75;
}
.touch-feedback:active {
@apply bg-black/5 dark:bg-white/5;
}
}
<button className="touch-feedback p-4 rounded-lg">
With Feedback
</button>
// tailwind.config.js
module.exports = {
future: {
hoverOnlyWhenSupported: true, // Disables hover on touch devices
},
};
Or use media query:
@media (hover: hover) {
.hover-only:hover {
@apply bg-gray-100;
}
}
// tailwind.config.js
module.exports = {
darkMode: 'media', // or 'class' for manual control
};
// tailwind.config.js
module.exports = {
darkMode: 'class',
};
// theme.ts
import { Preferences } from '@capacitor/preferences';
type Theme = 'light' | 'dark' | 'system';
async function setTheme(theme: Theme) {
await Preferences.set({ key: 'theme', value: theme });
if (theme === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.classList.toggle('dark', prefersDark);
} else {
document.documentElement.classList.toggle('dark', theme === 'dark');
}
}
// Listen for system changes
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e) => {
const theme = await Preferences.get({ key: 'theme' });
if (theme.value === 'system') {
document.documentElement.classList.toggle('dark', e.matches);
}
});
function Card() {
return (
<div className="
bg-white dark:bg-gray-800
border border-gray-200 dark:border-gray-700
rounded-xl
shadow-sm dark:shadow-none
">
<h3 className="text-gray-900 dark:text-white">
Card Title
</h3>
<p className="text-gray-600 dark:text-gray-400">
Card content
</p>
</div>
);
}
function PullToRefresh({ onRefresh, children }) {
return (
<div className="
h-full
overflow-y-auto
overscroll-contain
touch-pan-y
">
{children}
</div>
);
}
function BottomSheet({ isOpen, onClose, children }) {
return (
<>
{/* Backdrop */}
<div
className={`
fixed inset-0
bg-black/50
transition-opacity duration-300
${isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}
`}
onClick={onClose}
/>
{/* Sheet */}
<div
className={`
fixed left-0 right-0 bottom-0
bg-white dark:bg-gray-900
rounded-t-2xl
pb-safe-b
transition-transform duration-300 ease-out
${isOpen ? 'translate-y-0' : 'translate-y-full'}
`}
>
{/* Handle */}
<div className="flex justify-center py-2">
<div className="w-10 h-1 bg-gray-300 rounded-full" />
</div>
{children}
</div>
</>
);
}
function SwipeableItem({ children, onDelete }) {
return (
<div className="relative overflow-hidden">
{/* Background action */}
<div className="
absolute inset-y-0 right-0
flex items-center
bg-red-500
px-4
">
<span className="text-white">Delete</span>
</div>
{/* Foreground content */}
<div className="
relative
bg-white dark:bg-gray-800
transform transition-transform
active:cursor-grabbing
">
{children}
</div>
</div>
);
}
function BlurHeader() {
return (
<header className="
fixed top-0 left-0 right-0
pt-safe-t
bg-white/80 dark:bg-gray-900/80
backdrop-blur-lg
border-b border-gray-200/50
z-50
">
<div className="h-14 px-4 flex items-center">
<h1 className="font-semibold">Title</h1>
</div>
</header>
);
}
// tailwind.config.js
module.exports = {
content: [/* ... */],
// Only include used utilities
safelist: [], // Add dynamic classes here if needed
};
// Use transform for animations (GPU accelerated)
<div className="
transform transition-transform duration-200
hover:scale-105
will-change-transform
">
Animated Element
</div>
// BAD: Causes reflow
<div className="w-full h-auto">
// GOOD: Fixed dimensions
<div className="w-full h-48">
function ListItem({ title, subtitle, image, onClick }) {
return (
<button
onClick={onClick}
className="
w-full
flex items-center gap-4
px-4 py-3
min-h-[60px]
active:bg-gray-50 dark:active:bg-gray-800
text-left
"
>
{image && (
<img
src={image}
className="w-12 h-12 rounded-full object-cover"
alt=""
/>
)}
<div className="flex-1 min-w-0">
<p className="font-medium text-gray-900 dark:text-white truncate">
{title}
</p>
{subtitle && (
<p className="text-sm text-gray-500 truncate">
{subtitle}
</p>
)}
</div>
<svg className="w-5 h-5 text-gray-400" />
</button>
);
}
function MobileButton({ children, variant = 'primary', ...props }) {
const variants = {
primary: 'bg-blue-500 text-white active:bg-blue-600',
secondary: 'bg-gray-100 text-gray-900 active:bg-gray-200',
danger: 'bg-red-500 text-white active:bg-red-600',
};
return (
<button
className={`
w-full
min-h-[48px]
px-6 py-3
font-semibold
rounded-xl
transition-colors duration-75
disabled:opacity-50
${variants[variant]}
`}
{...props}
>
{children}
</button>
);
}
function MobileInput({ label, error, ...props }) {
return (
<label className="block">
{label && (
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 block">
{label}
</span>
)}
<input
className={`
w-full
px-4 py-3
text-base /* Prevents iOS zoom */
bg-gray-50 dark:bg-gray-800
border rounded-xl
placeholder-gray-400
focus:outline-none focus:ring-2 focus:ring-blue-500
${error
? 'border-red-500'
: 'border-gray-200 dark:border-gray-700'
}
`}
{...props}
/>
{error && (
<span className="text-sm text-red-500 mt-1 block">{error}</span>
)}
</label>
);
}