Skip to content

Commit 233e7c8

Browse files
committed
First commit
1 parent ac5df95 commit 233e7c8

File tree

12 files changed

+5056
-0
lines changed

12 files changed

+5056
-0
lines changed

.gitignore

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
27+
# local env files
28+
.env*.local
29+
30+
# vercel
31+
.vercel
32+
33+
# typescript
34+
*.tsbuildinfo
35+
next-env.d.ts

.prettierrc.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/** @type {import("@types/prettier").Options} */
2+
module.exports = {
3+
printWidth: 100,
4+
useTabs: true,
5+
};

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6+
7+
## [0.1.0] - 2023-09-09
8+
9+
Initial release!
10+
11+
[unreleased]: https://github.com/ekafyi/tailwindcss-view-transitions/compare/v0.1.0...HEAD
12+
[0.1.0]: https://github.com/ekafyi/tailwindcss-view-transitions/releases/tag/v0.1.0

README.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# tailwindcss-view-transitions
2+
3+
[![NPM package version](https://img.shields.io/npm/v/tailwindcss-view-transitions)](https://www.npmjs.com/package/tailwindcss-view-transitions)
4+
5+
A plugin for customizing styles for the [View Transitions Web API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API).
6+
7+
## Installation
8+
9+
```sh
10+
npm install -D tailwindcss-view-transitions
11+
```
12+
13+
Then add the plugin to your `tailwind.config.js` file:
14+
15+
```js
16+
// tailwind.config.js
17+
module.exports = {
18+
theme: {
19+
// ...
20+
},
21+
plugins: [
22+
require("tailwindcss-view-transitions"),
23+
// ...
24+
],
25+
}
26+
```
27+
28+
## Usage
29+
30+
Use the `vt-name-[ANY_STRING]` utility class to [create a separate view transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API#different_transitions_for_different_elements) on specific elements.
31+
32+
```html
33+
<div class="vt-name-[main-header]">
34+
</div>
35+
```
36+
37+
Use `vt-name-none` to disable a view transition. Can be used with any Tailwind variant, such as `md:*`.
38+
39+
```html
40+
<div class="vt-name-[main-header] md:vt-name-none">
41+
</div>
42+
43+
<div class="vt-name-[main-header] motion-reduce:vt-name-none">
44+
</div>
45+
```
46+
47+
The name can be any string except `root` (❌ `vt-name-[root]`), which is reserved for the default top-level view transition.
48+
49+
| Class | CSS properties |
50+
| --- | --- |
51+
| `vt-name-[foo]` | `view-transition-name: foo;` |
52+
| `vt-name-[foo-bar]` | `view-transition-name: foo-bar;` |
53+
| `vt-name-none` | `view-transition-name: none;` |
54+
55+
### Styling with CSS
56+
57+
Style the view transition pseudo-elements from your global CSS file.
58+
59+
```css
60+
/* input.css */
61+
@tailwind base;
62+
@tailwind components;
63+
@tailwind utilities;
64+
65+
::view-transition-old(root),
66+
::view-transition-new(root) {
67+
animation: none;
68+
}
69+
70+
::view-transition-old(main-content) {
71+
/* Add custom animation or style here */
72+
/* animation: ... */
73+
}
74+
75+
::view-transition-new(main-content) {
76+
/* Add custom animation or style here */
77+
/* animation: ... */
78+
}
79+
```
80+
81+
### Configuration
82+
83+
Alternatively, you can define styles from plugin configuration in your `tailwind.config.js` file.
84+
85+
```js
86+
// tailwind.config.js
87+
module.exports = {
88+
plugins: [
89+
require("tailwindcss-view-transitions")({
90+
disableAllReduceMotion: false,
91+
styles: {
92+
// ...
93+
},
94+
}),
95+
// ... other plugins
96+
],
97+
}
98+
```
99+
100+
## Options
101+
102+
The plugin config accepts an options object as argument which contains these properties. All are optional.
103+
104+
### `disableAllReduceMotion`
105+
106+
- Type: `boolean`
107+
- Default: `false`
108+
109+
Disables _all_ view transitions animation if user has set preference for reduced motion. (Note: Consider [this point](https://developer.chrome.com/docs/web-platform/view-transitions/#:~:text=a%20preference%20for%20%27reduced%20motion%27%20doesn%27t%20mean%20the%20user%20wants%20no%20motion) before disabling animations completely.)
110+
111+
If `true`, it applies this code globally:
112+
113+
```css
114+
@media (prefers-reduced-motion) {
115+
::view-transition-group(*),::view-transition-old(*),::view-transition-new(*) {
116+
animation: none !important;
117+
}
118+
}
119+
```
120+
121+
### `styles`
122+
123+
- Type: `Record<string, CSSRuleObject & { old?: CSSRuleObject; new?: CSSRuleObject }>`
124+
- Default: `{}`
125+
126+
Defines CSS styles for the view transition pseudo-elements.
127+
128+
The styles object may contain any number of properties.
129+
130+
- The **key** is the view transition name (`root` or any string value assigned [here](#usage))
131+
- The **value** is one or more of these:
132+
- a [CSS rule object](https://github.com/tailwindlabs/tailwindcss/blob/9faf10958b880067cacdd0ef3c4bf9e64172ed91/types/config.d.ts#L15), which will be applied to both outgoing (`::view-transition-old(VT_NAME)`) and incoming (`::view-transition-new(VT_NAME)`) pseudo-elements
133+
- a propery `old` containing a CSS rule object, which will be applied only to `::view-transition-old(VT_NAME)`
134+
- a propery `new` containing a CSS rule object, which will be applied only to `::view-transition-new(VT_NAME)`
135+
136+
| styles config | Generated CSS |
137+
| --- | --- |
138+
| <pre>{ <br/> root: { animation: "none" },<br/>}</pre> | <pre>::view-transition-old(root),<br/>::view-transition-new(root) {<br/> animation: none;<br/>}</pre> |
139+
| <pre>{ <br/> root: { <br/> old: { animationDuration: "1s" },<br/> new: { animationDuration: "3s" },<br/> },<br/>}</pre> | <pre>::view-transition-old(root) {<br/> animation-duration: 1s;<br/>}<br/>::view-transition-new(root) {<br/> animation-duration: 3s;<br/>}</pre> |
140+
| <pre>{ <br/> root: { animation: "none" },<br/> "main-content": { <br/> old: { animationDuration: "1s" },<br/> new: { animationDuration: "3s" },<br/> },<br/>}</pre> | <pre>::view-transition-old(root),<br/>::view-transition-new(root) {<br/> animation: none;<br/>}<br/><br/>::view-transition-old(main-content) {<br/> animation-duration: 1s;<br/>}<br/>::view-transition-new(main-content) {<br/> animation-duration: 3s;<br/>}</pre> |
141+
142+
⚠️ If applying custom CSS animation, you need to define `@keyframes` separately in your CSS file or through [Tailwind theme configuration](https://tailwindcss.com/docs/animation#customizing-your-theme), or alternatively use an existing `@keyframes` animation.
143+
144+
Detailed examples: https://github.com/ekafyi/tailwindcss-view-transitions/blob/main/docs/examples.md
145+
146+
## When not to use?
147+
148+
You may not need this plugin if:
149+
150+
* You don’t need to customize the browser default transition (crossfade) styles 😁
151+
* You do styling outside of Tailwind configuration
152+
* You exclusively use a (meta)framework that has its own API for conveniently styling view transitions, such as [Astro](https://docs.astro.build/en/guides/view-transitions/)
153+
154+
As an unofficial plugin, it will be deprecated when/if Tailwind adds an official plugin for styling view transitions.
155+
156+
## Bugs & feature requests
157+
158+
While I'm not actively accepting feature requests, I outlined future plans in the [Discussions](https://github.com/ekafyi/tailwindcss-view-transitions/discussions).
159+
160+
Found a bug? Feel free to [open an issue](https://github.com/ekafyi/tailwindcss-view-transitions/issues).

docs/examples.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Examples
2+
3+
TODO

jest/customMatchers.js

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// From https://github.com/tailwindlabs/tailwindcss-line-clamp/blob/master/jest/customMatchers.js
2+
const prettier = require('prettier')
3+
const { diff } = require('jest-diff')
4+
5+
function format(input) {
6+
return prettier.format(input, {
7+
parser: 'css',
8+
printWidth: 100,
9+
})
10+
}
11+
12+
expect.extend({
13+
// Compare two CSS strings with all whitespace removed
14+
// This is probably naive but it's fast and works well enough.
15+
toMatchCss(received, argument) {
16+
function stripped(str) {
17+
return str.replace(/\s/g, '').replace(/;/g, '')
18+
}
19+
20+
const options = {
21+
comment: 'stripped(received) === stripped(argument)',
22+
isNot: this.isNot,
23+
promise: this.promise,
24+
}
25+
26+
const pass = stripped(received) === stripped(argument)
27+
28+
const message = pass
29+
? () => {
30+
return (
31+
this.utils.matcherHint('toMatchCss', undefined, undefined, options) +
32+
'\n\n' +
33+
`Expected: not ${this.utils.printExpected(format(received))}\n` +
34+
`Received: ${this.utils.printReceived(format(argument))}`
35+
)
36+
}
37+
: () => {
38+
const actual = format(received)
39+
const expected = format(argument)
40+
41+
const diffString = diff(expected, actual, {
42+
expand: this.expand,
43+
})
44+
45+
return (
46+
this.utils.matcherHint('toMatchCss', undefined, undefined, options) +
47+
'\n\n' +
48+
(diffString && diffString.includes('- Expect')
49+
? `Difference:\n\n${diffString}`
50+
: `Expected: ${this.utils.printExpected(expected)}\n` +
51+
`Received: ${this.utils.printReceived(actual)}`)
52+
)
53+
}
54+
55+
return { actual: received, message, pass }
56+
},
57+
toIncludeCss(received, argument) {
58+
const options = {
59+
comment: 'stripped(received).includes(stripped(argument))',
60+
isNot: this.isNot,
61+
promise: this.promise,
62+
}
63+
64+
const actual = format(received)
65+
const expected = format(argument)
66+
67+
const pass = actual.includes(expected)
68+
69+
const message = pass
70+
? () => {
71+
return (
72+
this.utils.matcherHint('toIncludeCss', undefined, undefined, options) +
73+
'\n\n' +
74+
`Expected: not ${this.utils.printExpected(format(received))}\n` +
75+
`Received: ${this.utils.printReceived(format(argument))}`
76+
)
77+
}
78+
: () => {
79+
const diffString = diff(expected, actual, {
80+
expand: this.expand,
81+
})
82+
83+
return (
84+
this.utils.matcherHint('toIncludeCss', undefined, undefined, options) +
85+
'\n\n' +
86+
(diffString && diffString.includes('- Expect')
87+
? `Difference:\n\n${diffString}`
88+
: `Expected: ${this.utils.printExpected(expected)}\n` +
89+
`Received: ${this.utils.printReceived(actual)}`)
90+
)
91+
}
92+
93+
return { actual: received, message, pass }
94+
},
95+
})
96+
97+
expect.extend({
98+
// Compare two CSS strings with all whitespace removed
99+
// This is probably naive but it's fast and works well enough.
100+
toMatchFormattedCss(received, argument) {
101+
function format(input) {
102+
return prettier.format(input.replace(/\n/g, ''), {
103+
parser: 'css',
104+
printWidth: 100,
105+
})
106+
}
107+
const options = {
108+
comment: 'stripped(received) === stripped(argument)',
109+
isNot: this.isNot,
110+
promise: this.promise,
111+
}
112+
113+
let formattedReceived = format(received)
114+
let formattedArgument = format(argument)
115+
116+
const pass = formattedReceived === formattedArgument
117+
118+
const message = pass
119+
? () => {
120+
return (
121+
this.utils.matcherHint('toMatchCss', undefined, undefined, options) +
122+
'\n\n' +
123+
`Expected: not ${this.utils.printExpected(formattedReceived)}\n` +
124+
`Received: ${this.utils.printReceived(formattedArgument)}`
125+
)
126+
}
127+
: () => {
128+
const actual = formattedReceived
129+
const expected = formattedArgument
130+
131+
const diffString = diff(expected, actual, {
132+
expand: this.expand,
133+
})
134+
135+
return (
136+
this.utils.matcherHint('toMatchCss', undefined, undefined, options) +
137+
'\n\n' +
138+
(diffString && diffString.includes('- Expect')
139+
? `Difference:\n\n${diffString}`
140+
: `Expected: ${this.utils.printExpected(expected)}\n` +
141+
`Received: ${this.utils.printReceived(actual)}`)
142+
)
143+
}
144+
145+
return { actual: received, message, pass }
146+
},
147+
})

0 commit comments

Comments
 (0)