tailwind-capacitor

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Tailwind CSS for Capacitor Apps

为Capacitor应用使用Tailwind CSS

Build beautiful mobile apps with Tailwind CSS and Capacitor.
使用Tailwind CSS和Capacitor构建美观的移动应用。

When to Use This Skill

何时使用本技能

  • User is using Tailwind in Capacitor app
  • User asks about mobile styling
  • User needs responsive mobile design
  • User wants dark mode with Tailwind
  • User needs safe area handling
  • 用户在Capacitor应用中使用Tailwind
  • 用户询问移动端样式设置相关问题
  • 用户需要响应式移动端设计方案
  • 用户想要在Tailwind中实现深色模式
  • 用户需要处理安全区域适配

Getting Started

入门指南

Installation

安装

bash
bun add -D tailwindcss postcss autoprefixer
bunx tailwindcss init -p
bash
bun add -D tailwindcss postcss autoprefixer
bunx tailwindcss init -p

Configuration

配置

javascript
// 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: [],
};
javascript
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    './index.html',
    './src/**/*.{js,ts,jsx,tsx,vue,svelte}',
  ],
  theme: {
    extend: {
      // 移动端优先间距设置
      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)',
      },
      // 最小触摸目标(44px)
      minHeight: {
        'touch': '44px',
      },
      minWidth: {
        'touch': '44px',
      },
    },
  },
  plugins: [],
};

Import Styles

导入样式

css
/* 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;
  }
}
css
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

/* 移动端专属基础样式 */
@layer base {
  html {
    /* 防止旋转屏幕时调整文本大小 */
    -webkit-text-size-adjust: 100%;
    /* 平滑滚动 */
    scroll-behavior: smooth;
    /* 防止过度滚动时触发下拉刷新 */
    overscroll-behavior: none;
  }

  body {
    /* 防止长按选中文本 */
    -webkit-user-select: none;
    user-select: none;
    /* 禁用长按呼出菜单 */
    -webkit-touch-callout: none;
    /* 防止iOS弹性滚动 */
    position: fixed;
    width: 100%;
    height: 100%;
    overflow: hidden;
  }

  /* 允许输入框选中文本 */
  input, textarea {
    -webkit-user-select: text;
    user-select: text;
  }
}

Safe Area Handling

安全区域适配

Utility Classes

工具类

javascript
// 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)',
    },
  },
},
javascript
// 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)',
    },
  },
},

Usage in Components

组件中使用

tsx
// 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>
  );
}
tsx
// 带安全区域的头部
function Header() {
  return (
    <header className="
      fixed top-0 left-0 right-0
      pt-safe-t  /* 为刘海屏预留内边距 */
      bg-white dark:bg-gray-900
      border-b border-gray-200
    ">
      <div className="px-4 h-14 flex items-center">
        <h1 className="font-semibold">应用标题</h1>
      </div>
    </header>
  );
}

// 带安全区域的底部
function Footer() {
  return (
    <footer className="
      fixed bottom-0 left-0 right-0
      pb-safe-b  /* 为Home键指示器预留内边距 */
      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">首页</button>
        <button className="min-h-touch min-w-touch">搜索</button>
        <button className="min-h-touch min-w-touch">我的</button>
      </div>
    </footer>
  );
}

// 主内容区
function Main() {
  return (
    <main className="
      pt-safe-t  /* 适配头部+刘海屏高度 */
      pb-safe-b  /* 适配底部+Home键指示器高度 */
      h-screen
      overflow-y-auto
      overscroll-none
    ">
      {/* 内容 */}
    </main>
  );
}

Touch-Friendly Design

友好触摸设计

Minimum Touch Targets

最小触摸目标

tsx
// 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>
  );
}
tsx
// Apple人机交互指南建议最小44x44pt
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
    ">
      点击我
    </button>
  );
}

// 带合适触摸区域的图标按钮
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" />  {/* 图标小于触摸区域 */}
    </button>
  );
}

Touch Feedback

触摸反馈

css
/* Add to index.css */
@layer utilities {
  .touch-feedback {
    @apply transition-colors duration-75;
  }

  .touch-feedback:active {
    @apply bg-black/5 dark:bg-white/5;
  }
}
tsx
<button className="touch-feedback p-4 rounded-lg">
  With Feedback
</button>
css
/* 添加到index.css */
@layer utilities {
  .touch-feedback {
    @apply transition-colors duration-75;
  }

  .touch-feedback:active {
    @apply bg-black/5 dark:bg-white/5;
  }
}
tsx
<button className="touch-feedback p-4 rounded-lg">
  带触摸反馈
</button>

Disable Hover on Touch

禁用触摸设备上的悬停效果

javascript
// tailwind.config.js
module.exports = {
  future: {
    hoverOnlyWhenSupported: true, // Disables hover on touch devices
  },
};
Or use media query:
css
@media (hover: hover) {
  .hover-only:hover {
    @apply bg-gray-100;
  }
}
javascript
// tailwind.config.js
module.exports = {
  future: {
    hoverOnlyWhenSupported: true, // 在触摸设备上禁用悬停效果
  },
};
或使用媒体查询:
css
@media (hover: hover) {
  .hover-only:hover {
    @apply bg-gray-100;
  }
}

Dark Mode

深色模式

System Dark Mode

系统深色模式

javascript
// tailwind.config.js
module.exports = {
  darkMode: 'media', // or 'class' for manual control
};
javascript
// tailwind.config.js
module.exports = {
  darkMode: 'media', // 或使用'class'进行手动控制
};

Manual Dark Mode

手动深色模式

javascript
// tailwind.config.js
module.exports = {
  darkMode: 'class',
};
typescript
// 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);
    }
  });
javascript
// tailwind.config.js
module.exports = {
  darkMode: 'class',
};
typescript
// 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');
  }
}

// 监听系统主题变化
window.matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', async (e) => {
    const theme = await Preferences.get({ key: 'theme' });
    if (theme.value === 'system') {
      document.documentElement.classList.toggle('dark', e.matches);
    }
  });

Dark Mode Components

深色模式组件

tsx
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>
  );
}
tsx
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">
      卡片标题
      </h3>
      <p className="text-gray-600 dark:text-gray-400">
      卡片内容
      </p>
    </div>
  );
}

Mobile Patterns

移动端模式

Pull to Refresh Container

下拉刷新容器

tsx
function PullToRefresh({ onRefresh, children }) {
  return (
    <div className="
      h-full
      overflow-y-auto
      overscroll-contain
      touch-pan-y
    ">
      {children}
    </div>
  );
}
tsx
function PullToRefresh({ onRefresh, children }) {
  return (
    <div className="
      h-full
      overflow-y-auto
      overscroll-contain
      touch-pan-y
    ">
      {children}
    </div>
  );
}

Bottom Sheet

底部弹窗

tsx
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>
    </>
  );
}
tsx
function BottomSheet({ isOpen, onClose, children }) {
  return (
    <>
      {/* 遮罩层 */}
      <div
        className={`
          fixed inset-0
          bg-black/50
          transition-opacity duration-300
          ${isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'}
        `}
        onClick={onClose}
      />

      {/* 弹窗内容 */}
      <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'}
        `}
      >
        {/* 拖拽手柄 */}
        <div className="flex justify-center py-2">
          <div className="w-10 h-1 bg-gray-300 rounded-full" />
        </div>

        {children}
      </div>
    </>
  );
}

Swipe Actions

滑动操作

tsx
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>
  );
}
tsx
function SwipeableItem({ children, onDelete }) {
  return (
    <div className="relative overflow-hidden">
      {/* 背景操作按钮 */}
      <div className="
        absolute inset-y-0 right-0
        flex items-center
        bg-red-500
        px-4
      ">
        <span className="text-white">删除</span>
      </div>

      {/* 前景内容 */}
      <div className="
        relative
        bg-white dark:bg-gray-800
        transform transition-transform
        active:cursor-grabbing
      ">
        {children}
      </div>
    </div>
  );
}

Fixed Header with Blur

带模糊效果的固定头部

tsx
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>
  );
}
tsx
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">标题</h1>
      </div>
    </header>
  );
}

Performance Optimization

性能优化

Reduce Bundle Size

减小包体积

javascript
// tailwind.config.js
module.exports = {
  content: [/* ... */],
  // Only include used utilities
  safelist: [], // Add dynamic classes here if needed
};
javascript
// tailwind.config.js
module.exports = {
  content: [/* ... */],
  // 仅包含用到的工具类
  safelist: [], // 如有动态类,可在此添加
};

GPU Acceleration

GPU加速

tsx
// Use transform for animations (GPU accelerated)
<div className="
  transform transition-transform duration-200
  hover:scale-105
  will-change-transform
">
  Animated Element
</div>
tsx
// 使用transform实现动画(GPU加速)
<div className="
  transform transition-transform duration-200
  hover:scale-105
  will-change-transform
">
  动画元素
</div>

Avoid Layout Thrashing

避免布局抖动

tsx
// BAD: Causes reflow
<div className="w-full h-auto">

// GOOD: Fixed dimensions
<div className="w-full h-48">
tsx
// 错误写法:会导致重排
<div className="w-full h-auto">

// 正确写法:固定尺寸
<div className="w-full h-48">

Component Examples

组件示例

Mobile List Item

移动端列表项

tsx
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>
  );
}
tsx
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>
  );
}

Mobile Button

移动端按钮

tsx
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>
  );
}
tsx
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>
  );
}

Mobile Input

移动端输入框

tsx
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>
  );
}
tsx
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  /* 防止iOS自动缩放 */
          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>
  );
}

Resources

参考资源