Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ekafyi committed Sep 10, 2023
1 parent ac5df95 commit 233e7c8
Show file tree
Hide file tree
Showing 12 changed files with 5,056 additions and 0 deletions.
35 changes: 35 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
5 changes: 5 additions & 0 deletions .prettierrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import("@types/prettier").Options} */
module.exports = {
printWidth: 100,
useTabs: true,
};
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Changelog

All notable changes to this project will be documented in this file.

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).

## [0.1.0] - 2023-09-09

Initial release!

[unreleased]: https://github.com/ekafyi/tailwindcss-view-transitions/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/ekafyi/tailwindcss-view-transitions/releases/tag/v0.1.0
160 changes: 160 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# tailwindcss-view-transitions

[![NPM package version](https://img.shields.io/npm/v/tailwindcss-view-transitions)](https://www.npmjs.com/package/tailwindcss-view-transitions)

A plugin for customizing styles for the [View Transitions Web API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API).

## Installation

```sh
npm install -D tailwindcss-view-transitions
```

Then add the plugin to your `tailwind.config.js` file:

```js
// tailwind.config.js
module.exports = {
theme: {
// ...
},
plugins: [
require("tailwindcss-view-transitions"),
// ...
],
}
```

## Usage

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.

```html
<div class="vt-name-[main-header]">
</div>
```

Use `vt-name-none` to disable a view transition. Can be used with any Tailwind variant, such as `md:*`.

```html
<div class="vt-name-[main-header] md:vt-name-none">
</div>

<div class="vt-name-[main-header] motion-reduce:vt-name-none">
</div>
```

The name can be any string except `root` (❌ `vt-name-[root]`), which is reserved for the default top-level view transition.

| Class | CSS properties |
| --- | --- |
| `vt-name-[foo]` | `view-transition-name: foo;` |
| `vt-name-[foo-bar]` | `view-transition-name: foo-bar;` |
| `vt-name-none` | `view-transition-name: none;` |

### Styling with CSS

Style the view transition pseudo-elements from your global CSS file.

```css
/* input.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

::view-transition-old(root),
::view-transition-new(root) {
animation: none;
}

::view-transition-old(main-content) {
/* Add custom animation or style here */
/* animation: ... */
}

::view-transition-new(main-content) {
/* Add custom animation or style here */
/* animation: ... */
}
```

### Configuration

Alternatively, you can define styles from plugin configuration in your `tailwind.config.js` file.

```js
// tailwind.config.js
module.exports = {
plugins: [
require("tailwindcss-view-transitions")({
disableAllReduceMotion: false,
styles: {
// ...
},
}),
// ... other plugins
],
}
```

## Options

The plugin config accepts an options object as argument which contains these properties. All are optional.

### `disableAllReduceMotion`

- Type: `boolean`
- Default: `false`

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.)

If `true`, it applies this code globally:

```css
@media (prefers-reduced-motion) {
::view-transition-group(*),::view-transition-old(*),::view-transition-new(*) {
animation: none !important;
}
}
```

### `styles`

- Type: `Record<string, CSSRuleObject & { old?: CSSRuleObject; new?: CSSRuleObject }>`
- Default: `{}`

Defines CSS styles for the view transition pseudo-elements.

The styles object may contain any number of properties.

- The **key** is the view transition name (`root` or any string value assigned [here](#usage))
- The **value** is one or more of these:
- 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
- a propery `old` containing a CSS rule object, which will be applied only to `::view-transition-old(VT_NAME)`
- a propery `new` containing a CSS rule object, which will be applied only to `::view-transition-new(VT_NAME)`

| styles config | Generated CSS |
| --- | --- |
| <pre>{ <br/> root: { animation: "none" },<br/>}</pre> | <pre>::view-transition-old(root),<br/>::view-transition-new(root) {<br/> animation: none;<br/>}</pre> |
| <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> |
| <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> |

⚠️ 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.

Detailed examples: https://github.com/ekafyi/tailwindcss-view-transitions/blob/main/docs/examples.md

## When not to use?

You may not need this plugin if:

* You don’t need to customize the browser default transition (crossfade) styles 😁
* You do styling outside of Tailwind configuration
* 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/)

As an unofficial plugin, it will be deprecated when/if Tailwind adds an official plugin for styling view transitions.

## Bugs & feature requests

While I'm not actively accepting feature requests, I outlined future plans in the [Discussions](https://github.com/ekafyi/tailwindcss-view-transitions/discussions).

Found a bug? Feel free to [open an issue](https://github.com/ekafyi/tailwindcss-view-transitions/issues).
3 changes: 3 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Examples

TODO
147 changes: 147 additions & 0 deletions jest/customMatchers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// From https://github.com/tailwindlabs/tailwindcss-line-clamp/blob/master/jest/customMatchers.js
const prettier = require('prettier')
const { diff } = require('jest-diff')

function format(input) {
return prettier.format(input, {
parser: 'css',
printWidth: 100,
})
}

expect.extend({
// Compare two CSS strings with all whitespace removed
// This is probably naive but it's fast and works well enough.
toMatchCss(received, argument) {
function stripped(str) {
return str.replace(/\s/g, '').replace(/;/g, '')
}

const options = {
comment: 'stripped(received) === stripped(argument)',
isNot: this.isNot,
promise: this.promise,
}

const pass = stripped(received) === stripped(argument)

const message = pass
? () => {
return (
this.utils.matcherHint('toMatchCss', undefined, undefined, options) +
'\n\n' +
`Expected: not ${this.utils.printExpected(format(received))}\n` +
`Received: ${this.utils.printReceived(format(argument))}`
)
}
: () => {
const actual = format(received)
const expected = format(argument)

const diffString = diff(expected, actual, {
expand: this.expand,
})

return (
this.utils.matcherHint('toMatchCss', undefined, undefined, options) +
'\n\n' +
(diffString && diffString.includes('- Expect')
? `Difference:\n\n${diffString}`
: `Expected: ${this.utils.printExpected(expected)}\n` +
`Received: ${this.utils.printReceived(actual)}`)
)
}

return { actual: received, message, pass }
},
toIncludeCss(received, argument) {
const options = {
comment: 'stripped(received).includes(stripped(argument))',
isNot: this.isNot,
promise: this.promise,
}

const actual = format(received)
const expected = format(argument)

const pass = actual.includes(expected)

const message = pass
? () => {
return (
this.utils.matcherHint('toIncludeCss', undefined, undefined, options) +
'\n\n' +
`Expected: not ${this.utils.printExpected(format(received))}\n` +
`Received: ${this.utils.printReceived(format(argument))}`
)
}
: () => {
const diffString = diff(expected, actual, {
expand: this.expand,
})

return (
this.utils.matcherHint('toIncludeCss', undefined, undefined, options) +
'\n\n' +
(diffString && diffString.includes('- Expect')
? `Difference:\n\n${diffString}`
: `Expected: ${this.utils.printExpected(expected)}\n` +
`Received: ${this.utils.printReceived(actual)}`)
)
}

return { actual: received, message, pass }
},
})

expect.extend({
// Compare two CSS strings with all whitespace removed
// This is probably naive but it's fast and works well enough.
toMatchFormattedCss(received, argument) {
function format(input) {
return prettier.format(input.replace(/\n/g, ''), {
parser: 'css',
printWidth: 100,
})
}
const options = {
comment: 'stripped(received) === stripped(argument)',
isNot: this.isNot,
promise: this.promise,
}

let formattedReceived = format(received)
let formattedArgument = format(argument)

const pass = formattedReceived === formattedArgument

const message = pass
? () => {
return (
this.utils.matcherHint('toMatchCss', undefined, undefined, options) +
'\n\n' +
`Expected: not ${this.utils.printExpected(formattedReceived)}\n` +
`Received: ${this.utils.printReceived(formattedArgument)}`
)
}
: () => {
const actual = formattedReceived
const expected = formattedArgument

const diffString = diff(expected, actual, {
expand: this.expand,
})

return (
this.utils.matcherHint('toMatchCss', undefined, undefined, options) +
'\n\n' +
(diffString && diffString.includes('- Expect')
? `Difference:\n\n${diffString}`
: `Expected: ${this.utils.printExpected(expected)}\n` +
`Received: ${this.utils.printReceived(actual)}`)
)
}

return { actual: received, message, pass }
},
})
Loading

0 comments on commit 233e7c8

Please sign in to comment.