diff --git a/README.md b/README.md
index db74c31..40963d8 100644
--- a/README.md
+++ b/README.md
@@ -86,6 +86,8 @@ const twMerge = extendTailwindMerge(squircleMergeConfig, {
});
```
+The conflicts mirror Tailwind's own `rounded` hierarchy: a later all-corners utility cancels earlier side/corner utilities (from either family), a side cancels its two corners, and a narrower utility refines a broader one instead of canceling it — `squircle-md squircle-tl-sm` keeps both. `squircle-amt-*` is independent of radius classes.
+
### Utilities
| Utility | Equivalent | Description |
@@ -106,13 +108,34 @@ const twMerge = extendTailwindMerge(squircleMergeConfig, {
| `squircle-es-*` | `rounded-es-*` | End-start corner (logical) |
| `squircle-ee-*` | `rounded-ee-*` | End-end corner (logical) |
| `squircle-amt-*` | — | Superellipse exponent (default 2) |
+| `squircle-none` | `rounded-none` | Remove rounding |
+| `squircle-full` | `rounded-full` | Fully rounded (pill) |
### What values are accepted?
Values are validated strictly so typos fail at build time instead of producing invalid CSS:
-- **`squircle-*` and its variants** accept the same theme values as `rounded-*` (`sm`, `md`, `lg`, `xl`, `2xl`, `3xl`, `full`, plus anything you add to `@theme`) and arbitrary lengths like `squircle-[16px]`. Non-length arbitraries (`[50%]`, `[foo]`) and paren refs (`squircle-(--my-radius)`) are rejected — use a theme key instead.
-- **`squircle-amt-*`** accepts bare numbers (`squircle-amt-2`), arbitrary numbers (`squircle-amt-[3.5]`), and theme values. Unit-bearing arbitraries (`[1em]`) and paren refs (`(--my-amt)`) are rejected.
+- **`squircle-*` and its variants** accept the same theme values as `rounded-*` (`sm`, `md`, `lg`, `xl`, `2xl`, `3xl`, plus anything you add to `@theme`) and arbitrary lengths like `squircle-[16px]`. Non-length arbitraries (`[50%]`, `[foo]`) and paren refs (`squircle-(--my-radius)`) are rejected — use a theme key instead.
+- **`squircle-none` and `squircle-full`** (and their side/corner variants) are static utilities defined the same way as `rounded-none` and `rounded-full`: `0` and `calc(infinity * 1px)`. Neither needs the visual radius correction — correcting zero or infinity is a no-op — so `squircle-full` is just `rounded-full` plus the superellipse corner shape, and `squircle-none` is identical to `rounded-none`.
+- **`squircle-amt-*`** accepts bare numbers (`squircle-amt-2`), arbitrary numbers (`squircle-amt-[3.5]`), and theme values. Unit-bearing arbitraries (`[1em]`) and paren refs (`(--my-amt)`) are rejected. It only sets the amount — exactly what writing `--squircle-amt` yourself does — and applies no corner shape of its own, so it never reshapes a corner no `squircle-*` utility claimed.
+
+### Mixing squircled and rounded corners
+
+Corner shape is tracked per corner, so the two families compose in both directions on the same element:
+
+```html
+
+
…
+
+
+…
+```
+
+Two things make that work. A `squircle-*` utility only shapes the corners it sets a radius on — the all-corners utility uses the `corner-shape` shorthand, while the side and corner variants use the matching longhands (`corner-top-left-shape` and friends). And because Tailwind's own `rounded-*` utilities only set a radius, they could never take a corner back from a squircle, so this package re-declares them to also reset their corners to `round`. That reset is the initial value, so it changes nothing unless a squircle class is on the same element.
+
+Note that a corner reclaimed by `rounded-*` uses the plain radius, not the corrected one — the correction exists only to make a squircle look the same size as a rounded corner, so a genuinely round corner doesn't want it.
+
+The reset accepts the same values the `squircle-*` utilities do — theme keys and arbitrary lengths like `rounded-tl-[3px]`. Tailwind's own `rounded-*` is looser, so a corner named with a paren ref (`rounded-tl-(--my-radius)`) or a non-length arbitrary still gets its radius from Tailwind but keeps its squircle shape. Use a theme key there, as everywhere else in this package.
### What does `squircle-amt-*` control?
@@ -596,12 +619,24 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
/* ── Squircle utilities ─────────────────────────────────────── */
/* squircle-amt-[n] sets the superellipse amount (default 2) */
-/* squircle-* mirrors rounded-* variants: all, t, r, b, l, s, e, tl, tr, br, bl, ss, se, es, ee */
+/* squircle-* mirrors rounded-* variants: t, r, b, l, s, e, tl, tr, br, bl, ss, se, es, ee */
+/* squircle-*-none and squircle-*-full are static, matching rounded-none and rounded-full */
+/* squircle-amt-[n] only sets the amount — the same thing writing
+ --squircle-amt yourself does. It applies no corner-shape of its own, so it
+ never reshapes corners that no squircle-* utility claimed. */
@utility squircle-amt-* {
--squircle-amt: --value(--squircle-amt-*, number, [number]);
+}
+
+@utility squircle-none {
+ border-radius: 0;
+}
+
+@utility squircle-full {
+ border-radius: calc(infinity * 1px);
@supports (corner-shape: superellipse(2)) {
- corner-shape: superellipse(var(--squircle-amt));
+ corner-shape: superellipse(var(--squircle-amt, 2));
}
}
@@ -616,6 +651,20 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
/* --- Per-side physical variants --- */
+@utility squircle-t-none {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
+
+@utility squircle-t-full {
+ border-top-left-radius: calc(infinity * 1px);
+ border-top-right-radius: calc(infinity * 1px);
+ @supports (corner-shape: superellipse(2)) {
+ corner-top-left-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-right-shape: superellipse(var(--squircle-amt, 2));
+ }
+}
+
@utility squircle-t-* {
border-top-left-radius: --value(--radius-*, [length]);
border-top-right-radius: --value(--radius-*, [length]);
@@ -623,7 +672,22 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
--squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
border-top-left-radius: var(--squircle-r);
border-top-right-radius: var(--squircle-r);
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-left-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-right-shape: superellipse(var(--squircle-amt, 2));
+ }
+}
+
+@utility squircle-r-none {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+@utility squircle-r-full {
+ border-top-right-radius: calc(infinity * 1px);
+ border-bottom-right-radius: calc(infinity * 1px);
+ @supports (corner-shape: superellipse(2)) {
+ corner-top-right-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-right-shape: superellipse(var(--squircle-amt, 2));
}
}
@@ -634,7 +698,22 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
--squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
border-top-right-radius: var(--squircle-r);
border-bottom-right-radius: var(--squircle-r);
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-right-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-right-shape: superellipse(var(--squircle-amt, 2));
+ }
+}
+
+@utility squircle-b-none {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+@utility squircle-b-full {
+ border-bottom-left-radius: calc(infinity * 1px);
+ border-bottom-right-radius: calc(infinity * 1px);
+ @supports (corner-shape: superellipse(2)) {
+ corner-bottom-left-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-right-shape: superellipse(var(--squircle-amt, 2));
}
}
@@ -645,7 +724,22 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
--squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
border-bottom-left-radius: var(--squircle-r);
border-bottom-right-radius: var(--squircle-r);
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-left-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-right-shape: superellipse(var(--squircle-amt, 2));
+ }
+}
+
+@utility squircle-l-none {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+}
+
+@utility squircle-l-full {
+ border-top-left-radius: calc(infinity * 1px);
+ border-bottom-left-radius: calc(infinity * 1px);
+ @supports (corner-shape: superellipse(2)) {
+ corner-top-left-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-left-shape: superellipse(var(--squircle-amt, 2));
}
}
@@ -656,12 +750,27 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
--squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
border-top-left-radius: var(--squircle-r);
border-bottom-left-radius: var(--squircle-r);
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-left-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-left-shape: superellipse(var(--squircle-amt, 2));
}
}
/* --- Per-side logical variants --- */
+@utility squircle-s-none {
+ border-start-start-radius: 0;
+ border-end-start-radius: 0;
+}
+
+@utility squircle-s-full {
+ border-start-start-radius: calc(infinity * 1px);
+ border-end-start-radius: calc(infinity * 1px);
+ @supports (corner-shape: superellipse(2)) {
+ corner-start-start-shape: superellipse(var(--squircle-amt, 2));
+ corner-end-start-shape: superellipse(var(--squircle-amt, 2));
+ }
+}
+
@utility squircle-s-* {
border-start-start-radius: --value(--radius-*, [length]);
border-end-start-radius: --value(--radius-*, [length]);
@@ -669,7 +778,22 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
--squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
border-start-start-radius: var(--squircle-r);
border-end-start-radius: var(--squircle-r);
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-start-start-shape: superellipse(var(--squircle-amt, 2));
+ corner-end-start-shape: superellipse(var(--squircle-amt, 2));
+ }
+}
+
+@utility squircle-e-none {
+ border-start-end-radius: 0;
+ border-end-end-radius: 0;
+}
+
+@utility squircle-e-full {
+ border-start-end-radius: calc(infinity * 1px);
+ border-end-end-radius: calc(infinity * 1px);
+ @supports (corner-shape: superellipse(2)) {
+ corner-start-end-shape: superellipse(var(--squircle-amt, 2));
+ corner-end-end-shape: superellipse(var(--squircle-amt, 2));
}
}
@@ -680,17 +804,40 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
--squircle-r: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
border-start-end-radius: var(--squircle-r);
border-end-end-radius: var(--squircle-r);
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-start-end-shape: superellipse(var(--squircle-amt, 2));
+ corner-end-end-shape: superellipse(var(--squircle-amt, 2));
}
}
/* --- Per-corner physical variants --- */
+@utility squircle-tl-none {
+ border-top-left-radius: 0;
+}
+
+@utility squircle-tl-full {
+ border-top-left-radius: calc(infinity * 1px);
+ @supports (corner-shape: superellipse(2)) {
+ corner-top-left-shape: superellipse(var(--squircle-amt, 2));
+ }
+}
+
@utility squircle-tl-* {
border-top-left-radius: --value(--radius-*, [length]);
@supports (corner-shape: superellipse(2)) {
border-top-left-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-left-shape: superellipse(var(--squircle-amt, 2));
+ }
+}
+
+@utility squircle-tr-none {
+ border-top-right-radius: 0;
+}
+
+@utility squircle-tr-full {
+ border-top-right-radius: calc(infinity * 1px);
+ @supports (corner-shape: superellipse(2)) {
+ corner-top-right-shape: superellipse(var(--squircle-amt, 2));
}
}
@@ -698,7 +845,18 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
border-top-right-radius: --value(--radius-*, [length]);
@supports (corner-shape: superellipse(2)) {
border-top-right-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-right-shape: superellipse(var(--squircle-amt, 2));
+ }
+}
+
+@utility squircle-br-none {
+ border-bottom-right-radius: 0;
+}
+
+@utility squircle-br-full {
+ border-bottom-right-radius: calc(infinity * 1px);
+ @supports (corner-shape: superellipse(2)) {
+ corner-bottom-right-shape: superellipse(var(--squircle-amt, 2));
}
}
@@ -706,7 +864,18 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
border-bottom-right-radius: --value(--radius-*, [length]);
@supports (corner-shape: superellipse(2)) {
border-bottom-right-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-right-shape: superellipse(var(--squircle-amt, 2));
+ }
+}
+
+@utility squircle-bl-none {
+ border-bottom-left-radius: 0;
+}
+
+@utility squircle-bl-full {
+ border-bottom-left-radius: calc(infinity * 1px);
+ @supports (corner-shape: superellipse(2)) {
+ corner-bottom-left-shape: superellipse(var(--squircle-amt, 2));
}
}
@@ -714,17 +883,39 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
border-bottom-left-radius: --value(--radius-*, [length]);
@supports (corner-shape: superellipse(2)) {
border-bottom-left-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-left-shape: superellipse(var(--squircle-amt, 2));
}
}
/* --- Per-corner logical variants --- */
+@utility squircle-ss-none {
+ border-start-start-radius: 0;
+}
+
+@utility squircle-ss-full {
+ border-start-start-radius: calc(infinity * 1px);
+ @supports (corner-shape: superellipse(2)) {
+ corner-start-start-shape: superellipse(var(--squircle-amt, 2));
+ }
+}
+
@utility squircle-ss-* {
border-start-start-radius: --value(--radius-*, [length]);
@supports (corner-shape: superellipse(2)) {
border-start-start-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-start-start-shape: superellipse(var(--squircle-amt, 2));
+ }
+}
+
+@utility squircle-se-none {
+ border-start-end-radius: 0;
+}
+
+@utility squircle-se-full {
+ border-start-end-radius: calc(infinity * 1px);
+ @supports (corner-shape: superellipse(2)) {
+ corner-start-end-shape: superellipse(var(--squircle-amt, 2));
}
}
@@ -732,7 +923,18 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
border-start-end-radius: --value(--radius-*, [length]);
@supports (corner-shape: superellipse(2)) {
border-start-end-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-start-end-shape: superellipse(var(--squircle-amt, 2));
+ }
+}
+
+@utility squircle-es-none {
+ border-end-start-radius: 0;
+}
+
+@utility squircle-es-full {
+ border-end-start-radius: calc(infinity * 1px);
+ @supports (corner-shape: superellipse(2)) {
+ corner-end-start-shape: superellipse(var(--squircle-amt, 2));
}
}
@@ -740,7 +942,18 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
border-end-start-radius: --value(--radius-*, [length]);
@supports (corner-shape: superellipse(2)) {
border-end-start-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-end-start-shape: superellipse(var(--squircle-amt, 2));
+ }
+}
+
+@utility squircle-ee-none {
+ border-end-end-radius: 0;
+}
+
+@utility squircle-ee-full {
+ border-end-end-radius: calc(infinity * 1px);
+ @supports (corner-shape: superellipse(2)) {
+ corner-end-end-shape: superellipse(var(--squircle-amt, 2));
}
}
@@ -748,9 +961,170 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
border-end-end-radius: --value(--radius-*, [length]);
@supports (corner-shape: superellipse(2)) {
border-end-end-radius: calc(--value(--radius-*, [length]) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-end-end-shape: superellipse(var(--squircle-amt, 2));
}
}
+
+/* ── rounded-* corner-shape resets ──────────────────────────── */
+/* Tailwind's rounded-* utilities only set a radius, so on their own they can't
+ take a corner back from a squircle. These re-declarations add the matching
+ corner-shape reset; Tailwind still emits its own rule for the radius. The
+ reset is the initial value, so it does nothing unless a squircle class set a
+ shape on the same element. rounded-*-none needs none: a zero radius has no
+ visible corner to shape. */
+
+@utility rounded-full {
+ corner-shape: round;
+}
+
+@utility rounded-* {
+ border-radius: --value(--radius-*, [length]);
+ corner-shape: round;
+}
+
+@utility rounded-t-full {
+ corner-top-left-shape: round;
+ corner-top-right-shape: round;
+}
+
+@utility rounded-t-* {
+ border-top-left-radius: --value(--radius-*, [length]);
+ border-top-right-radius: --value(--radius-*, [length]);
+ corner-top-left-shape: round;
+ corner-top-right-shape: round;
+}
+
+@utility rounded-r-full {
+ corner-top-right-shape: round;
+ corner-bottom-right-shape: round;
+}
+
+@utility rounded-r-* {
+ border-top-right-radius: --value(--radius-*, [length]);
+ border-bottom-right-radius: --value(--radius-*, [length]);
+ corner-top-right-shape: round;
+ corner-bottom-right-shape: round;
+}
+
+@utility rounded-b-full {
+ corner-bottom-left-shape: round;
+ corner-bottom-right-shape: round;
+}
+
+@utility rounded-b-* {
+ border-bottom-left-radius: --value(--radius-*, [length]);
+ border-bottom-right-radius: --value(--radius-*, [length]);
+ corner-bottom-left-shape: round;
+ corner-bottom-right-shape: round;
+}
+
+@utility rounded-l-full {
+ corner-top-left-shape: round;
+ corner-bottom-left-shape: round;
+}
+
+@utility rounded-l-* {
+ border-top-left-radius: --value(--radius-*, [length]);
+ border-bottom-left-radius: --value(--radius-*, [length]);
+ corner-top-left-shape: round;
+ corner-bottom-left-shape: round;
+}
+
+@utility rounded-s-full {
+ corner-start-start-shape: round;
+ corner-end-start-shape: round;
+}
+
+@utility rounded-s-* {
+ border-start-start-radius: --value(--radius-*, [length]);
+ border-end-start-radius: --value(--radius-*, [length]);
+ corner-start-start-shape: round;
+ corner-end-start-shape: round;
+}
+
+@utility rounded-e-full {
+ corner-start-end-shape: round;
+ corner-end-end-shape: round;
+}
+
+@utility rounded-e-* {
+ border-start-end-radius: --value(--radius-*, [length]);
+ border-end-end-radius: --value(--radius-*, [length]);
+ corner-start-end-shape: round;
+ corner-end-end-shape: round;
+}
+
+@utility rounded-tl-full {
+ corner-top-left-shape: round;
+}
+
+@utility rounded-tl-* {
+ border-top-left-radius: --value(--radius-*, [length]);
+ corner-top-left-shape: round;
+}
+
+@utility rounded-tr-full {
+ corner-top-right-shape: round;
+}
+
+@utility rounded-tr-* {
+ border-top-right-radius: --value(--radius-*, [length]);
+ corner-top-right-shape: round;
+}
+
+@utility rounded-br-full {
+ corner-bottom-right-shape: round;
+}
+
+@utility rounded-br-* {
+ border-bottom-right-radius: --value(--radius-*, [length]);
+ corner-bottom-right-shape: round;
+}
+
+@utility rounded-bl-full {
+ corner-bottom-left-shape: round;
+}
+
+@utility rounded-bl-* {
+ border-bottom-left-radius: --value(--radius-*, [length]);
+ corner-bottom-left-shape: round;
+}
+
+@utility rounded-ss-full {
+ corner-start-start-shape: round;
+}
+
+@utility rounded-ss-* {
+ border-start-start-radius: --value(--radius-*, [length]);
+ corner-start-start-shape: round;
+}
+
+@utility rounded-se-full {
+ corner-start-end-shape: round;
+}
+
+@utility rounded-se-* {
+ border-start-end-radius: --value(--radius-*, [length]);
+ corner-start-end-shape: round;
+}
+
+@utility rounded-es-full {
+ corner-end-start-shape: round;
+}
+
+@utility rounded-es-* {
+ border-end-start-radius: --value(--radius-*, [length]);
+ corner-end-start-shape: round;
+}
+
+@utility rounded-ee-full {
+ corner-end-end-shape: round;
+}
+
+@utility rounded-ee-* {
+ border-end-end-radius: --value(--radius-*, [length]);
+ corner-end-end-shape: round;
+}
```
@@ -807,38 +1181,47 @@ If you'd rather not add a dependency, copy the source directly. Click to expand
import { extendTailwindMerge } from "tailwind-merge";
-const allRoundedGroups = [
- "rounded", "rounded-s", "rounded-e", "rounded-t", "rounded-r",
- "rounded-b", "rounded-l", "rounded-ss", "rounded-se", "rounded-es",
- "rounded-ee", "rounded-tl", "rounded-tr", "rounded-br", "rounded-bl",
-];
-
-export const squircleMergeConfig = { extend: {
- classGroups: {
- squircle: [
- { squircle: [() => true] },
- { "squircle-t": [() => true] },
- { "squircle-r": [() => true] },
- { "squircle-b": [() => true] },
- { "squircle-l": [() => true] },
- { "squircle-s": [() => true] },
- { "squircle-e": [() => true] },
- { "squircle-tl": [() => true] },
- { "squircle-tr": [() => true] },
- { "squircle-br": [() => true] },
- { "squircle-bl": [() => true] },
- { "squircle-ss": [() => true] },
- { "squircle-se": [() => true] },
- { "squircle-es": [() => true] },
- { "squircle-ee": [() => true] },
- ],
- "squircle-amt": [{ "squircle-amt": [() => true] }],
- },
- conflictingClassGroups: {
- squircle: [...allRoundedGroups, "squircle-amt"],
- ...Object.fromEntries(allRoundedGroups.map((g) => [g, ["squircle", "squircle-amt"]])),
+// Mirrors tailwind-merge's own `rounded` hierarchy: a later all-corners
+// utility cancels earlier side/corner utilities, a side cancels its two
+// corners, and a narrower utility never cancels a broader one. Each squircle
+// group also conflicts with its `rounded` counterpart (and vice versa).
+// `squircle-amt-*` is orthogonal: radius classes never cancel it.
+const SIDE_CORNERS = {
+ t: ["tl", "tr"], r: ["tr", "br"], b: ["br", "bl"],
+ l: ["tl", "bl"], s: ["ss", "es"], e: ["se", "ee"],
+};
+const CORNERS = ["tl", "tr", "br", "bl", "ss", "se", "es", "ee"];
+const SIDES = Object.keys(SIDE_CORNERS);
+const ALL_SUFFIXES = ["", ...SIDES, ...CORNERS];
+
+const sq = (suffix) => (suffix ? `squircle-${suffix}` : "squircle");
+const rd = (suffix) => (suffix ? `rounded-${suffix}` : "rounded");
+
+const conflictingClassGroups = {
+ squircle: [...ALL_SUFFIXES.slice(1).map(sq), ...ALL_SUFFIXES.map(rd)],
+ rounded: ALL_SUFFIXES.map(sq),
+};
+for (const side of SIDES) {
+ const corners = SIDE_CORNERS[side];
+ conflictingClassGroups[sq(side)] = [...corners.map(sq), rd(side), ...corners.map(rd)];
+ conflictingClassGroups[rd(side)] = [sq(side), ...corners.map(sq)];
+}
+for (const corner of CORNERS) {
+ conflictingClassGroups[sq(corner)] = [rd(corner)];
+ conflictingClassGroups[rd(corner)] = [sq(corner)];
+}
+
+export const squircleMergeConfig = {
+ extend: {
+ classGroups: {
+ ...Object.fromEntries(
+ ALL_SUFFIXES.map((suffix) => [sq(suffix), [{ [sq(suffix)]: [() => true] }]]),
+ ),
+ "squircle-amt": [{ "squircle-amt": [() => true] }],
+ },
+ conflictingClassGroups,
},
-} };
+};
export const twMerge = extendTailwindMerge(squircleMergeConfig);
```
@@ -918,7 +1301,11 @@ const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerTopLeftShape: {
+ default: null,
+ "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
+ },
+ cornerTopRightShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -932,7 +1319,11 @@ const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerTopRightShape: {
+ default: null,
+ "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
+ },
+ cornerBottomRightShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -946,7 +1337,11 @@ const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerBottomLeftShape: {
+ default: null,
+ "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
+ },
+ cornerBottomRightShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -960,7 +1355,11 @@ const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerTopLeftShape: {
+ default: null,
+ "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
+ },
+ cornerBottomLeftShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -974,7 +1373,11 @@ const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerStartStartShape: {
+ default: null,
+ "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
+ },
+ cornerEndStartShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -988,7 +1391,11 @@ const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerStartEndShape: {
+ default: null,
+ "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
+ },
+ cornerEndEndShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -998,7 +1405,7 @@ const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerTopLeftShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -1008,7 +1415,7 @@ const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerTopRightShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -1018,7 +1425,7 @@ const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerBottomRightShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -1028,7 +1435,7 @@ const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerBottomLeftShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -1038,7 +1445,7 @@ const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerStartStartShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -1048,7 +1455,7 @@ const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerStartEndShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -1058,7 +1465,7 @@ const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerEndStartShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -1068,7 +1475,7 @@ const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerEndEndShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
diff --git a/package/scripts/generate-squircle-css.ts b/package/scripts/generate-squircle-css.ts
index 996e05a..f308009 100644
--- a/package/scripts/generate-squircle-css.ts
+++ b/package/scripts/generate-squircle-css.ts
@@ -1,16 +1,44 @@
import { copyFileSync, mkdirSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
-import { DEFAULT_AMT, SUPPORTS_RULE, VARIANTS, isComment, squircleCssObj } from "../src/variants";
+import {
+ DEFAULT_AMT,
+ NONE_RADIUS,
+ cornerShapeProp,
+ squircleFullCssObj,
+ SUPPORTS_RULE,
+ VARIANTS,
+ isComment,
+ squircleCssObj,
+ type CssLikeObject,
+} from "../src/variants";
const __dirname = dirname(fileURLToPath(import.meta.url));
// Support arbitrary, bare, and theme values in one --value() call.
// https://tailwindcss.com/docs/adding-custom-styles#functional-utilities
const value = "--value(--radius-*, [length])";
+// The reset re-declares the radius purely so `--value()` still decides what
+// matches. Without it the utility would accept any suffix at all, turning a
+// typo like `rounded-tl-garbage` into a real class. It accepts the same values
+// the squircle-* utilities do — theme keys and arbitrary lengths, not paren
+// refs — and the radius it emits is the one core emits for the same class.
+const roundedValue = value;
-function renderUtility(name: string, props: string[]): string {
- const obj = squircleCssObj(props, value);
+/**
+ * Re-declare a `rounded-*` utility so it also resets the corners it owns back
+ * to `round`. Tailwind keeps its own definition and emits it alongside this
+ * one, so this only has to contribute the reset — which is the initial value,
+ * hence inert unless a squircle class set a shape on the same element.
+ */
+function renderRoundedReset(name: string, props: string[], radius?: string): string {
+ const obj: CssLikeObject = {};
+ if (radius) for (const p of props) obj[p] = radius;
+ for (const p of props) obj[cornerShapeProp(p)] = "round";
+ return renderUtility(name, obj);
+}
+
+function renderUtility(name: string, obj: CssLikeObject): string {
const lines: string[] = [`@utility ${name} {`];
for (const [key, val] of Object.entries(obj)) {
@@ -40,13 +68,14 @@ function generateCss(): string {
blocks.push(`\
/* ── Squircle utilities ─────────────────────────────────────── */
/* squircle-amt-[n] sets the superellipse amount (default ${DEFAULT_AMT}) */
-/* squircle-* mirrors rounded-* variants: all, t, r, b, l, s, e, tl, tr, br, bl, ss, se, es, ee */
+/* squircle-* mirrors rounded-* variants: t, r, b, l, s, e, tl, tr, br, bl, ss, se, es, ee */
+/* squircle-*-none and squircle-*-full are static, matching rounded-none and rounded-full */
+/* squircle-amt-[n] only sets the amount — the same thing writing
+ --squircle-amt yourself does. It applies no corner-shape of its own, so it
+ never reshapes corners that no squircle-* utility claimed. */
@utility squircle-amt-* {
--squircle-amt: --value(--squircle-amt-*, number, [number]);
- ${SUPPORTS_RULE} {
- corner-shape: superellipse(var(--squircle-amt));
- }
}`);
for (const [suffix, entry] of Object.entries(VARIANTS)) {
@@ -55,8 +84,33 @@ function generateCss(): string {
continue;
}
- const name = suffix ? `squircle-${suffix}-*` : "squircle-*";
- blocks.push(renderUtility(name, entry));
+ const root = suffix ? `squircle-${suffix}` : "squircle";
+ // Static -none/-full first, then the functional utility — the same shape
+ // Tailwind uses for rounded-none/rounded-full. -none needs no correction.
+ blocks.push(
+ renderUtility(`${root}-none`, Object.fromEntries(entry.map((p) => [p, NONE_RADIUS]))),
+ );
+ blocks.push(renderUtility(`${root}-full`, squircleFullCssObj(entry)));
+ blocks.push(renderUtility(`${root}-*`, squircleCssObj(entry, value)));
+ }
+
+ blocks.push(`\
+/* ── rounded-* corner-shape resets ──────────────────────────── */
+/* Tailwind's rounded-* utilities only set a radius, so on their own they can't
+ take a corner back from a squircle. These re-declarations add the matching
+ corner-shape reset; Tailwind still emits its own rule for the radius. The
+ reset is the initial value, so it does nothing unless a squircle class set a
+ shape on the same element. rounded-*-none needs none: a zero radius has no
+ visible corner to shape. */`);
+
+ for (const [suffix, entry] of Object.entries(VARIANTS)) {
+ if (isComment(entry)) continue;
+
+ const root = suffix ? `rounded-${suffix}` : "rounded";
+ // The -full name is static, so it needs no value matching and can stay
+ // radius-free; the functional one has to keep --value() to stay bounded.
+ blocks.push(renderRoundedReset(`${root}-full`, entry));
+ blocks.push(renderRoundedReset(`${root}-*`, entry, roundedValue));
}
return blocks.join("\n\n") + "\n";
diff --git a/package/scripts/generate-stylex.ts b/package/scripts/generate-stylex.ts
index fdbca5b..029be2c 100644
--- a/package/scripts/generate-stylex.ts
+++ b/package/scripts/generate-stylex.ts
@@ -1,7 +1,13 @@
import { readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
-import { CAMEL_VARIANTS, VARIANTS, isComment, SUPPORTS_RULE } from "../src/variants";
+import {
+ CAMEL_VARIANTS,
+ VARIANTS,
+ cornerShapeProp,
+ isComment,
+ SUPPORTS_RULE,
+} from "../src/variants";
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -30,7 +36,7 @@ function toCamel(kebab: string): string {
/**
* Render one `key: (radius, amt) => ({ ... })` entry for the
* `stylex.create({...})` literal. The body is one CSS-property block per
- * radius prop plus a single `cornerShape` block, both gated by `@supports`.
+ * radius prop plus a matching shape block for each, both gated by `@supports`.
*
* Tweak the indentation, comment style, or fallback expression here — the
* template file just stamps these strings into its create() call.
@@ -49,13 +55,24 @@ function renderVariant(side: string, kebabProps: string[]): string {
})
.join("\n");
+ // One shape block per radius prop, so a variant only reshapes the corners it
+ // actually sets a radius on. `border-radius` maps to the `cornerShape`
+ // shorthand, the narrower props to their per-corner longhands.
+ const shapeBlocks = kebabProps
+ .map((prop) =>
+ [
+ `${indent}${indent}${toCamel(cornerShapeProp(prop))}: {`,
+ `${indent}${indent}${indent}default: null,`,
+ `${indent}${indent}${indent}"${SUPPORTS_RULE}": \`superellipse(\${amt ?? 2})\`,`,
+ `${indent}${indent}},`,
+ ].join("\n"),
+ )
+ .join("\n");
+
return [
`${indent}${side}: (radius: string | number, amt: string | number | undefined) => ({`,
radiusBlocks,
- `${indent}${indent}cornerShape: {`,
- `${indent}${indent}${indent}default: null,`,
- `${indent}${indent}${indent}"${SUPPORTS_RULE}": \`superellipse(\${amt ?? 2})\`,`,
- `${indent}${indent}},`,
+ shapeBlocks,
`${indent}}),`,
].join("\n");
}
diff --git a/package/src/__snapshots__/squircle-css.test.ts.snap b/package/src/__snapshots__/squircle-css.test.ts.snap
index 5809a77..2b45d26 100644
--- a/package/src/__snapshots__/squircle-css.test.ts.snap
+++ b/package/src/__snapshots__/squircle-css.test.ts.snap
@@ -8,7 +8,8 @@ exports[`squircle.css utilities > squircle-b-md snapshot 1`] = `
--squircle-r: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
border-bottom-left-radius: var(--squircle-r);
border-bottom-right-radius: var(--squircle-r);
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-left-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-right-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -18,7 +19,7 @@ exports[`squircle.css utilities > squircle-bl-md snapshot 1`] = `
border-bottom-left-radius: var(--radius-md);
@supports (corner-shape: superellipse(2)) {
border-bottom-left-radius: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-left-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -28,7 +29,7 @@ exports[`squircle.css utilities > squircle-br-md snapshot 1`] = `
border-bottom-right-radius: var(--radius-md);
@supports (corner-shape: superellipse(2)) {
border-bottom-right-radius: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-right-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -41,7 +42,8 @@ exports[`squircle.css utilities > squircle-e-md snapshot 1`] = `
--squircle-r: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
border-start-end-radius: var(--squircle-r);
border-end-end-radius: var(--squircle-r);
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-start-end-shape: superellipse(var(--squircle-amt, 2));
+ corner-end-end-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -51,7 +53,7 @@ exports[`squircle.css utilities > squircle-ee-md snapshot 1`] = `
border-end-end-radius: var(--radius-md);
@supports (corner-shape: superellipse(2)) {
border-end-end-radius: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-end-end-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -61,7 +63,7 @@ exports[`squircle.css utilities > squircle-es-md snapshot 1`] = `
border-end-start-radius: var(--radius-md);
@supports (corner-shape: superellipse(2)) {
border-end-start-radius: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-end-start-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -74,7 +76,8 @@ exports[`squircle.css utilities > squircle-l-md snapshot 1`] = `
--squircle-r: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
border-top-left-radius: var(--squircle-r);
border-bottom-left-radius: var(--squircle-r);
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-left-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-left-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -98,7 +101,8 @@ exports[`squircle.css utilities > squircle-r-md snapshot 1`] = `
--squircle-r: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
border-top-right-radius: var(--squircle-r);
border-bottom-right-radius: var(--squircle-r);
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-right-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-right-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -111,7 +115,8 @@ exports[`squircle.css utilities > squircle-s-md snapshot 1`] = `
--squircle-r: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
border-start-start-radius: var(--squircle-r);
border-end-start-radius: var(--squircle-r);
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-start-start-shape: superellipse(var(--squircle-amt, 2));
+ corner-end-start-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -121,7 +126,7 @@ exports[`squircle.css utilities > squircle-se-md snapshot 1`] = `
border-start-end-radius: var(--radius-md);
@supports (corner-shape: superellipse(2)) {
border-start-end-radius: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-start-end-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -131,7 +136,7 @@ exports[`squircle.css utilities > squircle-ss-md snapshot 1`] = `
border-start-start-radius: var(--radius-md);
@supports (corner-shape: superellipse(2)) {
border-start-start-radius: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-start-start-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -144,7 +149,8 @@ exports[`squircle.css utilities > squircle-t-md snapshot 1`] = `
--squircle-r: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
border-top-left-radius: var(--squircle-r);
border-top-right-radius: var(--squircle-r);
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-left-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-right-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -154,7 +160,7 @@ exports[`squircle.css utilities > squircle-tl-md snapshot 1`] = `
border-top-left-radius: var(--radius-md);
@supports (corner-shape: superellipse(2)) {
border-top-left-radius: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-left-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -164,7 +170,7 @@ exports[`squircle.css utilities > squircle-tr-md snapshot 1`] = `
border-top-right-radius: var(--radius-md);
@supports (corner-shape: superellipse(2)) {
border-top-right-radius: calc(var(--radius-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-right-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
diff --git a/package/src/__snapshots__/tailwind.test.ts.snap b/package/src/__snapshots__/tailwind.test.ts.snap
index 9090b84..92994a5 100644
--- a/package/src/__snapshots__/tailwind.test.ts.snap
+++ b/package/src/__snapshots__/tailwind.test.ts.snap
@@ -8,7 +8,8 @@ exports[`plugin.ts utilities > squircle-b-md snapshot 1`] = `
--squircle-r: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
border-bottom-left-radius: var(--squircle-r);
border-bottom-right-radius: var(--squircle-r);
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-left-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-right-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -18,7 +19,7 @@ exports[`plugin.ts utilities > squircle-bl-md snapshot 1`] = `
border-bottom-left-radius: 0.375rem;
@supports (corner-shape: superellipse(2)) {
border-bottom-left-radius: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-left-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -28,7 +29,7 @@ exports[`plugin.ts utilities > squircle-br-md snapshot 1`] = `
border-bottom-right-radius: 0.375rem;
@supports (corner-shape: superellipse(2)) {
border-bottom-right-radius: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-right-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -41,7 +42,8 @@ exports[`plugin.ts utilities > squircle-e-md snapshot 1`] = `
--squircle-r: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
border-start-end-radius: var(--squircle-r);
border-end-end-radius: var(--squircle-r);
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-start-end-shape: superellipse(var(--squircle-amt, 2));
+ corner-end-end-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -51,7 +53,7 @@ exports[`plugin.ts utilities > squircle-ee-md snapshot 1`] = `
border-end-end-radius: 0.375rem;
@supports (corner-shape: superellipse(2)) {
border-end-end-radius: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-end-end-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -61,7 +63,7 @@ exports[`plugin.ts utilities > squircle-es-md snapshot 1`] = `
border-end-start-radius: 0.375rem;
@supports (corner-shape: superellipse(2)) {
border-end-start-radius: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-end-start-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -74,7 +76,8 @@ exports[`plugin.ts utilities > squircle-l-md snapshot 1`] = `
--squircle-r: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
border-top-left-radius: var(--squircle-r);
border-bottom-left-radius: var(--squircle-r);
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-left-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-left-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -98,7 +101,8 @@ exports[`plugin.ts utilities > squircle-r-md snapshot 1`] = `
--squircle-r: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
border-top-right-radius: var(--squircle-r);
border-bottom-right-radius: var(--squircle-r);
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-right-shape: superellipse(var(--squircle-amt, 2));
+ corner-bottom-right-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -111,7 +115,8 @@ exports[`plugin.ts utilities > squircle-s-md snapshot 1`] = `
--squircle-r: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
border-start-start-radius: var(--squircle-r);
border-end-start-radius: var(--squircle-r);
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-start-start-shape: superellipse(var(--squircle-amt, 2));
+ corner-end-start-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -121,7 +126,7 @@ exports[`plugin.ts utilities > squircle-se-md snapshot 1`] = `
border-start-end-radius: 0.375rem;
@supports (corner-shape: superellipse(2)) {
border-start-end-radius: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-start-end-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -131,7 +136,7 @@ exports[`plugin.ts utilities > squircle-ss-md snapshot 1`] = `
border-start-start-radius: 0.375rem;
@supports (corner-shape: superellipse(2)) {
border-start-start-radius: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-start-start-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -144,7 +149,8 @@ exports[`plugin.ts utilities > squircle-t-md snapshot 1`] = `
--squircle-r: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
border-top-left-radius: var(--squircle-r);
border-top-right-radius: var(--squircle-r);
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-left-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-right-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -154,7 +160,7 @@ exports[`plugin.ts utilities > squircle-tl-md snapshot 1`] = `
border-top-left-radius: 0.375rem;
@supports (corner-shape: superellipse(2)) {
border-top-left-radius: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-left-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
@@ -164,7 +170,7 @@ exports[`plugin.ts utilities > squircle-tr-md snapshot 1`] = `
border-top-right-radius: 0.375rem;
@supports (corner-shape: superellipse(2)) {
border-top-right-radius: calc(0.375rem * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))));
- corner-shape: superellipse(var(--squircle-amt, 2));
+ corner-top-right-shape: superellipse(var(--squircle-amt, 2));
}
}"
`;
diff --git a/package/src/panda.test.ts b/package/src/panda.test.ts
index 7a805fc..a9dd8e0 100644
--- a/package/src/panda.test.ts
+++ b/package/src/panda.test.ts
@@ -70,7 +70,7 @@ describe("panda preset transform output", () => {
{
"@supports (corner-shape: superellipse(2))": {
"borderTopLeftRadius": "calc(var(--radii-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))))",
- "cornerShape": "superellipse(var(--squircle-amt, 2))",
+ "cornerTopLeftShape": "superellipse(var(--squircle-amt, 2))",
},
"borderTopLeftRadius": "var(--radii-md)",
}
@@ -89,7 +89,8 @@ describe("panda preset transform output", () => {
"--squircle-r": "calc(var(--radii-md) * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * var(--squircle-amt, 2)))))",
"borderTopLeftRadius": "var(--squircle-r)",
"borderTopRightRadius": "var(--squircle-r)",
- "cornerShape": "superellipse(var(--squircle-amt, 2))",
+ "cornerTopLeftShape": "superellipse(var(--squircle-amt, 2))",
+ "cornerTopRightShape": "superellipse(var(--squircle-amt, 2))",
},
"borderTopLeftRadius": "var(--radii-md)",
"borderTopRightRadius": "var(--radii-md)",
@@ -97,7 +98,7 @@ describe("panda preset transform output", () => {
`);
});
- it("squircleAmount sets the variable and gates corner-shape", () => {
+ it("squircleAmount sets only the variable, applying no shape of its own", () => {
const out = preset.utilities!.extend!["squircleAmount"]!.transform!("3", {
token: mockToken,
raw: "3",
@@ -106,9 +107,6 @@ describe("panda preset transform output", () => {
expect(out).toMatchInlineSnapshot(`
{
"--squircle-amt": "3",
- "@supports (corner-shape: superellipse(2))": {
- "cornerShape": "superellipse(var(--squircle-amt))",
- },
}
`);
});
@@ -141,11 +139,8 @@ describe("panda preset options", () => {
mockArgs("3"),
) as Record;
expect(amtOut["--my-amt"]).toBe("3");
- const amtSupports = amtOut["@supports (corner-shape: superellipse(2))"] as Record<
- string,
- string
- >;
- expect(amtSupports["cornerShape"]).toBe("superellipse(var(--my-amt))");
+ // The amount utility contributes no shape, so the variable is all it emits.
+ expect(Object.keys(amtOut)).toEqual(["--my-amt"]);
const allOutput = JSON.stringify({ radiusOut, amtOut });
expect(allOutput).not.toContain("--squircle-amt");
diff --git a/package/src/panda.ts b/package/src/panda.ts
index 8c88928..2566f5e 100644
--- a/package/src/panda.ts
+++ b/package/src/panda.ts
@@ -55,15 +55,13 @@ export function squirclePandaPreset(options: SquirclePandaPresetOptions = {}) {
};
}
+ // Only sets the amount — the same thing writing the custom property yourself
+ // does. Applying a corner-shape here would reshape all four corners,
+ // including ones no squircle utility claimed.
utilities["squircleAmount"] = {
shorthand: "squircleAmt",
values: { type: "number" },
- transform: (value) => ({
- [amtVar]: value,
- [SUPPORTS_RULE]: {
- cornerShape: `superellipse(var(${amtVar}))`,
- },
- }),
+ transform: (value) => ({ [amtVar]: value }),
};
return definePreset({
diff --git a/package/src/squircle-css.test.ts b/package/src/squircle-css.test.ts
index e165a51..3c25090 100644
--- a/package/src/squircle-css.test.ts
+++ b/package/src/squircle-css.test.ts
@@ -1,13 +1,14 @@
import { describe, expect, it } from "vitest";
import { createCompiler, VARIANTS } from "./test-utils";
+import { cornerShapeProp } from "./variants";
const { compileCss } = createCompiler(import.meta.dirname);
describe("squircle.css utilities", () => {
- it("squircle-amt-* sets --squircle-amt and corner-shape", async () => {
+ it("squircle-amt-* sets only --squircle-amt, applying no shape of its own", async () => {
const css = await compileCss(["squircle-amt-2"]);
expect(css).toContain("--squircle-amt: 2");
- expect(css).toContain("corner-shape: superellipse(var(--squircle-amt))");
+ expect(css).not.toContain("corner-shape:");
});
for (const [suffix, props] of Object.entries(VARIANTS)) {
@@ -23,7 +24,11 @@ describe("squircle.css utilities", () => {
it(`${className} applies corrected radius in @supports block`, async () => {
const css = await compileCss([className]);
expect(css).toContain("@supports (corner-shape: superellipse(2))");
- expect(css).toContain("corner-shape: superellipse(var(--squircle-amt, 2))");
+ // Only the corners this variant sets a radius on get reshaped, so the
+ // all-corners utility uses the shorthand and the rest use longhands.
+ for (const prop of props) {
+ expect(css).toContain(`${cornerShapeProp(prop)}: superellipse(var(--squircle-amt, 2))`);
+ }
expect(css).toContain("pow(2, -0.5)");
});
@@ -50,6 +55,36 @@ describe("squircle.css utilities", () => {
});
}
+ describe("static none/full utilities (matching rounded-none/rounded-full)", () => {
+ it("squircle-full uses calc(infinity * 1px) uncorrected, with corner-shape", async () => {
+ const css = await compileCss(["squircle-full"]);
+ expect(css).toContain("border-radius: calc(infinity * 1px)");
+ expect(css).toContain("corner-shape: superellipse(var(--squircle-amt, 2))");
+ // Correcting an infinite radius is a no-op, so no correction is emitted.
+ expect(css).not.toContain("pow(");
+ expect(css).not.toContain("--squircle-r");
+ });
+
+ it("squircle-t-full applies the full radius to side variants", async () => {
+ const css = await compileCss(["squircle-t-full"]);
+ expect(css).toContain("border-top-left-radius: calc(infinity * 1px)");
+ expect(css).toContain("border-top-right-radius: calc(infinity * 1px)");
+ });
+
+ it("squircle-none is plain zero radius with no correction", async () => {
+ const css = await compileCss(["squircle-none"]);
+ expect(css).toContain("border-radius: 0");
+ expect(css).not.toContain("@supports");
+ expect(css).not.toContain("corner-shape");
+ });
+
+ it("squircle-tl-none zeroes a single corner", async () => {
+ const css = await compileCss(["squircle-tl-none"]);
+ expect(css).toContain("border-top-left-radius: 0");
+ expect(css).not.toContain("@supports");
+ });
+ });
+
describe("arbitrary values", () => {
it("squircle-[1rem] emits literal length in fallback and calc", async () => {
const css = await compileCss(["squircle-[1rem]"]);
@@ -87,7 +122,7 @@ describe("squircle.css utilities", () => {
it("squircle-amt-[4.5] accepts arbitrary bare number", async () => {
const css = await compileCss(["squircle-amt-[4.5]"]);
expect(css).toContain("--squircle-amt: 4.5");
- expect(css).toContain("corner-shape: superellipse(var(--squircle-amt))");
+ expect(css).not.toContain("corner-shape:");
});
it("squircle-amt-[1em] is rejected (unit-bearing values are not numbers)", async () => {
diff --git a/package/src/stylex.ts b/package/src/stylex.ts
index 82a8bf5..c226003 100644
--- a/package/src/stylex.ts
+++ b/package/src/stylex.ts
@@ -79,7 +79,11 @@ export const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerTopLeftShape: {
+ default: null,
+ "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
+ },
+ cornerTopRightShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -94,7 +98,11 @@ export const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerTopRightShape: {
+ default: null,
+ "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
+ },
+ cornerBottomRightShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -109,7 +117,11 @@ export const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerBottomLeftShape: {
+ default: null,
+ "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
+ },
+ cornerBottomRightShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -124,7 +136,11 @@ export const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerTopLeftShape: {
+ default: null,
+ "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
+ },
+ cornerBottomLeftShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -141,7 +157,11 @@ export const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerStartStartShape: {
+ default: null,
+ "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
+ },
+ cornerEndStartShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -156,7 +176,11 @@ export const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerStartEndShape: {
+ default: null,
+ "@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
+ },
+ cornerEndEndShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -169,7 +193,7 @@ export const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerTopLeftShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -180,7 +204,7 @@ export const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerTopRightShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -191,7 +215,7 @@ export const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerBottomRightShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -202,7 +226,7 @@ export const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerBottomLeftShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -215,7 +239,7 @@ export const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerStartStartShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -226,7 +250,7 @@ export const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerStartEndShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -237,7 +261,7 @@ export const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerEndStartShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
@@ -248,7 +272,7 @@ export const squircle = stylex.create({
default: radius,
"@supports (corner-shape: superellipse(2))": `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt ?? 2}))))`,
},
- cornerShape: {
+ cornerEndEndShape: {
default: null,
"@supports (corner-shape: superellipse(2))": `superellipse(${amt ?? 2})`,
},
diff --git a/package/src/tailwind-corner-shape.test.ts b/package/src/tailwind-corner-shape.test.ts
new file mode 100644
index 0000000..d930d57
--- /dev/null
+++ b/package/src/tailwind-corner-shape.test.ts
@@ -0,0 +1,151 @@
+import { describe, expect, it } from "vitest";
+import { createCompiler, VARIANTS } from "./test-utils";
+import { cornerShapeProp } from "./variants";
+
+const { compileCss, compilePlugin } = createCompiler(import.meta.dirname);
+const paths = [
+ ["css", compileCss],
+ ["plugin", compilePlugin],
+] as const;
+
+// The declaration form of the shorthand. The `@supports` condition also
+// mentions `corner-shape`, so matching the bare property name would always hit.
+const SHORTHAND_DECL = "corner-shape: superellipse(var(";
+
+describe("squircle utilities only reshape the corners they own", () => {
+ for (const [pathName, compile] of paths) {
+ describe(pathName, () => {
+ for (const [suffix, props] of Object.entries(VARIANTS)) {
+ const className = suffix ? `squircle-${suffix}-md` : "squircle-md";
+ const isAllCorners = props[0] === "border-radius";
+
+ it(`${className} uses ${isAllCorners ? "the shorthand" : "per-corner longhands"}`, async () => {
+ const css = await compile([className]);
+ for (const prop of props) {
+ expect(css).toContain(`${cornerShapeProp(prop)}: superellipse(var(--squircle-amt, 2))`);
+ }
+ if (isAllCorners) {
+ expect(css).toContain(SHORTHAND_DECL);
+ } else {
+ // The shorthand would reshape all four corners, including ones
+ // this utility never set a radius on.
+ expect(css).not.toContain(SHORTHAND_DECL);
+ }
+ });
+ }
+
+ it("squircle-t-md leaves the bottom corners' shape alone", async () => {
+ const css = await compile(["squircle-t-md"]);
+ expect(css).not.toContain("corner-bottom");
+ expect(css).not.toContain(SHORTHAND_DECL);
+ });
+
+ it("squircle-tl-md shapes only the top-left corner", async () => {
+ const css = await compile(["squircle-tl-md"]);
+ expect(css).toContain("corner-top-left-shape:");
+ expect(css).not.toContain("corner-top-right-shape:");
+ expect(css).not.toContain("corner-bottom");
+ });
+ });
+ }
+});
+
+describe("rounded-* utilities reset the corner shape they own", () => {
+ for (const [pathName, compile] of paths) {
+ describe(pathName, () => {
+ it("rounded-tl-lg resets only the top-left corner", async () => {
+ const css = await compile(["rounded-tl-lg"]);
+ expect(css).toContain("corner-top-left-shape: round");
+ expect(css).not.toContain("corner-top-right-shape: round");
+ });
+
+ it("rounded-t-lg resets both top corners", async () => {
+ const css = await compile(["rounded-t-lg"]);
+ expect(css).toContain("corner-top-left-shape: round");
+ expect(css).toContain("corner-top-right-shape: round");
+ expect(css).not.toContain("corner-bottom-left-shape: round");
+ });
+
+ it("rounded-lg resets all four via the shorthand", async () => {
+ const css = await compile(["rounded-lg"]);
+ expect(css).toContain("corner-shape: round");
+ });
+
+ it("rounded-tl-full resets the corner too", async () => {
+ const css = await compile(["rounded-tl-full"]);
+ expect(css).toContain("corner-top-left-shape: round");
+ });
+
+ it("rounded-tl-none needs no reset (a zero radius has no visible corner)", async () => {
+ const css = await compile(["rounded-tl-none"]);
+ expect(css).toContain("border-top-left-radius: 0");
+ expect(css).not.toContain("corner-top-left-shape: round");
+ });
+
+ // Same specificity, so the reset only wins if it is emitted later.
+ it("the reset is emitted after the squircle rule it has to override", async () => {
+ const css = await compile(["squircle-lg", "rounded-tl-lg"]);
+ expect(css.indexOf("corner-top-left-shape: round")).toBeGreaterThan(
+ css.indexOf(".squircle-lg {"),
+ );
+ });
+
+ // The reset accepts the same values the squircle-* utilities do: theme
+ // keys and arbitrary lengths, nothing else. Anything wider would reset
+ // corners for values this package tells you not to write.
+ for (const value of ["lg", "[3px]"]) {
+ it(`rounded-tl-${value} gets the reset`, async () => {
+ const css = await compile([`rounded-tl-${value}`]);
+ expect(css).toContain("corner-top-left-shape: round");
+ });
+ }
+
+ // A corner named this way keeps its squircle shape — use a theme key.
+ for (const value of ["[var(--my-r)]", "(--my-r)", "[50%]", "[foo]"]) {
+ it(`rounded-tl-${value} gets no reset (not a length)`, async () => {
+ const css = await compile([`rounded-tl-${value}`]);
+ expect(css).not.toContain("corner-top-left-shape: round");
+ });
+ }
+
+ it("a value Tailwind itself rejects stays unmatched", async () => {
+ const css = await compile(["rounded-tl-garbage"]);
+ expect(css).not.toContain("rounded-tl-garbage");
+ });
+ });
+ }
+
+ // The plugin bounds matching with its `values` map, so its reset carries no
+ // radius. The generated CSS has no such map: it keeps a `--value()` radius
+ // purely so the utility stays bounded, and emits the value core emits anyway.
+ it("plugin: the reset rule carries no radius of its own", async () => {
+ const css = await compilePlugin(["rounded-tl-lg"]);
+ const resetRule = (css.match(/\.rounded-tl-lg \{[^}]*\}/g) ?? []).find((r) =>
+ r.includes("corner-top-left-shape"),
+ );
+ expect(resetRule).toBeDefined();
+ expect(resetRule).not.toContain("border-top-left-radius");
+ });
+});
+
+describe("squircle-amt only sets the amount", () => {
+ for (const [pathName, compile] of paths) {
+ it(`${pathName}: squircle-amt applies no corner-shape of its own`, async () => {
+ const css = await compile(["squircle-amt-[2]"]);
+ expect(css).toContain("--squircle-amt: 2");
+ expect(css).not.toContain("corner-shape:");
+ });
+
+ it(`${pathName}: it does not reshape corners a side variant left alone`, async () => {
+ const css = await compile(["squircle-t-md", "squircle-amt-[3]"]);
+ expect(css).toContain("--squircle-amt: 3");
+ expect(css).not.toContain("corner-bottom");
+ expect(css).not.toContain(SHORTHAND_DECL);
+ });
+
+ it(`${pathName}: the amount still reaches the variants through the variable`, async () => {
+ const css = await compile(["squircle-t-md"]);
+ expect(css).toContain("corner-top-left-shape: superellipse(var(--squircle-amt, 2))");
+ });
+ }
+});
diff --git a/package/src/tailwind-merge.test.ts b/package/src/tailwind-merge.test.ts
new file mode 100644
index 0000000..672e068
--- /dev/null
+++ b/package/src/tailwind-merge.test.ts
@@ -0,0 +1,128 @@
+import { describe, expect, it } from "vitest";
+import { extendTailwindMerge } from "tailwind-merge";
+import { squircleMergeConfig } from "./tailwind";
+import { createCompiler } from "./test-utils";
+
+const twMerge = extendTailwindMerge(squircleMergeConfig);
+const { compileCss, compilePlugin } = createCompiler(import.meta.dirname);
+
+describe("squircleMergeConfig", () => {
+ describe("squircle vs squircle", () => {
+ it("later value wins within the same group", () => {
+ expect(twMerge("squircle-md squircle-lg")).toBe("squircle-lg");
+ expect(twMerge("squircle-tl-sm squircle-tl-lg")).toBe("squircle-tl-lg");
+ });
+
+ it("all-corners cancels earlier side and corner utilities", () => {
+ expect(twMerge("squircle-tl-sm squircle-md")).toBe("squircle-md");
+ expect(twMerge("squircle-t-lg squircle-md")).toBe("squircle-md");
+ expect(twMerge("squircle-tl-full squircle-none")).toBe("squircle-none");
+ });
+
+ it("a single corner does not cancel an earlier all-corners utility", () => {
+ expect(twMerge("squircle-md squircle-tl-sm")).toBe("squircle-md squircle-tl-sm");
+ expect(twMerge("squircle-md squircle-tl-full")).toBe("squircle-md squircle-tl-full");
+ });
+
+ it("a side cancels its own two corners", () => {
+ expect(twMerge("squircle-tl-sm squircle-t-lg")).toBe("squircle-t-lg");
+ expect(twMerge("squircle-tr-sm squircle-t-lg")).toBe("squircle-t-lg");
+ expect(twMerge("squircle-ss-sm squircle-s-lg")).toBe("squircle-s-lg");
+ });
+
+ it("a side does not cancel an unrelated corner or an all-corners utility", () => {
+ expect(twMerge("squircle-bl-sm squircle-t-lg")).toBe("squircle-bl-sm squircle-t-lg");
+ expect(twMerge("squircle-md squircle-t-lg")).toBe("squircle-md squircle-t-lg");
+ });
+
+ it("a corner does not cancel a side", () => {
+ expect(twMerge("squircle-t-lg squircle-tl-sm")).toBe("squircle-t-lg squircle-tl-sm");
+ });
+ });
+
+ describe("squircle vs rounded", () => {
+ it("all-corners utilities cancel each other across families", () => {
+ expect(twMerge("rounded-lg squircle-md")).toBe("squircle-md");
+ expect(twMerge("squircle-md rounded-lg")).toBe("rounded-lg");
+ expect(twMerge("rounded-full squircle-full")).toBe("squircle-full");
+ });
+
+ it("all-corners cancels the other family's sides and corners", () => {
+ expect(twMerge("rounded-tl-sm squircle-md")).toBe("squircle-md");
+ expect(twMerge("rounded-t-lg squircle-md")).toBe("squircle-md");
+ expect(twMerge("squircle-tl-sm rounded-lg")).toBe("rounded-lg");
+ });
+
+ it("a narrower utility refines the other family's broader one", () => {
+ expect(twMerge("squircle-md rounded-tl-sm")).toBe("squircle-md rounded-tl-sm");
+ expect(twMerge("rounded-lg squircle-tl-full")).toBe("rounded-lg squircle-tl-full");
+ });
+
+ it("a side cancels the other family's matching side and corners", () => {
+ expect(twMerge("squircle-tl-sm rounded-t-lg")).toBe("rounded-t-lg");
+ expect(twMerge("rounded-tr-sm squircle-t-none")).toBe("squircle-t-none");
+ });
+
+ it("corners cancel only the matching corner across families", () => {
+ expect(twMerge("rounded-tl-sm squircle-tl-lg")).toBe("squircle-tl-lg");
+ expect(twMerge("rounded-t-lg squircle-tl-sm")).toBe("rounded-t-lg squircle-tl-sm");
+ });
+ });
+
+ describe("squircle-amt is orthogonal to radius classes", () => {
+ it("radius utilities never cancel squircle-amt", () => {
+ expect(twMerge("squircle-amt-3 squircle-md")).toBe("squircle-amt-3 squircle-md");
+ expect(twMerge("squircle-amt-3 rounded-lg")).toBe("squircle-amt-3 rounded-lg");
+ });
+
+ it("later squircle-amt wins over earlier squircle-amt", () => {
+ expect(twMerge("squircle-amt-2 squircle-amt-3")).toBe("squircle-amt-3");
+ });
+ });
+});
+
+/**
+ * Keeping both classes only refines the corner if the narrower utility also
+ * wins the cascade — these all have single-class specificity, so it comes down
+ * to emission order. Tailwind sorts radius utilities by property breadth
+ * (`border-radius` shorthand, then two-corner sides, then single corners) and
+ * interleaves `squircle-*` with `rounded-*` in each tier, so the narrower
+ * utility lands later regardless of family. These cases pin that down.
+ */
+describe("cascade order agrees with merge semantics", () => {
+ // [broader utility, narrower utility that must win the overlapping corner]
+ const pairs: [string, string][] = [
+ ["squircle-md", "squircle-t-lg"],
+ ["squircle-md", "squircle-tl-sm"],
+ ["squircle-md", "squircle-tl-full"],
+ ["squircle-md", "squircle-tl-none"],
+ ["squircle-t-lg", "squircle-tl-sm"],
+ ["squircle-full", "squircle-tl-sm"],
+ ["squircle-none", "squircle-tl-full"],
+ ["squircle-t-full", "squircle-tl-none"],
+ ["squircle-s-md", "squircle-ss-sm"],
+ ["squircle-e-full", "squircle-ee-none"],
+ ["squircle-md", "rounded-tl-sm"],
+ ["rounded-lg", "squircle-tl-full"],
+ ["rounded-lg", "squircle-tl-none"],
+ ["rounded-full", "squircle-tl-sm"],
+ ["rounded-t-lg", "squircle-tl-sm"],
+ ];
+
+ function selectorIndex(css: string, className: string): number {
+ const index = css.indexOf(`.${className} {`);
+ expect(index, `${className} was not emitted`).toBeGreaterThan(-1);
+ return index;
+ }
+
+ for (const [broad, narrow] of pairs) {
+ it(`"${broad} ${narrow}" keeps both, and ${narrow} is emitted last`, async () => {
+ expect(twMerge(`${broad} ${narrow}`)).toBe(`${broad} ${narrow}`);
+
+ for (const compile of [compileCss, compilePlugin]) {
+ const css = await compile([broad, narrow]);
+ expect(selectorIndex(css, narrow)).toBeGreaterThan(selectorIndex(css, broad));
+ }
+ });
+ }
+});
diff --git a/package/src/tailwind.test.ts b/package/src/tailwind.test.ts
index fa7e8b2..7c9d48d 100644
--- a/package/src/tailwind.test.ts
+++ b/package/src/tailwind.test.ts
@@ -4,10 +4,10 @@ import { createCompiler, VARIANTS } from "./test-utils";
const { compilePlugin } = createCompiler(import.meta.dirname);
describe("plugin.ts utilities", () => {
- it("squircle-amt-* sets --squircle-amt and corner-shape", async () => {
+ it("squircle-amt-* sets only --squircle-amt, applying no shape of its own", async () => {
const css = await compilePlugin(["squircle-amt-[2]"]);
expect(css).toContain("--squircle-amt: 2");
- expect(css).toContain("corner-shape: superellipse(var(--squircle-amt))");
+ expect(css).not.toContain("corner-shape:");
});
for (const [suffix, props] of Object.entries(VARIANTS)) {
@@ -48,6 +48,49 @@ describe("plugin.ts utilities", () => {
});
}
+ describe("static none/full utilities (matching rounded-none/rounded-full)", () => {
+ it("squircle-full uses calc(infinity * 1px) uncorrected, with corner-shape", async () => {
+ const css = await compilePlugin(["squircle-full"]);
+ expect(css).toContain("border-radius: calc(infinity * 1px)");
+ expect(css).toContain("corner-shape: superellipse(var(--squircle-amt, 2))");
+ // Correcting an infinite radius is a no-op, so no correction is emitted.
+ expect(css).not.toContain("pow(");
+ expect(css).not.toContain("--squircle-r");
+ });
+
+ it("squircle-full is emitted exactly once (no v3-compat theme duplicate)", async () => {
+ const css = await compilePlugin(["squircle-full"]);
+ expect(css.match(/\.squircle-full \{/g)).toHaveLength(1);
+ expect(css).not.toContain("9999px");
+ });
+
+ it("squircle-tl-full applies the full radius to corner variants", async () => {
+ const css = await compilePlugin(["squircle-tl-full"]);
+ expect(css).toContain("border-top-left-radius: calc(infinity * 1px)");
+ });
+
+ it("squircle-none is plain zero radius with no correction", async () => {
+ const css = await compilePlugin(["squircle-none"]);
+ expect(css).toContain("border-radius: 0");
+ expect(css).not.toContain("@supports");
+ expect(css).not.toContain("corner-shape");
+ expect(css.match(/\.squircle-none \{/g)).toHaveLength(1);
+ });
+
+ it("squircle-t-none zeroes side variants", async () => {
+ const css = await compilePlugin(["squircle-t-none"]);
+ expect(css).toContain("border-top-left-radius: 0");
+ expect(css).toContain("border-top-right-radius: 0");
+ });
+
+ it("custom prefix applies to static none/full utilities", async () => {
+ const css = await compilePlugin(["se-none", "se-full"], "prefix: se;");
+ expect(css).toContain(".se-none");
+ expect(css).toContain(".se-full");
+ expect(css).toContain("border-radius: calc(infinity * 1px)");
+ });
+ });
+
describe("arbitrary and invalid values", () => {
it("squircle-[1rem] emits literal length", async () => {
const css = await compilePlugin(["squircle-[1rem]"]);
@@ -105,7 +148,7 @@ describe("plugin.ts custom options", () => {
it("custom prefix works for amt utility", async () => {
const css = await compilePlugin(["se-amt-[3]"], "prefix: se;");
expect(css).toContain(".se-amt-\\[3\\]");
- expect(css).toContain("corner-shape: superellipse");
+ expect(css).toContain("--squircle-amt: 3");
});
it("custom amt-var changes the CSS variable name", async () => {
@@ -117,7 +160,7 @@ describe("plugin.ts custom options", () => {
it("custom amt-var applies to amt utility", async () => {
const css = await compilePlugin(["squircle-amt-[3]"], "amt-var: --se-amt;");
expect(css).toContain("--se-amt: 3");
- expect(css).toContain("superellipse(var(--se-amt))");
+ expect(css).not.toContain("corner-shape:");
});
it("custom r-var changes the intermediate CSS variable name", async () => {
diff --git a/package/src/tailwind.ts b/package/src/tailwind.ts
index cd430d7..cf9bb86 100644
--- a/package/src/tailwind.ts
+++ b/package/src/tailwind.ts
@@ -7,7 +7,9 @@ import plugin from "tailwindcss/plugin";
import {
DEFAULT_AMOUNT_VAR_NAME,
DEFAULT_R_VAR_NAME,
- SUPPORTS_RULE,
+ NONE_RADIUS,
+ cornerShapeProp,
+ squircleFullCssObj,
squircleCssObj,
variantEntries,
} from "./variants";
@@ -30,26 +32,35 @@ export interface SquirclePluginOptions {
const squircle: ReturnType> =
plugin.withOptions((options = {}) =>
// eslint-disable-next-line @typescript-eslint/unbound-method
- ({ matchUtilities, theme }) => {
+ ({ addUtilities, matchUtilities, theme }) => {
const amtVar = options.amtVar ?? options["amt-var"] ?? DEFAULT_AMOUNT_VAR_NAME;
const rVar = options.rVar ?? options["r-var"] ?? DEFAULT_R_VAR_NAME;
const prefix = options.prefix ?? "squircle";
- const radiusValues = theme("borderRadius");
+ // Drop none/full from the functional values (the v3-compat theme still
+ // carries them) — they're registered as static utilities below instead,
+ // with the same values Tailwind uses for rounded-none/rounded-full.
+ const { none: _none, full: _full, ...radiusValues } = theme("borderRadius") ?? {};
+ // Only sets the amount — the same thing writing the custom property
+ // yourself does. Applying a corner-shape here would reshape all four
+ // corners, including ones no squircle-* utility claimed.
matchUtilities(
- {
- [`${prefix}-amt`]: (value: string) => ({
- [amtVar]: value,
- [SUPPORTS_RULE]: {
- "corner-shape": `superellipse(var(${amtVar}))`,
- },
- }),
- },
+ { [`${prefix}-amt`]: (value: string) => ({ [amtVar]: value }) },
{ type: "number" },
);
for (const [suffix, props] of variantEntries()) {
const name = suffix ? `${prefix}-${suffix}` : prefix;
+ // Static -none/-full utilities, registered the same way Tailwind
+ // defines rounded-none and rounded-full (0 and calc(infinity * 1px)
+ // rather than theme values). -none needs no superellipse correction.
+ addUtilities({
+ [`.${name}-none`]: Object.fromEntries(props.map((p) => [p, NONE_RADIUS])),
+ [`.${name}-full`]: squircleFullCssObj(props, { amtVar }) as Record<
+ string,
+ string | Record
+ >,
+ });
matchUtilities(
{
[name]: (value: string) =>
@@ -60,6 +71,20 @@ const squircle: ReturnType> =
},
{ type: "length", values: radiusValues },
);
+
+ // Re-declare the matching rounded-* utility so it also resets the
+ // corners it owns back to `round`. Tailwind keeps its own definition and
+ // emits it alongside this one, so the radius still comes from core and
+ // this contributes only the reset — the initial value, so it is inert
+ // unless a squircle class set a shape on the same element. Without it a
+ // rounded-* utility cannot take a corner back from a squircle, since it
+ // only ever sets a radius. Restricted to lengths, the same values the
+ // squircle-* utilities accept: a paren ref keeps its squircle shape, so
+ // reach for a theme key there as everywhere else in this package.
+ const roundedName = suffix ? `rounded-${suffix}` : "rounded";
+ const reset = Object.fromEntries(props.map((p) => [cornerShapeProp(p), "round"]));
+ addUtilities({ [`.${roundedName}-full`]: reset });
+ matchUtilities({ [roundedName]: () => reset }, { type: "length", values: radiusValues });
}
});
@@ -67,49 +92,50 @@ export default squircle;
// --- tailwind-merge config ---------------------------------------------------
-const allRoundedGroups: string[] = [
- "rounded",
- "rounded-s",
- "rounded-e",
- "rounded-t",
- "rounded-r",
- "rounded-b",
- "rounded-l",
- "rounded-ss",
- "rounded-se",
- "rounded-es",
- "rounded-ee",
- "rounded-tl",
- "rounded-tr",
- "rounded-br",
- "rounded-bl",
-];
+// Mirrors tailwind-merge's own `rounded` hierarchy: a later all-corners
+// utility cancels earlier side/corner utilities, a side cancels its two
+// corners, and a narrower utility never cancels a broader one — so
+// `squircle-md squircle-tl-sm` keeps both, refining one corner. Each squircle
+// group also conflicts with its `rounded` counterpart (and vice versa), since
+// both set the same border-radius properties. `squircle-amt-*` is orthogonal:
+// it controls corner shape, not radius, so radius classes never cancel it.
+const SIDE_CORNERS = {
+ t: ["tl", "tr"],
+ r: ["tr", "br"],
+ b: ["br", "bl"],
+ l: ["tl", "bl"],
+ s: ["ss", "es"],
+ e: ["se", "ee"],
+} as const;
+const CORNERS = ["tl", "tr", "br", "bl", "ss", "se", "es", "ee"] as const;
+const SIDES = Object.keys(SIDE_CORNERS) as (keyof typeof SIDE_CORNERS)[];
+const ALL_SUFFIXES: readonly string[] = ["", ...SIDES, ...CORNERS];
+
+const sq = (suffix: string) => (suffix ? `squircle-${suffix}` : "squircle");
+const rd = (suffix: string) => (suffix ? `rounded-${suffix}` : "rounded");
+
+const conflictingClassGroups: Record = {
+ squircle: [...ALL_SUFFIXES.slice(1).map(sq), ...ALL_SUFFIXES.map(rd)],
+ rounded: ALL_SUFFIXES.map(sq),
+};
+for (const side of SIDES) {
+ const corners: readonly string[] = SIDE_CORNERS[side];
+ conflictingClassGroups[sq(side)] = [...corners.map(sq), rd(side), ...corners.map(rd)];
+ conflictingClassGroups[rd(side)] = [sq(side), ...corners.map(sq)];
+}
+for (const corner of CORNERS) {
+ conflictingClassGroups[sq(corner)] = [rd(corner)];
+ conflictingClassGroups[rd(corner)] = [sq(corner)];
+}
export const squircleMergeConfig = {
extend: {
classGroups: {
- squircle: [
- { squircle: [() => true] },
- { "squircle-t": [() => true] },
- { "squircle-r": [() => true] },
- { "squircle-b": [() => true] },
- { "squircle-l": [() => true] },
- { "squircle-s": [() => true] },
- { "squircle-e": [() => true] },
- { "squircle-tl": [() => true] },
- { "squircle-tr": [() => true] },
- { "squircle-br": [() => true] },
- { "squircle-bl": [() => true] },
- { "squircle-ss": [() => true] },
- { "squircle-se": [() => true] },
- { "squircle-es": [() => true] },
- { "squircle-ee": [() => true] },
- ],
+ ...Object.fromEntries(
+ ALL_SUFFIXES.map((suffix) => [sq(suffix), [{ [sq(suffix)]: [() => true] }]]),
+ ),
"squircle-amt": [{ "squircle-amt": [() => true] }],
},
- conflictingClassGroups: {
- squircle: [...allRoundedGroups, "squircle-amt"],
- ...Object.fromEntries(allRoundedGroups.map((g) => [g, ["squircle", "squircle-amt"]])),
- },
+ conflictingClassGroups,
},
-} as const;
+};
diff --git a/package/src/variants.ts b/package/src/variants.ts
index ad05bb2..8a01759 100644
--- a/package/src/variants.ts
+++ b/package/src/variants.ts
@@ -6,6 +6,10 @@
export const DEFAULT_AMT = 2 as const;
export const DEFAULT_AMOUNT_VAR_NAME = "--squircle-amt" as const;
export const DEFAULT_R_VAR_NAME = "--squircle-r" as const;
+/** Static value for `squircle-full`; matches Tailwind's `rounded-full`. */
+export const FULL_RADIUS = "calc(infinity * 1px)" as const;
+/** Static value for `squircle-none`; matches Tailwind's `rounded-none`. */
+export const NONE_RADIUS = "0" as const;
export const DEFAULT_AMT_CSS = `var(${DEFAULT_AMOUNT_VAR_NAME}, ${DEFAULT_AMT})` as const;
export const getCornerShape = (varName: string = DEFAULT_AMOUNT_VAR_NAME) =>
@@ -15,6 +19,36 @@ export function correctedRadius(radius: string, amt: string = DEFAULT_AMT_CSS):
return `calc(${radius} * (1 - pow(2, -0.5)) / (1 - pow(2, -1 * pow(2, -1 * ${amt}))))` as const;
}
+/**
+ * The corner-shape property matching a radius property: `border-radius` maps to
+ * the four-corner `corner-shape` shorthand, `border-top-left-radius` to
+ * `corner-top-left-shape`, and so on. A utility that owns fewer than four
+ * corners has to use the longhands, or it reshapes corners whose radius it
+ * never set — `squircle-t-lg rounded-b-lg` would squircle the bottom corners.
+ */
+export function cornerShapeProp(radiusProp: string): string {
+ return radiusProp.replace(/^border-/, "corner-").replace(/radius$/, "shape");
+}
+
+/**
+ * CSS object for the static `-full` utilities. The radius stays uncorrected —
+ * an infinite radius times any positive factor is still infinite, and the
+ * browser clamps it either way — so both branches share the same value, like
+ * `rounded-full`; the `@supports` block only adds the superellipse shape.
+ */
+export function squircleFullCssObj(
+ props: string[],
+ options: Pick = {},
+): CssLikeObject {
+ const obj: CssLikeObject = {};
+ for (const p of props) obj[p] = FULL_RADIUS;
+ const shape = getCornerShape(options.amtVar ?? DEFAULT_AMOUNT_VAR_NAME);
+ const supportsBlock: Record = {};
+ for (const p of props) supportsBlock[cornerShapeProp(p)] = shape;
+ obj[SUPPORTS_RULE] = supportsBlock;
+ return obj;
+}
+
type SectionComment = { comment: string };
type VariantEntry = string[] | SectionComment;
@@ -208,7 +242,6 @@ export function squircleCssObj(
const cornerShape = getCornerShape(amtVar);
const corrected = correctedRadius(radius, amtCss);
- const cornerShapeKey = keyCase === "camel" ? "cornerShape" : "corner-shape";
const propKey = (p: string) => (keyCase === "camel" ? toCamel(p) : p);
const fallback: Record = {};
@@ -221,7 +254,7 @@ export function squircleCssObj(
} else {
for (const p of props) supportsBlock[propKey(p)] = corrected;
}
- supportsBlock[cornerShapeKey] = cornerShape;
+ for (const p of props) supportsBlock[propKey(cornerShapeProp(p))] = cornerShape;
return {
...fallback,
diff --git a/package/vite.config.ts b/package/vite.config.ts
index c7a847f..ec5db17 100644
--- a/package/vite.config.ts
+++ b/package/vite.config.ts
@@ -18,7 +18,10 @@ export default defineConfig({
run: {
tasks: {
"test:tailwind": {
+ // Matches tailwind.test.ts and tailwind-merge.test.ts; the latter
+ // compiles the generated utils.css, so the build has to run first.
command: "vp test run tailwind",
+ dependsOn: ["build"],
},
"test:css": {
command: "vp test run squircle-css",