Apple 連續曲率與同心圓角:CSS、SwiftUI、Tailwind 元件實作
最近整理元件樣式時,我開始研究一個看似很小的問題:網頁的圓角能不能再好看一點?
我原本以為只要把 border-radius 從 16px 調成 24px 或 32px,卡片就會更接近 Apple 介面的柔和感。實際比較後卻發現,有些角落只是變大,轉折仍顯得偏緊;卡片裡再放一層圖片或面板時,內外間距還會在角落忽寬忽窄。
繼續拆解才發現,我遇到的不是單一半徑數值問題,而是兩件事混在一起:連續曲率決定角落如何從直線過渡到曲線;同心圓角決定巢狀容器的內外曲線能不能共享同一個中心。
我遇到的問題不是把圓角調大就能解決
以一張外層有底色、內層放圖片的卡片為例,我一開始很自然地讓兩層都使用相同的 rounded-3xl。單獨看每一層都沒有問題,疊在一起後卻會出現三種不協調:
- 普通圓弧在大半徑下仍可能顯得收得太快,沒有 Apple 連續曲率那種飽滿感。
- 父子元素使用相同半徑時,內層被 padding 推進去,兩條曲線的中心也跟著錯開。
- Tailwind 的
rounded-*只控制半徑,無法知道父層 padding,也不會自動改變曲線種類。
如果卡片外框是 32px 圓角、內層圖片也寫成 32px,問題不在兩個數值是否一致,而在它們之間少了幾何關係。Web 元件要接近 Apple 的形狀語言,需要同時管理「曲線種類」與「半徑關係」。
先分清楚三種常被叫成 Apple 圓角的概念
一般 border-radius 是四分之一圓或橢圓
標準 border-radius 會在盒子角落放入一段四分之一橢圓。它與水平、垂直邊緣的方向能順暢接上,但直線曲率是 0,進入圓弧後曲率立即變成固定值,曲率本身不是連續變化。
這不代表普通圓角不好。小按鈕、標籤與資訊密集的介面用它通常已經足夠;只有在半徑較大、元件巢狀,或設計稿刻意追求柔和外輪廓時,差異才更容易被看見。
連續曲率讓轉彎提早發生
Apple 在 SwiftUI 將 .continuous 定義為 continuous curvature rounded rect corners,並把 .circular 定義為四分之一圓角。連續曲率不會等到接近角落時才突然彎曲,而是更早從直邊進入曲線,形成視覺上較飽滿的角落。
Web 平台的新選項是:
.card { border-radius: 2rem; corner-shape: squircle;}corner-shape 會修改 border-radius 所建立的角落形狀;如果半徑是 0,單獨設定它不會產生效果。CSS 的 squircle 對應 superellipse(2),背景、邊框、outline、陰影與 overflow 都會跟著新形狀繪製。
注意:
corner-shape: squircle是適合 Web 的原生近似,不等於「複製 Apple 的曲線公式」。Apple 公開文件描述了.continuous的行為,但沒有承諾它等同 CSS 的某個superellipse()參數。
同心圓角是內外曲線共享中心
假設外層圓角半徑為 R,內層距離外框為 P,要讓兩個圓角中心對齊,基本關係是:
內層半徑 = 外層半徑 - 內縮距離r = R - P外層半徑 32px、四周 padding 8px 時,內層應從 24px 開始,而不是繼續使用 32px:
外層曲線中心:32px內層曲線中心:8px + 24px = 32pxApple 在 WWDC25 對同心版面給出的說明也是由父層半徑扣掉 padding。SwiftUI 的 ConcentricRectangle 會依容器形狀與位置自動解析半徑;當內層離容器角落太遠時,算出的半徑可能變成 0,角落便會回到直角。
這次研究後,我會採用的 Web 寫法
我最後不會用 SVG 重畫所有卡片,也不會全站強制依賴仍未完整支援的 corner-shape。對一般響應式元件,較容易維護的組合是:
- 先用標準
border-radius保證所有瀏覽器都有正常圓角。 - 以 CSS 變數保存外層半徑與內縮距離,讓內層半徑自動計算。
- 在
@supports中加入corner-shape: squircle,把連續曲率當成漸進增強。 - 圓形 avatar 與膠囊按鈕保留
round,不要一起改成 squircle。
元件結構不需要為新式圓角增加大量包裝,只要讓外層與需要同心的內層有清楚的對應:
<article class="concentric-card"> <div class="concentric-card__surface"> <img src="cover.webp" alt="文章封面"> <div class="concentric-card__content"> <h2>連續曲率卡片</h2> <p>內外曲線使用同一組半徑與間距規則。</p> </div> </div></article>.concentric-card { --corner-outer: 32px; --corner-gap: 8px; --corner-inner: max(0px, calc(var(--corner-outer) - var(--corner-gap)));
padding: var(--corner-gap); border-radius: var(--corner-outer); background: #dbeafe; overflow: clip;}
.concentric-card__surface { border-radius: var(--corner-inner); background: #ffffff; overflow: hidden;}
@supports (corner-shape: squircle) { .concentric-card, .concentric-card__surface { corner-shape: squircle; }}
.concentric-card img { display: block; width: 100%; aspect-ratio: 16 / 9; object-fit: cover;}
.concentric-card__content { padding: 20px;}這段就是我會放進正式元件的基礎版本。它有三個刻意保留的細節:
max(0px, ...)避免 padding 大於外層半徑後得到負值。@supports只把連續曲率當成漸進增強;不支援時仍有正常的border-radius。overflow負責裁切圖片,不能只畫出外框卻讓內容穿過角落。
如果元件有 border,公式中的內縮距離要使用「外框最外側到子元素邊緣」的總距離,也就是 border 加上 padding。若四邊 padding 不同,水平與垂直半徑也要分開算;實務上需要巢狀同心效果的元件,使用相等 padding 會比較容易維護。
CSS 的 squircle 套在兩個獨立盒子上,能讓中心與形狀語言一致,但它不是外層曲線的精密幾何 offset。設計稿要求像素級一致時,才需要由設計工具輸出同一組 inset path,或使用 SVG path;一般響應式卡片先採用這個可縮放的原生方案,成本與效果會比較平衡。
SwiftUI 可以直接選連續曲率與同心形狀
只需要單一連續曲率圓角時,使用 RoundedRectangle 的 .continuous style:
RoundedRectangle(cornerRadius: 32, style: .continuous) .fill(.blue) .frame(height: 180)巢狀元件可以先宣告容器形狀,再讓 ConcentricRectangle 依容器計算:
struct ConcentricCard: View { private let outerShape = RoundedRectangle( cornerRadius: 32, style: .continuous )
var body: some View { ZStack { outerShape.fill(.blue.opacity(0.16))
ConcentricRectangle() .fill(.background) .padding(8) } .containerShape(outerShape) .frame(height: 180) }}這比把內層半徑硬編碼成 24 更能適應不同容器與裝置邊緣。若內層即使離角落較遠也必須保留圓角,可使用 .concentric(minimum:) 設定最低半徑;那是產品視覺規則,不再是純粹的幾何同心。
Tailwind 單一元件怎麼寫
Tailwind 的 rounded-* 工具只負責 border-radius。目前若要使用 corner-shape,最短方式是任意 CSS 屬性:
<article class="rounded-[32px] p-2 bg-blue-100 overflow-clip [corner-shape:squircle]"> <div class="rounded-[24px] bg-white overflow-hidden [corner-shape:squircle]" > <!-- content --> </div></article>這段可以呈現效果,但 32px、8px、24px 被拆成三個值,修改時很容易只改到其中兩個。較可靠的寫法是讓 CSS 變數保留數學關係,再由 Tailwind 處理其他樣式:
<article class="[--corner-outer:32px] [--corner-gap:8px] rounded-(--corner-outer) p-(--corner-gap) bg-blue-100 overflow-clip [corner-shape:squircle]"> <div class="rounded-[max(0px,calc(var(--corner-outer)_-_var(--corner-gap)))] bg-white overflow-hidden [corner-shape:squircle]" > <!-- content --> </div></article>上面的 rounded-(--corner-outer) 與 p-(--corner-gap) 是 Tailwind CSS v4 語法。Tailwind CSS v3 可以改用 rounded-[var(--corner-outer)] 與 p-[var(--corner-gap)];任意值中的底線會轉成 CSS 需要的空白。
Tailwind 能不能設定一次,讓所有圓角都變成連續曲率?
可以,但建議限定在一個 UI 區域,不要直接對整站的 * 套用。把 continuous-corners 放在應用程式根節點一次:
<main class="continuous-corners"> <!-- 此區域內的 rounded-* 元件都會採用 squircle --></main>@supports (corner-shape: squircle) { .continuous-corners, .continuous-corners :where(*), .continuous-corners :where(*)::before, .continuous-corners :where(*)::after { corner-shape: squircle; }
/* 圓形與膠囊是另一種形狀,不應被全域 squircle 改掉 */ .continuous-corners :where(.rounded-full, [data-corner-shape="round"]) { corner-shape: round; }}corner-shape 本身不會產生半徑,所以這條規則雖然會命中所有後代,實際上只有已設定 border-radius 的元素會改變外觀。圓形 avatar、狀態點與膠囊按鈕要保留 round;Apple 的設計系統也把 fixed shape、capsule 與 concentric shape 當成不同類型,而不是把所有東西都改成同一種 squircle。
如果是 Tailwind CSS v4,也可以把單一工具註冊進樣式表:
@utility corner-continuous { corner-shape: squircle;}
@utility corner-circular { corner-shape: round;}之後可使用 corner-continuous、hover:corner-continuous 等 class。Tailwind CSS v3 則直接在 @layer utilities 定義相同 class 即可:
@layer utilities { .corner-continuous { corner-shape: squircle; }
.corner-circular { corner-shape: round; }}需要特別區分的是:全域規則只能一次統一曲線種類,不能自動讓父子元素同心。 Tailwind 無法只看一個 rounded-* class,就知道它與父層之間實際隔了多少 padding、border 或 gap。同心關係仍要用設計 token、CSS 變數公式,或封裝成 concentric-card 這類元件 class。
與其用 [class*="rounded-"] 猜測 Tailwind class 名稱,我會選擇上面的 scope selector。字串選擇器容易誤中第三方元件,也可能漏掉動態 class 或自行命名的圓角元件。
瀏覽器尚未完整支援時怎麼降級
截至 2026 年 8 月,MDN 仍將 corner-shape 標示為 Limited availability 與 experimental,不應把它當成所有瀏覽器都具備的必要條件。實作時可以分成兩個等級:
| 需求 | 做法 |
|---|---|
| 允許外觀略有差異 | 保留 border-radius,以 @supports 加上 corner-shape: squircle |
| 每個瀏覽器都要呈現相同曲線 | 使用 SVG、mask 或設計工具輸出的 path,並逐一處理裁切、邊框與陰影 |
| Apple 原生 App | 優先使用 SwiftUI 的 .continuous、ConcentricRectangle 與 containerShape(_:) |
第一種最適合一般網站:舊瀏覽器看到普通圓角,支援的新瀏覽器得到連續曲率,排版與操作都不受影響。若想檢查當前瀏覽器,可在 DevTools 執行:
CSS.supports("corner-shape", "squircle");結果為 true 只代表瀏覽器理解這個宣告。正式驗收仍要檢查圖片裁切、focus outline、陰影、動畫,以及 rounded-full 的例外是否保留。
如果你正在處理更自由的凹角或曲線,可以搭配純 CSS 實作內凹圓角頁籤理解 corner-shape、CSS mask 與 SVG 的適用邊界;Tailwind 的任意值與元件尺寸管理,也可參考CSS 圖片比例控制:aspect-ratio 與 Padding Hack。
實作時保留這兩條規則
這次研究後,我留下的實作原則是:連續曲率與同心圓角要分開處理。Web 元件先用 corner-shape: squircle 做漸進增強,再用 內層半徑 = 外層半徑 - 內縮距離 管理巢狀關係;Tailwind 可以在根節點加一次 scope class 統一曲線,但父子半徑仍要由元件 token 或公式決定。
若只想先修正現有卡片,優先做這三件事:找出外層半徑、量出 border 加 padding 的總內縮距離、把圓形與膠囊排除在全域 squircle 之外。這比把所有 rounded-lg 換成更大的半徑更接近 Apple 所說的 harmony 與 concentricity。
常見問題
Q: Apple 的連續曲率就等於 CSS squircle 嗎?
A: 不能直接畫上等號。Apple 將 SwiftUI .continuous 描述為連續曲率圓角;CSS squircle 則是規格明確定義的 superellipse(2)。兩者都能避免普通四分之一圓角過於收緊的觀感,但 Apple 並未在公開文件承諾 .continuous 使用相同公式。Web 專案應把 squircle 視為原生、可縮放的近似方案。
Q: 為什麼父層和子層都用 rounded-3xl,圓角卻不同心?
A: 因為相同半徑會讓兩個圓弧中心各自落在盒子邊緣向內相同距離的位置;子層本身又被 padding 推進去,中心自然跟著內移。四周間距相同時,內層半徑應以外層半徑減去 border 與 padding 的總距離,並用 max(0px, ...) 避免負值。
Q: Tailwind 可以只設定一次,就讓所有 rounded class 自動同心嗎?
A: 可以用根節點 scope class 一次統一 corner-shape: squircle,但不能只靠這條規則自動計算所有內外半徑。Tailwind 不知道每一組父子元素的 padding、border 與相對位置;要真正同心,仍需用 CSS 變數、設計 token 或封裝元件保存「外層半徑減內縮距離」的關係。
Q: corner-shape 現在適合放進正式網站嗎?
A: 適合當漸進增強,不適合當版面或可操作性的必要條件。先提供普通 border-radius,再用 @supports (corner-shape: squircle) 覆蓋;不支援的瀏覽器仍會得到可用元件。若品牌規範要求跨瀏覽器曲線完全一致,則需要 SVG 或 mask fallback,並額外驗證邊框、陰影與裁切。
參考資料:
Apple Human Interface Guidelines
Apple WWDC25:Get to know the new design system
Apple Developer:RoundedCornerStyle.continuous
Apple Developer:ConcentricRectangle
回報錯字、失效連結,或告訴我你想看的延伸主題。