Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .changeset/custom-bezier-and-optimizations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
"tw-easing-gradients": minor
---

feat: add custom bezier curves and optimize plugin

### Custom Bezier Curves

Use arbitrary easing values with bracket notation:

```html
<div class="bg-ease-to-r-[0.22,1,0.36,1] from-black"></div>
```

The four values are cubic bezier control points (x1, y1, x2, y2).

### New Exports

- `getCoordinatesFromControlPoints(controlPoints, stops)` — compute easing coordinates from raw control points
- `parseBezierValues(input)` — parse comma-separated bezier strings

### Optimizations

- Gradient stops now cached per easing (4 computations instead of 32)
- Removed redundant class name ternary
- Constants derived from source types instead of manually duplicated
- `matchUtilities` batched into single call
- `getCoordinatesFromControlPoints` accepts `readonly` tuples (no unnecessary type casts)

### Docs

- Interactive custom bezier curve editor in playground
- Custom bezier documented in utilities, examples, and getting started
- Landing page updated with custom curves mention
- Pseudo-element examples replaced with absolute div pattern
- Package manager tabs on install button (pnpm/npm/yarn/bun)
9 changes: 9 additions & 0 deletions .claude/skills/tw-easing-gradients/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@ Four easing functions available:
| `to-bl` | to bottom left |
| `to-br` | to bottom right |

## Custom Bezier

Arbitrary easing curves via bracket notation:

```html
<div class="bg-ease-to-r-[0.22,1,0.36,1] from-black"></div>
<div class="bg-ease-to-b-[0.42,0,0.58,1] from-violet-600 to-pink-500"></div>
```

## Configuration

Configure gradient stops in CSS (default: 15):
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v5

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'pnpm'
Expand All @@ -35,8 +35,8 @@ jobs:
- name: Build package
run: pnpm --filter tw-easing-gradients build

- name: Type check
run: pnpm --filter tw-easing-gradients typecheck

- name: Run tests
run: pnpm --filter tw-easing-gradients test

- name: Type check
run: pnpm --filter tw-easing-gradients tsc --noEmit
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v5

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'pnpm'
Expand Down
30 changes: 20 additions & 10 deletions apps/docs/.velite/docs.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/docs/.velite/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// This file is generated by Velite

export { default as docs } from './docs.json'
export { default as docs } from './docs.json' with { type: 'json' }
27 changes: 19 additions & 8 deletions apps/docs/src/content/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,16 @@ Create smooth fade overlays on hero images:
</div>
```

## Pseudo-Element Overlays
## Dual Overlay

Apply gradients to `::before` and `::after` pseudo-elements for dual overlays:
Top and bottom fade overlays using absolute positioned divs:

<PseudoOverlay variant="black" />

```html
<div
class="relative bg-white before:absolute before:inset-x-0 before:top-0 before:z-10 before:h-1/3
before:bg-ease-to-b before:from-black before:to-transparent before:content-['']
after:absolute after:inset-x-0 after:bottom-0 after:z-10 after:h-1/3
after:bg-ease-to-t after:from-black after:to-transparent after:content-['']"
>
<div class="relative bg-white">
<div class="absolute inset-x-0 top-0 z-10 h-1/3 bg-ease-to-b from-black to-transparent"></div>
<div class="absolute inset-x-0 bottom-0 z-10 h-1/3 bg-ease-to-t from-black to-transparent"></div>
<span class="relative z-20">Content</span>
</div>
```
Expand Down Expand Up @@ -122,6 +119,20 @@ Animated loading placeholder with smooth gradients:
The `via-*` utility is not supported with easing gradients. Use only `from-*` and `to-*` for color stops.
</Callout>

## Custom Bezier

Define your own easing curve using arbitrary values with four cubic bezier control points:

```html
<!-- Aggressive ease-out -->
<div class="bg-ease-to-b-[0.22,1,0.36,1] from-black">
<img src="hero.jpg" class="w-full" />
</div>

<!-- Bouncy overshoot -->
<div class="bg-ease-to-r-[0.68,0,0.27,1.55] from-indigo-500 to-cyan-500"></div>
```

## Next Steps

<CardGrid cols={2}>
Expand Down
8 changes: 8 additions & 0 deletions apps/docs/src/content/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ Use the utilities together with Tailwind's `from-*` and `to-*` color utilities:
</div>
```

You can also use custom easing curves with arbitrary values:

```html
<div class="bg-ease-to-r-[0.22,1,0.36,1] from-black to-transparent">
<!-- Custom cubic bezier easing -->
</div>
```

</Steps>

<Callout type="note" title="Requirements">
Expand Down
1 change: 1 addition & 0 deletions apps/docs/src/content/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ A common use case: text overlays on images. The linear gradient stays solid and
- **oklch color space** - Colors blend naturally without getting gray or muddy in the middle
- **Multiple easing functions** - ease, ease-in, ease-out, ease-in-out
- **All directions** - 8 gradient directions supported
- **Custom bezier** - Arbitrary easing curves via `bg-ease-to-r-[0.22,1,0.36,1]`
- **Works with Tailwind colors** - Uses existing `from-*` and `to-*` utilities
- **Zero runtime JS** - Pure CSS output

Expand Down
11 changes: 11 additions & 0 deletions apps/docs/src/content/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ Skip `to-*` and the plugin fades to transparent automatically.
</TabItem>
</Tabs>

## Custom Bezier

Use arbitrary values to define your own easing curve with four control points:

```html
<div class="bg-ease-to-r-[0.22,1,0.36,1] from-black"></div>
<div class="bg-ease-to-b-[0.68,0,0.27,1.55] from-indigo-500 to-cyan-500"></div>
```

The four values correspond to the control points of a cubic bezier curve: `x1, y1, x2, y2`.

## Next Steps

<CardGrid cols={2}>
Expand Down
80 changes: 50 additions & 30 deletions apps/docs/src/lib/components/InstallButton.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,59 @@
import X from 'phosphor-svelte/lib/X';
import { scale } from 'svelte/transition';

interface Props {
command?: string;
}
const PKG = 'tw-easing-gradients';

let { command = 'npm install tw-easing-gradients' }: Props = $props();
const managers = [
{ label: 'pnpm', verb: 'add' },
{ label: 'npm', verb: 'install' },
{ label: 'yarn', verb: 'add' },
{ label: 'bun', verb: 'add' }
];

let active = $state(0);
let pm = $derived(managers[active]);
let currentCommand = $derived(`${pm.label} ${pm.verb} ${PKG}`);

const clipboard = new UseClipboard();
</script>

<button
onclick={() => clipboard.copy(command)}
class="group border-border bg-foreground/10 text-muted-foreground hover:bg-foreground/15 flex items-center gap-3 rounded-lg border px-5 py-3 font-mono text-sm transition-colors"
>
<span class="text-muted-foreground/70">$</span>
<span>
<span class="text-foreground">npm install</span>
<span class="bg-foreground/10 rounded px-1.5 whitespace-nowrap">
tw-easing-gradients
<div class="flex flex-col items-center gap-2">
<div class="flex gap-1">
{#each managers as pm, i (pm.label)}
<button
onclick={() => (active = i)}
class="rounded-md px-2.5 py-1 text-xs font-medium transition-colors {active === i
? 'bg-foreground/15 text-foreground'
: 'text-muted-foreground/70 hover:text-muted-foreground'}"
>
{pm.label}
</button>
{/each}
</div>

<button
onclick={() => clipboard.copy(currentCommand)}
class="group border-border bg-foreground/10 text-muted-foreground hover:bg-foreground/15 flex items-center gap-3 rounded-lg border px-5 py-3 font-mono text-sm transition-colors"
>
<span class="text-muted-foreground/70">$</span>
<span>
<span class="text-foreground">{pm.label} {pm.verb}</span>
<span class="bg-foreground/10 rounded px-1.5 whitespace-nowrap">{PKG}</span>
</span>
</span>
{#if clipboard.status === 'success'}
<div in:scale={{ duration: 150, start: 0.85 }}>
<Check class="size-4 text-emerald-400" />
</div>
{:else if clipboard.status === 'failure'}
<div in:scale={{ duration: 150, start: 0.85 }}>
<X class="size-4 text-red-400" />
</div>
{:else}
<div in:scale={{ duration: 150, start: 0.85 }}>
<Copy
class="text-muted-foreground/70 group-hover:text-muted-foreground size-4 transition-colors"
/>
</div>
{/if}
</button>
{#if clipboard.status === 'success'}
<div in:scale={{ duration: 150, start: 0.85 }}>
<Check class="size-4 text-emerald-400" />
</div>
{:else if clipboard.status === 'failure'}
<div in:scale={{ duration: 150, start: 0.85 }}>
<X class="size-4 text-red-400" />
</div>
{:else}
<div in:scale={{ duration: 150, start: 0.85 }}>
<Copy
class="text-muted-foreground/70 group-hover:text-muted-foreground size-4 transition-colors"
/>
</div>
{/if}
</button>
</div>
16 changes: 12 additions & 4 deletions apps/docs/src/lib/components/docs/examples/PseudoOverlay.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@
<div class="relative">
{#if variant === 'brand'}
<div
class="ring-border before:from-brand after:from-brand relative flex aspect-4/3 items-center justify-center overflow-hidden rounded-xl bg-white ring-1 before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-10 before:h-1/3 before:bg-linear-to-b before:to-transparent before:content-[''] after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:z-10 after:h-1/3 after:bg-linear-to-t after:to-transparent after:content-['']"
class="ring-border relative flex aspect-4/3 items-center justify-center overflow-hidden rounded-xl bg-white ring-1"
>
<div class="from-brand pointer-events-none absolute inset-x-0 top-0 z-10 h-1/3 bg-linear-to-b to-transparent"></div>
<div class="from-brand pointer-events-none absolute inset-x-0 bottom-0 z-10 h-1/3 bg-linear-to-t to-transparent"></div>
<span class="text-brand relative z-20 font-medium">tailwind</span>
</div>
{:else}
<div
class="ring-border relative flex aspect-4/3 items-center justify-center overflow-hidden rounded-xl bg-white ring-1 before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-10 before:h-1/3 before:bg-linear-to-b before:from-black before:to-transparent before:content-[''] after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:z-10 after:h-1/3 after:bg-linear-to-t after:from-black after:to-transparent after:content-['']"
class="ring-border relative flex aspect-4/3 items-center justify-center overflow-hidden rounded-xl bg-white ring-1"
>
<div class="pointer-events-none absolute inset-x-0 top-0 z-10 h-1/3 bg-linear-to-b from-black to-transparent"></div>
<div class="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-1/3 bg-linear-to-t from-black to-transparent"></div>
<span class="relative z-20 font-medium text-black">tailwind</span>
</div>
{/if}
Expand All @@ -29,16 +33,20 @@
<div class="relative">
{#if variant === 'brand'}
<div
class="ring-brand before:bg-ease-in-out-to-b after:bg-ease-in-out-to-t before:from-brand after:from-brand relative flex aspect-4/3 items-center justify-center overflow-hidden rounded-xl bg-white ring-1 before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-10 before:h-1/3 before:to-transparent before:content-[''] after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:z-10 after:h-1/3 after:to-transparent after:content-['']"
class="ring-brand relative flex aspect-4/3 items-center justify-center overflow-hidden rounded-xl bg-white ring-1"
>
<div class="from-brand pointer-events-none absolute inset-x-0 top-0 z-10 h-1/3 bg-ease-in-out-to-b to-transparent"></div>
<div class="from-brand pointer-events-none absolute inset-x-0 bottom-0 z-10 h-1/3 bg-ease-in-out-to-t to-transparent"></div>
<span class="text-brand relative z-20 font-medium">
tw-easing-gradients
</span>
</div>
{:else}
<div
class="ring-border before:bg-ease-to-b after:bg-ease-to-t relative flex aspect-4/3 items-center justify-center overflow-hidden rounded-xl bg-white ring-1 before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-10 before:h-1/3 before:from-black before:to-transparent before:content-[''] after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:z-10 after:h-1/3 after:from-black after:to-transparent after:content-['']"
class="ring-border relative flex aspect-4/3 items-center justify-center overflow-hidden rounded-xl bg-white ring-1"
>
<div class="pointer-events-none absolute inset-x-0 top-0 z-10 h-1/3 bg-ease-to-b from-black to-transparent"></div>
<div class="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-1/3 bg-ease-to-t from-black to-transparent"></div>
<span class="relative z-20 font-medium text-black">
tw-easing-gradients
</span>
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/lib/components/landing/HeroSection.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
class="text-muted-foreground mt-6 max-w-xl text-lg leading-relaxed text-balance"
>
Linear gradients produce hard color transitions. Fix them with easing
functions, directly in your utility classes.
functions or custom bezier curves, directly in your utility classes.
</p>

<div class="mt-10">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
<!-- Linear (banding visible) -->
<div class="space-y-3">
<div
class="ring-border before:from-brand after:from-brand relative flex aspect-4/3 items-center justify-center overflow-hidden rounded-xl bg-white ring-1 before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-10 before:h-1/3 before:bg-linear-to-b before:to-transparent before:content-[''] after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:z-10 after:h-1/3 after:bg-linear-to-t after:to-transparent after:content-['']"
class="ring-border relative flex aspect-4/3 items-center justify-center overflow-hidden rounded-xl bg-white ring-1"
>
<div class="from-brand pointer-events-none absolute inset-x-0 top-0 z-10 h-1/3 bg-linear-to-b to-transparent"></div>
<div class="from-brand pointer-events-none absolute inset-x-0 bottom-0 z-10 h-1/3 bg-linear-to-t to-transparent"></div>
<span class="text-brand relative z-20 text-sm font-medium">
tailwind
</span>
Expand All @@ -14,8 +16,10 @@
<!-- Eased (smooth) -->
<div class="space-y-3">
<div
class="ring-brand before:bg-ease-in-out-to-b after:bg-ease-in-out-to-t before:from-brand after:from-brand relative flex aspect-4/3 items-center justify-center overflow-hidden rounded-xl bg-white ring-1 before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:z-10 before:h-1/3 before:to-transparent before:content-[''] after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:z-10 after:h-1/3 after:to-transparent after:content-['']"
class="ring-brand relative flex aspect-4/3 items-center justify-center overflow-hidden rounded-xl bg-white ring-1"
>
<div class="from-brand pointer-events-none absolute inset-x-0 top-0 z-10 h-1/3 bg-ease-in-out-to-b to-transparent"></div>
<div class="from-brand pointer-events-none absolute inset-x-0 bottom-0 z-10 h-1/3 bg-ease-in-out-to-t to-transparent"></div>
<span class="text-brand relative z-20 text-sm font-medium">
tw-easing-gradients
</span>
Expand Down
23 changes: 23 additions & 0 deletions apps/docs/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,26 @@ export const BRAND_COLORS = {
from: '#6366F1',
to: '#06B6D4'
};

export type BezierCurve = [number, number, number, number];

export const EASING_CURVES: Record<string, BezierCurve> = {
ease: [0.25, 0.1, 0.25, 1],
'ease-in': [0.42, 0, 1, 1],
'ease-out': [0, 0, 0.58, 1],
'ease-in-out': [0.42, 0, 0.58, 1]
};

export const DIRECTION_CSS: Record<string, string> = {
t: 'to top', r: 'to right', b: 'to bottom', l: 'to left',
tl: 'to top left', tr: 'to top right', bl: 'to bottom left', br: 'to bottom right'
};

export const COLOR_PRESETS = [
{ fromHex: BRAND_COLORS.from, toHex: BRAND_COLORS.to, label: 'Indigo → Cyan' },
{ fromHex: '#8b5cf6', toHex: '#ec4899', label: 'Violet → Pink' },
{ fromHex: '#10b981', toHex: '#3b82f6', label: 'Emerald → Blue' },
{ fromHex: '#f59e0b', toHex: '#ef4444', label: 'Amber → Red' },
{ fromHex: '#000000', toHex: '#ffffff', label: 'Black → White' },
{ fromHex: '#f43f5e', toHex: '#fb923c', label: 'Rose → Orange' }
];
Loading