├── .github └── assets │ └── vta.gif ├── README.md ├── animation-registry.js ├── animations.txt ├── assets ├── custom-svg.svg ├── favicon.svg └── og.png ├── index.html ├── prism.css ├── script.js └── style.css /.github/assets/vta.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rudrodip/theme-toggle-effect/df45f3602d53572b2418735bf148c4c7821e2e43/.github/assets/vta.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Theme toggle effect 2 | 3 | Here's how we can create theme toggle effect using view transitions api 4 | 5 | This is literally the two lines of js you need 6 | 7 | ```js 8 | if (!document.startViewTransition) switchTheme() 9 | document.startViewTransition(switchTheme); 10 | ``` 11 | 12 | Then you can write your css as you wish to 13 | 14 | For example 15 | 16 | ```css 17 | ::view-transition-group(root) { 18 | animation-timing-function: var(--expo-out); 19 | } 20 | 21 | ::view-transition-new(root) { 22 | mask: url('data:image/svg+xml,') center / 0 no-repeat; 23 | animation: scale 1s; 24 | } 25 | 26 | ::view-transition-old(root), 27 | .dark::view-transition-old(root) { 28 | animation: none; 29 | z-index: -1; 30 | } 31 | .dark::view-transition-new(root) { 32 | animation: scale 1s; 33 | } 34 | 35 | @keyframes scale { 36 | to { 37 | mask-size: 200vmax; 38 | } 39 | } 40 | ``` 41 | 42 | This will create a nice circular transition effect when you switch themes. 43 | ![theme-toggle](.github/assets/vta.gif) 44 | 45 | For more examples, visit [theme-toggle.rdsx.dev](https://theme-toggle.rdsx.dev) 46 | 47 | Don't forget to star the repo if you like it 48 | 49 | Follow me on [x (twitter)](https://x.com/rds_agi) & [github](https://github.com/rudrodip) 50 | -------------------------------------------------------------------------------- /animation-registry.js: -------------------------------------------------------------------------------- 1 | const ANIMATIONS = [ 2 | { 3 | name: "circle", 4 | css: ` 5 | ::view-transition-group(root) { 6 | animation-timing-function: var(--expo-out); 7 | } 8 | ::view-transition-old(root), .dark::view-transition-old(root) { 9 | animation: none; 10 | z-index: -1; 11 | } 12 | ::view-transition-new(root) { 13 | mask: url('data:image/svg+xml,') center / 0 no-repeat; 14 | animation: scale 1s; 15 | } 16 | @keyframes scale { 17 | to { 18 | mask-size: 200vmax; 19 | } 20 | } 21 | `, 22 | }, 23 | { 24 | name: "circle-with-blur", 25 | css: ` 26 | ::view-transition-group(root) { 27 | animation-timing-function: var(--expo-out); 28 | } 29 | ::view-transition-new(root) { 30 | mask: url('data:image/svg+xml,') center / 0 no-repeat; 31 | animation: scale 1s; 32 | } 33 | ::view-transition-old(root), 34 | .dark::view-transition-old(root) { 35 | animation: none; 36 | z-index: -1; 37 | } 38 | .dark::view-transition-new(root) { 39 | animation: scale 1s; 40 | } 41 | @keyframes scale { 42 | to { 43 | mask-size: 200vmax; 44 | } 45 | } 46 | `, 47 | }, 48 | { 49 | name: "circle-blur-top-left", 50 | css: ` 51 | ::view-transition-group(root) { 52 | animation-timing-function: var(--expo-out); 53 | } 54 | ::view-transition-new(root) { 55 | mask: url('data:image/svg+xml,') top left / 0 no-repeat; 56 | mask-origin: content-box; 57 | animation: scale 1s; 58 | transform-origin: top left; 59 | } 60 | ::view-transition-old(root), 61 | .dark::view-transition-old(root) { 62 | animation: scale 1s; 63 | transform-origin: top left; 64 | z-index: -1; 65 | } 66 | @keyframes scale { 67 | to { 68 | mask-size: 350vmax; 69 | } 70 | } 71 | `, 72 | }, 73 | { 74 | name: "polygon", 75 | css: ` 76 | ::view-transition-group(root) { 77 | animation-duration: 0.7s; 78 | animation-timing-function: var(--expo-out); 79 | } 80 | ::view-transition-new(root) { 81 | animation-name: reveal-light; 82 | } 83 | ::view-transition-old(root), 84 | .dark::view-transition-old(root) { 85 | animation: none; 86 | z-index: -1; 87 | } 88 | .dark::view-transition-new(root) { 89 | animation-name: reveal-dark; 90 | } 91 | @keyframes reveal-dark { 92 | from { 93 | clip-path: polygon(50% -71%, -50% 71%, -50% 71%, 50% -71%); 94 | } 95 | to { 96 | clip-path: polygon(50% -71%, -50% 71%, 50% 171%, 171% 50%); 97 | } 98 | } 99 | @keyframes reveal-light { 100 | from { 101 | clip-path: polygon(171% 50%, 50% 171%, 50% 171%, 171% 50%); 102 | } 103 | to { 104 | clip-path: polygon(171% 50%, 50% 171%, -50% 71%, 50% -71%); 105 | } 106 | } 107 | `, 108 | }, 109 | { 110 | name: "polygon-gradient", 111 | css: ` 112 | ::view-transition-group(root) { 113 | animation-timing-function: var(--expo-out); 114 | } 115 | ::view-transition-new(root) { 116 | mask: url('assets/custom-svg.svg') top left / 0 no-repeat; 117 | mask-origin: top left; 118 | animation: scale 1.5s; 119 | } 120 | ::view-transition-old(root), 121 | .dark::view-transition-old(root) { 122 | animation: scale 1.5s; 123 | z-index: -1; 124 | transform-origin: top left; 125 | } 126 | @keyframes scale { 127 | to { 128 | mask-size: 200vmax; 129 | } 130 | } 131 | `, 132 | }, 133 | { 134 | name: "gif-1", 135 | css: ` 136 | ::view-transition-group(root) { 137 | animation-timing-function: var(--expo-in); 138 | } 139 | ::view-transition-new(root) { 140 | mask: url('https://media.tenor.com/cyORI7kwShQAAAAi/shigure-ui-dance.gif') center / 0 no-repeat; 141 | animation: scale 3s; 142 | } 143 | ::view-transition-old(root), 144 | .dark::view-transition-old(root) { 145 | animation: scale 3s; 146 | } 147 | @keyframes scale { 148 | 0% { 149 | mask-size: 0; 150 | } 151 | 10% { 152 | mask-size: 50vmax; 153 | } 154 | 90% { 155 | mask-size: 50vmax; 156 | } 157 | 100% { 158 | mask-size: 2000vmax; 159 | } 160 | } 161 | `, 162 | }, 163 | { 164 | name: "gif-2", 165 | css: ` 166 | ::view-transition-group(root) { 167 | animation-timing-function: var(--expo-in); 168 | } 169 | ::view-transition-new(root) { 170 | mask: url('https://media.tenor.com/Jz0aSpk9VIQAAAAi/i-love-you-love.gif') center / 0 no-repeat; 171 | animation: scale 2s; 172 | } 173 | ::view-transition-old(root), 174 | .dark::view-transition-old(root) { 175 | animation: scale 2s; 176 | } 177 | @keyframes scale { 178 | 0% { 179 | mask-size: 0; 180 | } 181 | 10% { 182 | mask-size: 50vmax; 183 | } 184 | 90% { 185 | mask-size: 50vmax; 186 | } 187 | 100% { 188 | mask-size: 2000vmax; 189 | } 190 | } 191 | `, 192 | }, 193 | ]; -------------------------------------------------------------------------------- /animations.txt: -------------------------------------------------------------------------------- 1 | /* circle */ 2 | 3 | ::view-transition-group(root) { 4 | animation-timing-function: var(--expo-out); 5 | } 6 | ::view-transition-old(root), .dark::view-transition-old(root) { 7 | animation: none; 8 | z-index: -1; 9 | } 10 | ::view-transition-new(root) { 11 | mask: url('data:image/svg+xml,') center / 0 no-repeat; 12 | animation: scale 1s; 13 | } 14 | @keyframes scale { 15 | to { 16 | mask-size: 200vmax; 17 | } 18 | } 19 | 20 | 21 | /* circle-with-blur */ 22 | 23 | ::view-transition-group(root) { 24 | animation-timing-function: var(--expo-out); 25 | } 26 | ::view-transition-new(root) { 27 | mask: url('data:image/svg+xml,') center / 0 no-repeat; 28 | animation: scale 1s; 29 | } 30 | ::view-transition-old(root), 31 | .dark::view-transition-old(root) { 32 | animation: none; 33 | z-index: -1; 34 | } 35 | .dark::view-transition-new(root) { 36 | animation: scale 1s; 37 | } 38 | @keyframes scale { 39 | to { 40 | mask-size: 200vmax; 41 | } 42 | } 43 | 44 | 45 | /* circle-blur-top-left */ 46 | 47 | ::view-transition-group(root) { 48 | animation-timing-function: var(--expo-out); 49 | } 50 | ::view-transition-new(root) { 51 | mask: url('data:image/svg+xml,') top left / 0 no-repeat; 52 | mask-origin: content-box; 53 | animation: scale 1s; 54 | transform-origin: top left; 55 | } 56 | ::view-transition-old(root), 57 | .dark::view-transition-old(root) { 58 | animation: scale 1s; 59 | transform-origin: top left; 60 | z-index: -1; 61 | } 62 | @keyframes scale { 63 | to { 64 | mask-size: 350vmax; 65 | } 66 | } 67 | 68 | 69 | /* polygon */ 70 | 71 | ::view-transition-group(root) { 72 | animation-duration: 0.7s; 73 | animation-timing-function: var(--expo-out); 74 | } 75 | ::view-transition-new(root) { 76 | animation-name: reveal-light; 77 | } 78 | ::view-transition-old(root), 79 | .dark::view-transition-old(root) { 80 | animation: none; 81 | z-index: -1; 82 | } 83 | .dark::view-transition-new(root) { 84 | animation-name: reveal-dark; 85 | } 86 | @keyframes reveal-dark { 87 | from { 88 | clip-path: polygon(50% -71%, -50% 71%, -50% 71%, 50% -71%); 89 | } 90 | to { 91 | clip-path: polygon(50% -71%, -50% 71%, 50% 171%, 171% 50%); 92 | } 93 | } 94 | @keyframes reveal-light { 95 | from { 96 | clip-path: polygon(171% 50%, 50% 171%, 50% 171%, 171% 50%); 97 | } 98 | to { 99 | clip-path: polygon(171% 50%, 50% 171%, -50% 71%, 50% -71%); 100 | } 101 | } 102 | 103 | 104 | /* polygon-gradient */ 105 | 106 | ::view-transition-group(root) { 107 | animation-timing-function: var(--expo-out); 108 | } 109 | ::view-transition-new(root) { 110 | mask: url('assets/custom-svg.svg') top left / 0 no-repeat; 111 | mask-origin: top left; 112 | animation: scale 1.5s; 113 | } 114 | ::view-transition-old(root), 115 | .dark::view-transition-old(root) { 116 | animation: scale 1.5s; 117 | z-index: -1; 118 | transform-origin: top left; 119 | } 120 | @keyframes scale { 121 | to { 122 | mask-size: 200vmax; 123 | } 124 | } 125 | 126 | 127 | /* gif-1 */ 128 | 129 | ::view-transition-group(root) { 130 | animation-timing-function: var(--expo-in); 131 | } 132 | ::view-transition-new(root) { 133 | mask: url('https://media.tenor.com/cyORI7kwShQAAAAi/shigure-ui-dance.gif') center / 0 no-repeat; 134 | animation: scale 3s; 135 | } 136 | ::view-transition-old(root), 137 | .dark::view-transition-old(root) { 138 | animation: scale 3s; 139 | } 140 | @keyframes scale { 141 | 0% { 142 | mask-size: 0; 143 | } 144 | 10% { 145 | mask-size: 50vmax; 146 | } 147 | 90% { 148 | mask-size: 50vmax; 149 | } 150 | 100% { 151 | mask-size: 2000vmax; 152 | } 153 | } 154 | 155 | 156 | /* gif-2 */ 157 | 158 | ::view-transition-group(root) { 159 | animation-timing-function: var(--expo-in); 160 | } 161 | ::view-transition-new(root) { 162 | mask: url('https://media.tenor.com/Jz0aSpk9VIQAAAAi/i-love-you-love.gif') center / 0 no-repeat; 163 | animation: scale 2s; 164 | } 165 | ::view-transition-old(root), 166 | .dark::view-transition-old(root) { 167 | animation: scale 2s; 168 | } 169 | @keyframes scale { 170 | 0% { 171 | mask-size: 0; 172 | } 173 | 10% { 174 | mask-size: 50vmax; 175 | } 176 | 90% { 177 | mask-size: 50vmax; 178 | } 179 | 100% { 180 | mask-size: 2000vmax; 181 | } 182 | } 183 | 184 | /* animation timing functions */ 185 | :root { 186 | --expo-in: linear( 187 | 0 0%, 0.0085 31.26%, 0.0167 40.94%, 188 | 0.0289 48.86%, 0.0471 55.92%, 189 | 0.0717 61.99%, 0.1038 67.32%, 190 | 0.1443 72.07%, 0.1989 76.7%, 191 | 0.2659 80.89%, 0.3465 84.71%, 192 | 0.4419 88.22%, 0.554 91.48%, 193 | 0.6835 94.51%, 0.8316 97.34%, 1 100% 194 | ); 195 | --expo-out: linear( 196 | 0 0%, 0.1684 2.66%, 0.3165 5.49%, 197 | 0.446 8.52%, 0.5581 11.78%, 198 | 0.6535 15.29%, 0.7341 19.11%, 199 | 0.8011 23.3%, 0.8557 27.93%, 200 | 0.8962 32.68%, 0.9283 38.01%, 201 | 0.9529 44.08%, 0.9711 51.14%, 202 | 0.9833 59.06%, 0.9915 68.74%, 1 100% 203 | ); 204 | } 205 | -------------------------------------------------------------------------------- /assets/custom-svg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 11 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /assets/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rudrodip/theme-toggle-effect/df45f3602d53572b2418735bf148c4c7821e2e43/assets/og.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 14 | Theme toggle - view transition api 15 | 16 | 20 | 24 | 25 | 26 | 27 | 28 | 32 | 36 | 37 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 48 |

Theme toggle effect

49 |

50 | using 51 | 56 | view transitions api 57 | 58 |

59 |
68 | 73 | github 74 | 75 | 76 | x (twitter) 77 | 78 |
79 | click here to get all the css written for the following examples 82 |
90 |

91 | this is literally all the 92 | javascript you need 93 |

94 |
 95 |       if (!document.startViewTransition) switchTheme()
 96 | document.startViewTransition(switchTheme);
 97 |     
98 |
99 |

Here's some demos

100 |
110 |

Now you can write your css as you wish to

111 |
119 |

following is a simple example, that uses a circular mask

120 | 128 |
129 |
130 |       
151 |     
152 |
160 |

lets add a little blur to the svg

161 | 169 |
170 |
171 |       
195 |     
196 |
204 |

let's try to pivot the center of the circle to top left

205 | 213 |
214 |
215 |       
239 |     
240 |

241 | see this is simple, now the skylimit is your imagination 242 |

243 |
251 |

252 | we've seen all the svg mask animations, but we can use clip-paths too 253 |

254 | 262 |
263 |
264 |       
300 |     
301 |

302 | the issue with using clip path is that you can't do much with it, like 303 | adding gradient or blur. so svg should be a good choice for most cases 304 |

305 |
306 |

307 | lets see how can we improve the clip-path animation with a custom svg with 308 | linear gradient 309 |

310 |
318 |

we can use local assets too

319 | 327 |
328 |
329 |       
352 |     
353 |

here's the cool part

354 |
362 |

you can use gifs too

363 | 371 |
372 |
373 |       
402 |     
403 |
411 |

this one's good 😉

412 | 420 |
421 |
422 |       
451 |     
452 |

453 | here are the two animation timing functions i'm using for the examples 454 |

455 |
456 |       
477 |     
478 |

479 | thats basically it. you have enough context to build cool theme 480 | transitions with view transitions api 481 |

482 |
483 | 488 | github 489 | 490 | 491 | x (twitter) 492 | 493 |
494 |
503 |
504 |

Used by

505 | 516 | 521 |
522 |
523 | 535 | 540 |
541 |
542 | 561 | 566 |
567 |
568 | 589 | 594 |
595 |
596 | 610 | 615 |
616 |
617 | 631 | 636 |
637 |
638 | 652 | 657 |
658 |
659 | 660 | 661 | 662 | 663 | -------------------------------------------------------------------------------- /prism.css: -------------------------------------------------------------------------------- 1 | /* Prism Code */ 2 | 3 | /* PrismJS 1.23.0 4 | https://prismjs.com/download.html#themes=prism-tomorrow&languages=css+css-extras&plugins=line-numbers+inline-color+toolbar+copy-to-clipboard */ 5 | /** 6 | * prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML 7 | * Based on https://github.com/chriskempson/tomorrow-theme 8 | * @author Rose Pritchard 9 | */ 10 | 11 | .dark code[class*="language-"], 12 | .dark pre[class*="language-"] { 13 | color: #ccc; 14 | } 15 | 16 | code[class*="language-"], 17 | pre[class*="language-"] { 18 | color: hsl(0 0% 20%); 19 | background: none; 20 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 21 | font-size: 0.8rem; 22 | text-align: left; 23 | white-space: pre; 24 | word-spacing: normal; 25 | word-break: normal; 26 | word-wrap: normal; 27 | line-height: 1.5; 28 | 29 | -moz-tab-size: 4; 30 | -o-tab-size: 4; 31 | tab-size: 4; 32 | 33 | -webkit-hyphens: none; 34 | -moz-hyphens: none; 35 | -ms-hyphens: none; 36 | hyphens: none; 37 | 38 | } 39 | 40 | /* Code blocks */ 41 | pre[class*="language-"] { 42 | padding: 1rem; 43 | margin: 1rem 0rem; 44 | overflow: auto; 45 | outline: transparent; 46 | } 47 | 48 | :not(pre) > code[class*="language-"], 49 | pre[class*="language-"] { 50 | background: hsl(0, 0%, 100%); 51 | } 52 | 53 | .dark :not(pre) > code[class*="language-"], 54 | .dark pre[class*="language-"] { 55 | background: #2d2d2d; 56 | } 57 | 58 | /* Inline code */ 59 | :not(pre) > code[class*="language-"] { 60 | padding: .1em; 61 | white-space: normal; 62 | } 63 | 64 | pre { 65 | height: 100%; 66 | display: flex; 67 | flex-direction: column; 68 | border: 1px solid var(--border); 69 | } 70 | 71 | .token.comment, 72 | .token.block-comment, 73 | .token.prolog, 74 | .token.doctype, 75 | .token.cdata { 76 | color: #999; 77 | } 78 | 79 | .dark .token.punctuation { 80 | color: #ccc; 81 | } 82 | 83 | .token-punctuation { 84 | color: red; 85 | } 86 | 87 | .token.tag, 88 | .token.attr-name, 89 | .token.namespace, 90 | .token.deleted { 91 | color: #e2777a; 92 | } 93 | 94 | .token.function-name { 95 | color: #6196cc; 96 | } 97 | 98 | .token.boolean, 99 | .token.number, 100 | .token.function { 101 | color: hsl(10 100% 50%); 102 | } 103 | .dark .token.boolean, 104 | .dark .token.number, 105 | .dark .token.function { 106 | color: hsl(20 100% 70%); 107 | } 108 | 109 | .token.property, 110 | .token.class-name, 111 | .token.constant, 112 | .token.symbol { 113 | color: #f8c555; 114 | } 115 | 116 | .token.selector, 117 | .token.important, 118 | .token.atrule, 119 | .token.keyword, 120 | .token.builtin { 121 | color: hsl(280 80% 50%); 122 | } 123 | 124 | .dark .token.selector, 125 | .dark .token.important, 126 | .dark .token.atrule, 127 | .dark .token.keyword, 128 | .dark .token.builtin { 129 | color: hsl(280 80% 80%); 130 | } 131 | 132 | .token.string, 133 | .token.char, 134 | .token.attr-value, 135 | .token.regex, 136 | .token.variable { 137 | color: #7ec699; 138 | } 139 | 140 | .token.operator, 141 | .token.entity, 142 | .token.url { 143 | color: hsl(140 100% 30%); 144 | } 145 | 146 | .dark .token.operator, 147 | .dark .token.entity, 148 | .dark .token.url { 149 | color: hsl(140 100% 80%); 150 | } 151 | 152 | .token.important, 153 | .token.bold { 154 | font-weight: bold; 155 | } 156 | .token.italic { 157 | font-style: italic; 158 | } 159 | 160 | .token.entity { 161 | cursor: help; 162 | } 163 | 164 | .token.inserted { 165 | color: green; 166 | } 167 | 168 | pre[class*="language-"].line-numbers { 169 | position: relative; 170 | padding-left: 3.8em; 171 | counter-reset: linenumber; 172 | } 173 | 174 | pre[class*="language-"].line-numbers > code { 175 | position: relative; 176 | white-space: inherit; 177 | } 178 | 179 | .line-numbers .line-numbers-rows { 180 | position: absolute; 181 | pointer-events: none; 182 | top: 0; 183 | font-size: 100%; 184 | left: -3.8em; 185 | width: 3em; /* works for line-numbers below 1000 lines */ 186 | letter-spacing: -1px; 187 | border-right: 1px solid #999; 188 | 189 | -webkit-user-select: none; 190 | -moz-user-select: none; 191 | -ms-user-select: none; 192 | user-select: none; 193 | 194 | } 195 | 196 | .line-numbers-rows > span { 197 | display: block; 198 | counter-increment: linenumber; 199 | } 200 | 201 | .line-numbers-rows > span:before { 202 | content: counter(linenumber); 203 | color: #999; 204 | display: block; 205 | padding-right: 0.8em; 206 | text-align: right; 207 | } 208 | 209 | span.inline-color-wrapper { 210 | /* 211 | * The background image is the following SVG inline in base 64: 212 | * 213 | * 214 | * 215 | * 216 | * 217 | * 218 | * SVG-inlining explained: 219 | * https://stackoverflow.com/a/21626701/7595472 220 | */ 221 | background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyIDIiPjxwYXRoIGZpbGw9ImdyYXkiIGQ9Ik0wIDBoMnYySDB6Ii8+PHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0wIDBoMXYxSDB6TTEgMWgxdjFIMXoiLz48L3N2Zz4="); 222 | /* This is to prevent visual glitches where one pixel from the repeating pattern could be seen. */ 223 | background-position: center; 224 | background-size: 110%; 225 | 226 | display: inline-block; 227 | height: 1.333ch; 228 | width: 1.333ch; 229 | margin: 0 .333ch; 230 | box-sizing: border-box; 231 | border: 1px solid white; 232 | outline: 1px solid rgba(0,0,0,.5); 233 | overflow: hidden; 234 | } 235 | 236 | span.inline-color { 237 | display: block; 238 | /* To prevent visual glitches again */ 239 | height: 120%; 240 | width: 120%; 241 | } 242 | 243 | div.code-toolbar { 244 | height: 100%; 245 | position: relative; 246 | } 247 | 248 | div.code-toolbar > .toolbar { 249 | position: absolute; 250 | top: .3em; 251 | right: .2em; 252 | opacity: 1; 253 | } 254 | 255 | div.code-toolbar:hover > .toolbar { 256 | opacity: 1; 257 | } 258 | 259 | /* Separate line b/c rules are thrown out if selector is invalid. 260 | IE11 and old Edge versions don't support :focus-within. */ 261 | div.code-toolbar:focus-within > .toolbar { 262 | opacity: 1; 263 | } 264 | 265 | div.code-toolbar > .toolbar .toolbar-item { 266 | display: inline-block; 267 | } 268 | 269 | div.code-toolbar > .toolbar a { 270 | cursor: pointer; 271 | } 272 | 273 | div.code-toolbar > .toolbar button { 274 | background: none; 275 | border: 0; 276 | color: inherit; 277 | font: inherit; 278 | line-height: normal; 279 | overflow: visible; 280 | padding: 0; 281 | -webkit-user-select: none; /* for button */ 282 | -moz-user-select: none; 283 | -ms-user-select: none; 284 | } 285 | 286 | div.code-toolbar > .toolbar a, 287 | div.code-toolbar > .toolbar button { 288 | color: #bbb; 289 | font-size: 1rem; 290 | padding: 0.5rem; 291 | font-family: sans-serif; 292 | background: hsl(0, 0%, 25%); 293 | outline: transparent; 294 | cursor: pointer; 295 | } 296 | 297 | div.code-toolbar > .toolbar a:hover, 298 | div.code-toolbar > .toolbar a:focus, 299 | div.code-toolbar > .toolbar button:hover, 300 | div.code-toolbar > .toolbar button:focus, 301 | div.code-toolbar > .toolbar span:hover, 302 | div.code-toolbar > .toolbar span:focus { 303 | background: hsl(0, 0%, 40%); 304 | text-decoration: none; 305 | } -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | import Prism from 'https://cdn.skypack.dev/prismjs' 2 | 3 | let styleElement = document.createElement('style'); 4 | document.head.appendChild(styleElement); 5 | 6 | let activeButton = null; 7 | let currentTheme = 'light'; // Assuming 'light' is the default theme 8 | 9 | const injectCSS = (css) => { 10 | styleElement.textContent = css; 11 | }; 12 | 13 | const SWITCH = (button, animation) => { 14 | const newTheme = currentTheme === 'light' ? 'dark' : 'light'; 15 | button.setAttribute("aria-pressed", newTheme === 'dark'); 16 | document.documentElement.className = newTheme; 17 | currentTheme = newTheme; 18 | injectCSS(animation.css); 19 | }; 20 | 21 | const updateButtonStates = () => { 22 | document.querySelectorAll('.theme-toggle').forEach(btn => { 23 | if (btn === activeButton) { 24 | btn.disabled = false; 25 | btn.setAttribute("aria-pressed", currentTheme === 'dark'); 26 | } else { 27 | btn.disabled = currentTheme === 'dark'; 28 | btn.setAttribute("aria-pressed", "false"); 29 | } 30 | }); 31 | }; 32 | 33 | const TOGGLE_THEME = (button, animation) => { 34 | if (activeButton && activeButton !== button) { 35 | return; // If there's an active button and it's not this one, do nothing 36 | } 37 | 38 | if (!document.startViewTransition) { 39 | SWITCH(button, animation); 40 | activeButton = currentTheme === 'dark' ? button : null; 41 | updateButtonStates(); 42 | } else { 43 | const transition = document.startViewTransition(() => { 44 | SWITCH(button, animation); 45 | activeButton = currentTheme === 'dark' ? button : null; 46 | }); 47 | transition.finished.then(() => { 48 | updateButtonStates(); 49 | }); 50 | } 51 | }; 52 | 53 | const getAnimationByName = (name) => { 54 | return ANIMATIONS.find(animation => animation.name === name); 55 | }; 56 | 57 | // Use event delegation on the document body 58 | document.body.addEventListener('click', (event) => { 59 | if (event.target.classList.contains('theme-toggle') && !event.target.disabled) { 60 | const animationName = event.target.dataset.animation; 61 | const animation = getAnimationByName(animationName); 62 | 63 | if (animation) { 64 | TOGGLE_THEME(event.target, animation); 65 | } else { 66 | console.warn(`Animation "${animationName}" not found for button:`, event.target); 67 | } 68 | } 69 | }); 70 | 71 | // demo containers 72 | const DEMO_CONTAINER = document.getElementById("demo-container"); 73 | 74 | ANIMATIONS.forEach((animation) => { 75 | const button = document.createElement("button"); 76 | button.setAttribute("aria-pressed", "false"); 77 | button.className = "theme-toggle"; 78 | button.dataset.animation = animation.name; 79 | button.textContent = animation.name; 80 | DEMO_CONTAINER.appendChild(button); 81 | }); 82 | 83 | // Initial button state setup 84 | updateButtonStates(); -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | body { 8 | padding: 0rem 0.3rem; 9 | min-height: 100vh; 10 | width: 100%; 11 | max-width: 42rem; 12 | margin: auto; 13 | color: var(--color); 14 | background: var(--bg); 15 | font-family: "Manrope", sans-serif; 16 | font-optical-sizing: auto; 17 | } 18 | 19 | .sr-only { 20 | position: absolute; 21 | width: 1px; 22 | height: 1px; 23 | padding: 0; 24 | margin: -1px; 25 | overflow: hidden; 26 | clip: rect(0, 0, 0, 0); 27 | white-space: nowrap; 28 | border-width: 0; 29 | } 30 | 31 | :root { 32 | --color: hsl(0 0% 6%); 33 | --bg: hsl(0 0% 98%); 34 | --border: hsl(0, 0%, 22%); 35 | --expo-out: linear( 36 | 0 0%, 37 | 0.1684 2.66%, 38 | 0.3165 5.49%, 39 | 0.446 8.52%, 40 | 0.5581 11.78%, 41 | 0.6535 15.29%, 42 | 0.7341 19.11%, 43 | 0.8011 23.3%, 44 | 0.8557 27.93%, 45 | 0.8962 32.68%, 46 | 0.9283 38.01%, 47 | 0.9529 44.08%, 48 | 0.9711 51.14%, 49 | 0.9833 59.06%, 50 | 0.9915 68.74%, 51 | 1 100% 52 | ); 53 | --expo-in: linear( 54 | 0 0%, 55 | 0.0085 31.26%, 56 | 0.0167 40.94%, 57 | 0.0289 48.86%, 58 | 0.0471 55.92%, 59 | 0.0717 61.99%, 60 | 0.1038 67.32%, 61 | 0.1443 72.07%, 62 | 0.1989 76.7%, 63 | 0.2659 80.89%, 64 | 0.3465 84.71%, 65 | 0.4419 88.22%, 66 | 0.554 91.48%, 67 | 0.6835 94.51%, 68 | 0.8316 97.34%, 69 | 1 100% 70 | ); 71 | } 72 | 73 | h1 { 74 | letter-spacing: -0.05em; 75 | font-size: clamp(2rem, 4vw + 1rem, 4rem); 76 | line-height: 1; 77 | font-weight: bolder; 78 | } 79 | 80 | pre { 81 | max-width: 100%; 82 | } 83 | 84 | p { 85 | width: 60ch; 86 | max-width: 100%; 87 | } 88 | 89 | a { 90 | color: var(--color); 91 | text-decoration: none; 92 | border-bottom: 1px solid var(--color); 93 | } 94 | 95 | a:is(:hover, :focus-visible) { 96 | border-bottom: 2px solid rgb(0, 140, 255); 97 | } 98 | 99 | .dark a:is(:hover, :focus-visible) { 100 | border-bottom: 2px solid rgb(0, 183, 255); 101 | } 102 | 103 | .dark { 104 | --color: hsl(0 0% 98%); 105 | --bg: hsl(0 0% 6%); 106 | --border: hsl(0, 0%, 52%); 107 | } 108 | 109 | .theme-toggle { 110 | color: var(--color); 111 | padding: 5px 10px; 112 | background-color: var(--bg); 113 | border: 1px solid var(--border); 114 | display: grid; 115 | place-items: center; 116 | background: transparent; 117 | transition: background 0.2s; 118 | cursor: pointer; 119 | z-index: 10; 120 | } 121 | 122 | .theme-toggle:is(:hover, :focus-visible) { 123 | background: hsl(0 0% 90%); 124 | } 125 | 126 | .dark .theme-toggle:is(:hover, :focus-visible) { 127 | background: hsl(0 0% 30%); 128 | } 129 | 130 | .theme-toggle:disabled { 131 | opacity: 0.5; 132 | cursor: not-allowed; 133 | } 134 | 135 | .tweet-container { 136 | flex: 1; 137 | margin: 10px; 138 | display: flex; 139 | flex-direction: column; 140 | justify-content: space-between; 141 | align-items: center; 142 | } 143 | .tweets-wrapper { 144 | display: flex; 145 | flex-wrap: wrap; 146 | justify-content: center; 147 | align-items: center; 148 | } 149 | .twitter-tweet { 150 | flex: 1; 151 | } --------------------------------------------------------------------------------