TailwindCSS 互動性:cursor、scroll-snap、field-sizing 全解 | TailwindCSS 完整教學

2026/08/23
TailwindCSS 互動性:cursor、scroll-snap、field-sizing 全解 | TailwindCSS 完整教學

前幾篇我們把元素「排得整齊、看得漂亮」,這一篇要讓介面真正「用起來順手」。TailwindCSS 的互動性(Interactivity)工具,涵蓋 cursor-*(游標樣式)、pointer-events-*(指標事件穿透)、select-*(文字選取)、resize-*(縮放把手)、scroll-smoothsnap-*(滾動吸附輪播)、touch-*(觸控行為)、以及 v4 新增的 field-sizing-content(自動長高)、scheme-*(配色宣告)、accent-*(強調色)、caret-*(游標色)、will-change-*(渲染提示)與 appearance-none(移除原生外觀)。這一組看似瑣碎的細節工具,正是把介面從「能操作」打磨到「好操作」的關鍵——讓每一次點擊、滑動、輸入都恰到好處

前言

上一篇 《表格與清單》 我們把資料排進格子、把重點條列成清單,完成了「內容排版」的功課。到這裡,讓「內容好看、好讀」的視覺工具你已大致收齊。這一篇 互動性(Interactivity),我們要把視線從「畫面長什麼樣」轉向「使用者怎麼跟畫面互動」——滑鼠移過去游標變成什麼形狀、這段文字能不能被選取、這個遮罩擋不擋點擊、捲動要瞬間跳還是平滑滑、手機上滑動與縮放的行為怎麼調、輸入框要不要跟著內容自動長高。

先用一個生活化的類比建立心智模型。如果說前面的視覺工具是在裝潢一間店面,那互動性工具就是在調整「顧客動線與操作手感」:門把(游標)要讓人一看就知道能不能推;地上(pointer-events)哪裡能走、哪裡是裝飾不能踩;櫥窗商品(user-select)能不能拿起來看;電動門(scroll-smooth)要緩緩開還是啪一下彈開;手扶梯(scroll-snap)每一階都要停在定點還是能一路滑過去;觸控螢幕(touch-action)點一下要立刻反應還是等你會不會雙擊。這些都不是「畫面本身」,而是「人與畫面之間」的細節——而細節,正是專業與業餘的分野。

本系列以 TailwindCSS v4 為預設版本。互動性這組工具在 v4 補齊了好幾個過去只能寫原生 CSS 的能力(field-sizingscheme-*)。本篇你將學到:

  • 指標與選取:cursor-* 游標樣式、pointer-events-* 事件穿透、select-* 文字選取、resize-* 縮放把手
  • 滾動體驗:scroll-smooth 平滑滾動、scroll-mt-* 錨點偏移、snap-* 滾動吸附(輪播的靈魂)、touch-* 觸控行為
  • 表單質感:v4 的 field-sizing-content 自動長高、accent-* 強調色、caret-* 游標色、appearance-none 移除原生外觀
  • 配色與效能:v4 的 scheme-* 配色宣告(影響原生捲軸/表單明暗)、will-change-* 渲染提示的正確用法

本系列以 TailwindCSS v4 為預設版本,範例皆可直接執行。凡涉及 v3 差異之處,我會特別標註。

核心概念

互動性工具的分類全景

Tailwind 的互動性 utility 數量不少,但依「控制什麼」可以整理成六大群組。下表先建立全景,涵蓋每個工具的代表 class 與對應 CSS:

群組代表 utility對應 CSS作用
游標cursor-pointercursor-grabcursor-not-allowedcursor滑鼠移過時的游標形狀
指標事件pointer-events-nonepointer-events-autopointer-events元素是否接收/穿透指標事件
文字選取select-noneselect-textselect-alluser-select使用者能否選取文字
縮放把手resizeresize-yresize-noneresizetextarea 右下角拖曳把手方向
滾動scroll-smoothsnap-xsnap-mandatorysnap-startscroll-behaviorscroll-snap-*平滑滾動與滾動吸附
觸控touch-manipulationtouch-pan-ytouch-nonetouch-action移動端手勢行為
表單/外觀(多為 v4)field-sizing-contentaccent-*caret-*scheme-*appearance-none對應同名 CSS表單控件的尺寸/顏色/明暗/原生外觀
渲染提示will-change-transformwill-change-autowill-change提前告知瀏覽器準備 GPU 加速

不用一次背完。核心心智模型分成兩組:「輸入端」的細節——游標形狀(cursor-*)、事件穿透(pointer-events-*)、能否選字(select-*)、能否拖大(resize-*),這些調的是滑鼠與鍵盤的手感;「捲動與觸控」的行為——平滑滾動(scroll-smooth)、滾動吸附(snap-*)、手勢控制(touch-*),這些調的是滑動的手感。再加上 v4 大幅補強的表單質感三兄弟(accent-*caret-*scheme-*)。抓住這幾條主線,其餘都是刻度與命名細節。

運作原理:三個最需要理解的機制

大部分互動 utility 看名字就懂(cursor-pointer 就是手形游標),但有三個機制值得先講透,因為它們最常被誤用:

  • pointer-events 是「事件穿透」開關,不是「禁用」開關pointer-events-none 讓元素對所有指標事件(click、hover、focus)完全透明,事件會直接穿到它底下的元素;pointer-events-auto 則恢復接收。最經典的用途是「裝飾性遮罩層不擋點擊」——一個蓋在按鈕上的漸層或圖示,加上 pointer-events-none 就不會攔截使用者對按鈕的點擊。但要記住:一旦 none,這個元素連自己的 hover 都收不到了(詳見常見錯誤)。
  • scroll-snap 是「容器 + 子項目」的雙層配合。滾動吸附不是單一 class 能完成的,它需要容器宣告吸附「軸向與強度」(snap-x/snap-y 決定水平或垂直、snap-mandatory/snap-proximity 決定強制或接近才吸),再由每個子項目宣告要吸附到自己的哪個邊(snap-start/snap-center/snap-end)。少了任何一層,吸附都不會發生。這是做輪播、全螢幕分頁最核心的機制。
  • field-sizing-content(v4)把「自動長高」變成純 CSS。過去要做「輸入框跟著文字自動長高」,得寫 JavaScript 監聽 input 事件、讀 scrollHeight、再設 style.height。v4 的 field-sizing-content 讓瀏覽器原生接管這件事——文字多就長、少就縮,一個 class 搞定,再搭配 min-h-*/max-h-* 設下限與上限即可。

至於 user-selectresize 兩組則單純許多。select-* 控制文字能否被反白選取:select-none 常用在按鈕、拖曳把手、導覽列這類「文字只是標籤、不該被選取」的元素,避免使用者連點時整片反白的尷尬;select-all 則相反,點一下就選取整段,適合要一鍵複製的金鑰、序號欄位。resize-* 控制的是 textarea 右下角那個手動拖曳把手的方向:resize(雙向)、resize-x(僅水平)、resize-y(僅垂直,最常用)、resize-none(關閉)。實務上最常見的組合是給 textarea 加 resize-y 只允許垂直拉伸,避免使用者把它拖成破壞版面的怪異寬度。

關鍵術語:scroll-snap 家族一次搞懂

滾動吸附是本篇技術含量最高的部分,先把術語對照清楚,實作時就不會混淆:

  • scroll-snap-type(容器):snap-x(水平吸附)、snap-y(垂直吸附)、snap-both(雙軸)、snap-none(不吸附)。強度另外用 snap-mandatory(強制,一定停在吸附點,輪播必用)或 snap-proximity(接近才吸,較自然)。
  • scroll-snap-align(子項目):snap-start(吸附到子項目起點,輪播最常用)、snap-center(吸附到中央)、snap-end(吸附到終點)、snap-align-none(取消對齊)。
  • scroll-snap-stop(子項目):snap-always(必須停在每個吸附點,不能一次滑過多張)、snap-normal(可快速滑過,預設)。
  • 搭配的間距:scroll-p-*(容器的 scroll-padding,吸附時保留的內距)、scroll-m-*(子項目的 scroll-margin)。而 scroll-mt-* 更常單獨用來解決「固定導覽列遮蓋錨點」的老問題——搭配 scroll-smooth 時尤其重要。

實作範例

理論看完,我們用三個高頻範例把「滾動吸附輪播、自訂 accent 表單、自動長高 textarea」串起來。以下都是完整的 HTML + Tailwind,可直接貼進頁面觀察效果。

範例一:scroll-snap 滾動吸附輪播

輪播是 scroll-snap 最經典的應用。過去要靠 JavaScript 套件,現在純 CSS 就能做出「一滑就精準停在下一張卡片」的效果。關鍵是容器宣告吸附、每張卡片宣告吸附邊:

<!-- 容器:snap-x 水平吸附 + snap-mandatory 強制停點 + overflow-x-auto 可捲動 -->
<div class="flex gap-4 overflow-x-auto snap-x snap-mandatory scroll-px-4
            px-4 pb-4 touch-pan-x">

  <!-- 每張卡片:snap-start 吸附到起始邊、flex-none 不被壓縮 -->
  <div class="snap-start flex-none w-[85%] sm:w-80
              bg-white rounded-2xl shadow-md overflow-hidden">
    <div class="h-40 bg-cyan-500"></div>
    <div class="p-4">
      <h3 class="font-semibold text-gray-900">卡片一</h3>
      <p class="mt-1 text-sm text-gray-600">滑動時會精準吸附到我的左緣</p>
    </div>
  </div>

  <div class="snap-start flex-none w-[85%] sm:w-80
              bg-white rounded-2xl shadow-md overflow-hidden">
    <div class="h-40 bg-teal-500"></div>
    <div class="p-4">
      <h3 class="font-semibold text-gray-900">卡片二</h3>
      <p class="mt-1 text-sm text-gray-600">snap-mandatory 讓每次滑動都停在定點</p>
    </div>
  </div>

  <div class="snap-start flex-none w-[85%] sm:w-80
              bg-white rounded-2xl shadow-md overflow-hidden">
    <div class="h-40 bg-cyan-600"></div>
    <div class="p-4">
      <h3 class="font-semibold text-gray-900">卡片三</h3>
      <p class="mt-1 text-sm text-gray-600">純 CSS,不需要任何 JavaScript</p>
    </div>
  </div>
</div>

這個範例把 scroll-snap 的雙層配合展示得很完整。容器層overflow-x-auto 開啟水平捲動、snap-x snap-mandatory 宣告「水平方向、強制吸附」——snap-mandatory 是輪播感的來源,它保證每次放手後畫面一定停在某張卡片的邊界,不會停在兩張中間。scroll-px-4 給容器四周留 scroll-padding,讓吸附後卡片左緣與容器邊有一點呼吸空間。touch-pan-x 則是移動端最佳化,明確告訴瀏覽器「這裡只做水平滑動」,避免與頁面垂直捲動打架。子項目層每張卡片都加 snap-start(吸附到自己的左緣)與 flex-none(不被 flex 壓縮,維持固定寬度);寬度用 w-[85%] sm:w-80 讓手機露出一點下一張卡片邊緣暗示「可以滑」、桌機則固定 320px。試著把 snap-mandatory 拿掉,你會發現卡片可以停在任意位置——那就是「有無吸附」的差別。

範例二:自訂 accent 強調色表單

原生的 checkbox、radio、range 滑桿在各瀏覽器長得不一樣、顏色也醜。v4 之前要客製得寫一堆前綴 hack,現在 accent-* 一個 class 就統一了強調色,再搭配 caret-* 客製輸入游標顏色、appearance-none 客製下拉選單:

<!-- accent-cyan-600 統一 checkbox/radio/range 的強調色 -->
<form class="max-w-sm space-y-5 accent-cyan-600">

  <!-- 勾選框:打勾的底色會變成 cyan-600 -->
  <label class="flex items-center gap-2 select-none cursor-pointer">
    <input type="checkbox" class="size-4" checked>
    <span class="text-gray-700">訂閱電子報</span>
  </label>

  <!-- 單選框:選中的圓點會變成 cyan-600 -->
  <div class="space-y-2">
    <label class="flex items-center gap-2 select-none cursor-pointer">
      <input type="radio" name="plan" class="size-4" checked>
      <span class="text-gray-700">月繳方案</span>
    </label>
    <label class="flex items-center gap-2 select-none cursor-pointer">
      <input type="radio" name="plan" class="size-4">
      <span class="text-gray-700">年繳方案</span>
    </label>
  </div>

  <!-- 滑桿:軌道與滑塊都會套用 cyan 強調色 -->
  <input type="range" class="w-full cursor-pointer">

  <!-- 文字輸入:caret-cyan-500 讓閃爍的輸入游標變 cyan 色 -->
  <input type="text" placeholder="輸入時游標是 cyan 色的"
         class="w-full rounded-lg border border-gray-300 px-4 py-2.5
                caret-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500">

  <!-- 自訂下拉選單:appearance-none 移除原生箭頭,自己畫一個 -->
  <div class="relative">
    <select class="w-full appearance-none rounded-lg border border-gray-300
                   bg-white px-4 py-2.5 pr-10 text-gray-900 cursor-pointer
                   focus:outline-none focus:ring-2 focus:ring-cyan-500">
      <option>選項一</option>
      <option>選項二</option>
    </select>
    <!-- 自訂箭頭:pointer-events-none 讓點擊穿透到 select -->
    <svg class="pointer-events-none absolute right-3 top-1/2 size-4 -translate-y-1/2 text-gray-500"
         fill="none" stroke="currentColor" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
    </svg>
  </div>
</form>

這個範例把「表單質感三兄弟」一次用齊。accent-cyan-600 加在 <form>,底下所有 checkbox、radio、range 都自動繼承這個強調色——這是 v4 之前最麻煩、現在最省事的改進,一行搞定原本要寫幾十行的跨瀏覽器 hack。caret-cyan-500 是很細節的品牌點綴,讓文字輸入時那條閃爍的直線變成品牌色,使用者不一定說得出哪裡不同,但整體質感就是更一致。appearance-none 移除 <select> 的原生下拉箭頭,讓我們自己用 SVG 畫一個更好看的;而那個自訂箭頭加上 pointer-events-none 是點睛之筆——它讓箭頭「看得到、擋不住」,使用者點在箭頭上時,點擊會穿透到底下的 <select> 正常展開選單,而不會被箭頭 SVG 攔截。另外每個 <label> 都加了 select-none cursor-pointer,防止使用者連點時意外選取到文字、並用手形游標暗示可點擊。

範例三:v4 field-sizing 自動長高 textarea

留言框、聊天輸入框最理想的體驗是「打多少長多高」。v4 的 field-sizing-content 讓這件事不再需要 JavaScript:

<!-- field-sizing-content:textarea 隨內容自動長高;純 CSS,無需 JS -->
<div class="max-w-md">
  <label class="mb-1 block text-sm font-medium text-gray-700">留言</label>
  <textarea
    class="field-sizing-content w-full resize-none
           min-h-[3rem] max-h-48
           rounded-lg border border-gray-300 px-4 py-2.5 text-gray-900
           focus:outline-none focus:ring-2 focus:ring-cyan-500"
    placeholder="輸入文字,方框會自動跟著長高……"></textarea>
  <p class="mt-1 text-xs text-gray-500">超過上限高度後,內部會出現捲動條</p>
</div>

這個範例雖短,卻藏了三個要一起用才完整的細節。field-sizing-content 是主角——它讓 textarea 的高度由內容決定,使用者每換一行,方框就長高一行,刪掉就縮回去,全程不需要一行 JavaScript。min-h-[3rem]max-h-48 是它的左右護法:min-h 設定「空的時候」的初始高度(不設的話空 textarea 會塌成一行很難點),max-h 設定「長到多高就封頂」,超過上限後內部自動出現捲動條而不是無限撐長頁面。resize-none 是關鍵搭配:因為高度已經交給 field-sizing-content 自動管理,就該關掉右下角那個手動拖曳把手,避免「自動長高」與「手動拖曳」兩套機制打架、產生尷尬的操作衝突。這三者是 field-sizing 的標準組合,記成一套即可。要提醒的是,field-sizing 是較新的 CSS 特性,在不支援的舊瀏覽器上會優雅回退成固定高度(以 min-h 為準),不會壞版,但正式上線前仍建議確認目標客群的瀏覽器版本。

常見錯誤與最佳實踐

四個最容易踩的互動性坑

坑一:pointer-events-none 讓元素連自己的 hover 都失效。 想「只禁用點擊、保留 hover 變色」,結果 hover 也一起沒了。原因:pointer-events-none 讓元素對所有指標事件(含 hover)完全透明,不是只擋 click。根治法:若某個子元素仍需互動,在容器加 none、對該子元素單獨加 pointer-events-auto 把接收能力加回來。

<!-- ❌ 期望只擋 click 卻連 hover 都沒了 -->
<div class="pointer-events-none hover:bg-gray-100 cursor-pointer">hover 不會生效</div>

<!-- ✅ 遮罩不擋點擊,但底下按鈕正常互動 -->
<div class="relative">
  <div class="pointer-events-none absolute inset-0 bg-cyan-500/10"></div>
  <button class="relative z-10 px-4 py-2 bg-cyan-600 text-white rounded-lg">可正常點擊</button>
</div>

坑二:scroll-snap 只加了容器或只加了子項目,吸附完全沒作用。 加了 snap-x 卻不吸,或給每張卡片加了 snap-start 也沒反應。原因:滾動吸附是「容器 + 子項目」雙層機制,缺一不可——容器要宣告 snap-x/snap-y(通常再加 snap-mandatory),每個子項目要宣告 snap-start/center/end。根治法:兩層都要加。

<!-- ❌ 只有容器,子項目沒宣告吸附邊 -->
<div class="flex overflow-x-auto snap-x snap-mandatory">
  <div class="flex-none w-80">卡片(不會吸附)</div>
</div>

<!-- ✅ 容器 + 子項目雙層齊全 -->
<div class="flex overflow-x-auto snap-x snap-mandatory">
  <div class="snap-start flex-none w-80">卡片(正確吸附)</div>
</div>

坑三:開了 scroll-smooth,錨點跳轉後標題被固定導覽列蓋住。 平滑滾動很順,但滾到目標區塊時,標題卡在固定 navbar 底下看不到。原因:平滑滾動本身不留偏移量,固定導覽列會遮住錨點頂部。根治法:給錨點目標區塊加 scroll-mt-*,數值對齊 navbar 高度(如 navbar h-16 就配 scroll-mt-16)。

<html class="scroll-smooth">
<!-- ✅ scroll-mt-16 為固定 navbar 預留偏移,標題不被遮 -->
<section id="pricing" class="scroll-mt-16 py-20">定價方案</section>

坑四:對每個動畫元素都掛 will-change,反而讓頁面更卡。 以為 will-change-transform 到處加就會更快,結果記憶體暴增、更卡頓。原因:will-change 會讓瀏覽器提前為該元素配置獨立圖層(佔 GPU 記憶體),濫用適得其反。根治法:只在「即將發生」的動畫上用,理想是 hover 時才提示(hover:will-change-transform),動畫結束就該回到 auto;絕不對靜態元素或整棵樹濫加。

<!-- ❌ 靜態元素濫加 will-change,浪費 GPU 記憶體 -->
<p class="will-change-transform">靜態文字</p>

<!-- ✅ 只在 hover 即將動畫時提示 -->
<div class="transition-transform hover:scale-105 hover:will-change-transform">卡片</div>

互動性最佳實踐清單

  • 可點擊的非按鈕元素加 cursor-pointer:<div><li> 等綁了點擊事件時,補上手形游標給出視覺暗示。
  • 禁用狀態同時給 cursor-not-allowed 與視覺弱化:配 opacity-50,必要時加 pointer-events-none 徹底擋操作。
  • 可拖曳元素配 cursor-grab active:cursor-grabbing + select-none:握手游標暗示可拖、select-none 防止拖曳時誤選文字。
  • 移動端按鈕加 touch-manipulation:對應 touch-action: manipulation,消除舊瀏覽器的雙擊縮放延遲、確保跨瀏覽器一致。
  • 滾動吸附務必雙層齊全:容器 snap-x snap-mandatory + 子項目 snap-start,並用 scroll-px-* 給吸附留白。
  • 暗色模式別忘了 scheme-*:對支援暗色的區塊加 scheme-light-darkscheme-dark,讓原生捲軸、下拉選單、日期選擇器的明暗跟著切換,避免暗底配亮捲軸的違和感(此為 v4 新增,v3 需寫原生 color-scheme 或搭配 <meta>)。
  • 自動長高輸入框用 field-sizing-content + resize-none:v4 純 CSS 解法,搭配 min-h-*/max-h-* 設下限與上限,並關掉手動把手避免衝突。
  • will-change 用完就收:只在即將動畫時提示,優先用 hover:will-change-*,別對靜態元素或整棵 DOM 樹濫加。

小結

這是 TailwindCSS 完整教學 系列的第二十三篇。上一篇 《表格與清單》 讓我們把內容排得整齊、讀得順暢;而這一篇我們把焦點從「畫面長什麼樣」轉向「人與畫面怎麼互動」——用互動性工具,把介面從「能操作」打磨到「好操作」。回顧幾個重點:

  • 輸入端細節:cursor-* 給游標形狀暗示、pointer-events-* 是「事件穿透」開關(none 連 hover 都會透)、select-* 控制能否選字、resize-* 控制 textarea 手動把手。
  • 滾動體驗:scroll-smooth 平滑滾動、scroll-mt-* 修正固定 navbar 遮蓋、snap-* 是「容器 + 子項目」雙層機制,做輪播與全螢幕分頁的靈魂。
  • 觸控最佳化:touch-manipulation 消除雙擊延遲、touch-pan-x/touch-pan-y 精準控制手勢方向。
  • v4 表單質感:field-sizing-content 純 CSS 自動長高、accent-* 統一表單強調色、caret-* 客製輸入游標色、scheme-* 宣告配色影響原生 UI 明暗、appearance-none 移除原生外觀。
  • 效能提示:will-change-* 只在即將動畫時用,濫加反而更卡。

到這裡,TW-4「視覺樣式」這個大章節(色彩、邊框、陰影、變形、過渡、表格清單、互動性)就完整收尾了——你已經掌握了讓單一畫面「好看、好讀、好操作」的整套工具。接下來我們要進入全新的 TW-5「響應式與狀態」章節,面對真實世界最核心的挑戰:同一份程式碼,如何在手機、平板、桌機上都呈現最佳樣貌。下一篇 《響應式設計》 會帶你認識 sm:md:lg: 這組斷點前綴,以及 Tailwind「行動優先(mobile-first)」的設計哲學,從「打磨單一畫面」進化到「一份程式碼適配所有螢幕」。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →