CSS effects collection
TOC
- TOC
- Rainbow Artword
- Hover Text Effects
- Apple-style OS dock
- 3D Flip Hover Effects
- Color Palettes
- 3D Clock
- Animation with View Transitions
- Filter and backdrop filter
- Scroll-driven animations
- Reveal hover effect
- Gradient border card
- The Periodic Table
- Double input range slider
Rainbow Artword
<style>
.wordart {
display: inline-block;
background: linear-gradient(
90deg,
#ff0000,
#ff8800,
#ffff00,
#02be02,
#0000ff,
#4f00ff,
#9c00ff
);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
font-size: 60px;
font-weight: bold;
transform: skewY(-8deg) scaleY(1.3) scaleX(0.8);
filter: drop-shadow(2px 2px 0px rgba(0, 0, 0, 0.2));
}
</style>
<span class="wordart">WordArt</span>
- Using the
background-clip
property we can control where the background shows. Specifically, we can setbackground-clip: text
to make the background only show wherever there’s text in the element. drop-shadow
is similar to thebox-shadow
property. Thebox-shadow
property creates a rectangular shadow behind an element’s entire box, while thedrop-shadow
creates a shadow that conforms to the shape (alpha channel) of the image itself.- A cool website: https://www.makewordart.com
Hover Text Effects
https://codepen.io/jh3y/pen/abGPKGO
<script src="https://unpkg.com/splitting/dist/splitting.min.js"></script>
<style>
.char {
--pop: 0;
display: inline-block;
position: relative;
color: transparent;
z-index: calc(1 + (var(--pop) * 2));
}
.char:after {
content: attr(data-char);
position: absolute;
inset: 0;
color: hsl(45 calc(var(--pop) * 100%) calc(80% - (30% * var(--pop))));
translate: 0 calc(var(--pop, 0) * -65%);
scale: calc(1 + var(--pop) * 0.75);
transition: translate 0.2s, scale 0.2s, color 0.2s;
}
.char:hover {
--pop: 1;
}
/* elements that are immediately before and after the char being hovered */
.char:hover + .char,
.char:has(+ .char:hover) {
--pop: 0.4;
}
</style>
<h1 data-splitting>Happy Birthday!</h1>
<script> Splitting(); </script>
- Splitting.js is designed to split an element in a variety of ways, such as words, characters, child nodes, and more.
- The
inset
CSS property is a shorthand that corresponds to thetop
,right
,bottom
andleft
properties. :has(+ .char:hover)
means target any character that is directly followed by a character that is hovered. It is available in Chrome 105.
Apple-style OS dock
CSS only, no JS. This one would be pretty sweet as a nav on your portfolio.
https://codepen.io/jh3y/pen/GRwwWoV
.b:has(+ .b:hover),
.b:hover + .b {
flex: calc(0.2 + (sin(30deg) * 1.5));
translate: 0 calc(sin(30deg) * -75%);
}
3D Flip Hover Effects
https://codepen.io/rikanutyy/pen/PEJBxX
<style>
.card {
color: #013243;
position: absolute;
top: 50%;
left: 50%;
width: 300px;
height: 400px;
background: #e0e1dc;
transform-style: preserve-3d;
transform: translate(-50%,-50%) perspective(2000px);
box-shadow: inset 300px 0 50px rgba(0,0,0,.5), 20px 0 60px rgba(0,0,0,.5);
transition: 1s;
}
.card:hover {
transform: translate(-50%,-50%) perspective(2000px) rotate(15deg) scale(1.2);
box-shadow: inset 20px 0 50px rgba(0,0,0,.5), 0 10px 100px rgba(0,0,0,.5);
}
.card:before {
content:'';
position: absolute;
top: -5px;
left: 0;
width: 100%;
height: 5px;
background: #BAC1BA;
transform-origin: bottom;
transform: skewX(-45deg);
}
.card:after {
content: '';
position: absolute;
top: 0;
right: -5px;
width: 5px;
height: 100%;
background: #92A29C;
transform-origin: left;
transform: skewY(-45deg);
}
.card .imgBox {
width: 100%;
height: 100%;
position: relative;
transform-origin: left;
transition: .7s;
}
.card .bark {
position: absolute;
background: #e0e1dc;
width: 100%;
height: 100%;
opacity: 0;
transition: .7s;
}
.card .imgBox img {
min-width: 250px;
max-height: 400px;
}
.card:hover .imgBox {
transform: rotateY(-135deg);
}
.card:hover .bark {
opacity: 1;
transition: .6s;
box-shadow: 300px 200px 100px rgba(0, 0, 0, .4) inset;
}
.card .details {
position: absolute;
top: 0;
left: 0;
box-sizing: border-box;
padding: 0 0 0 20px;
z-index: -1;
margin-top: 70px;
}
</style>
<div class="card">
<div class="imgBox">
<div class="bark"></div>
<img src="https://placekitten.com/300/400">
</div>
<div class="details">
<h4>HAPPY BIRTHDAY</h4>
</div>
</div>
Color Palettes
- Build a wide gamut color palette with okLCH and inspects color with devtools: https://www.youtube.com/watch?v=6aCsAMgwnjE
- Create a custom palette: https://www.radix-ui.com/colors/custom
<style>
html {
--hue: 140;
--swatch-1: oklch(99% .05 var(--hue));
--swatch-2: oklch(90% .1 var(--hue));
--swatch-3: oklch(80% .2 var(--hue));
--swatch-4: oklch(72% .25 var(--hue));
--swatch-5: oklch(67% .31 var(--hue));
--swatch-6: oklch(50% .27 var(--hue));
--swatch-7: oklch(35% .25 var(--hue));
--swatch-8: oklch(25% .2 var(--hue));
--swatch-9: oklch(13% .2 var(--hue));
--swatch-10: oklch(5% .1 var(--hue));
--text-1: var(--swatch-10);
--text-2: var(--swatch-9);
--surface-1: var(--swatch-1);
--surface-2: var(--swatch-2);
--surface-3: var(--swatch-3);
}
html {
background: var(--surface-1);
color: var(--text-1);
}
body {
display: grid;
/* justify-content && align-content */
place-content: center;
gap: 5vmin;
grid-auto-flow: column;
}
.palette {
display: grid;
grid-auto-rows: 8vh;
grid-template-columns: 20vw;
}
.swatch {
box-shadow: inset 0 0 0 1px oklch(50% 0 0 / 20%);
}
.swatch:nth-of-type(1) { background: var(--swatch-1) }
.swatch:nth-of-type(2) { background: var(--swatch-2) }
.swatch:nth-of-type(3) { background: var(--swatch-3) }
.swatch:nth-of-type(4) { background: var(--swatch-4) }
.swatch:nth-of-type(5) { background: var(--swatch-5) }
.swatch:nth-of-type(6) { background: var(--swatch-6) }
.swatch:nth-of-type(7) { background: var(--swatch-7) }
.swatch:nth-of-type(8) { background: var(--swatch-8) }
.swatch:nth-of-type(9) { background: var(--swatch-9) }
.swatch:nth-of-type(10) { background: var(--swatch-10) }
.card {
display: grid;
border-radius: 10px;
background: var(--surface-2);
border: 1px solid var(--surface-3);
padding: 1rem;
}
</style>
<body>
<div class="palette">
<div class="swatch"></div>
<div class="swatch"></div>
<div class="swatch"></div>
<div class="swatch"></div>
<div class="swatch"></div>
<div class="swatch"></div>
<div class="swatch"></div>
<div class="swatch"></div>
<div class="swatch"></div>
<div class="swatch"></div>
</div>
<article>
<div class="card">
<h2>I'm a card</h2>
<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Itaque doloremque modi veniam aspernatur voluptatum
labore dolores perspiciatis.</p>
</div>
</article>
</body>
Another way is using CSS color-mix()
, which is stable in Chrome 111. The trick for creating semi-opaque versions of the brand colors is mixing them with the transparent color value.
:root {
--brandBlue: skyblue;
--brandBlue-a10: color-mix(in srgb, var(--brandBlue), transparent 90%);
--brandBlue-a20: color-mix(in srgb, var(--brandBlue), transparent 80%);
--brandBlue-a30: color-mix(in srgb, var(--brandBlue), transparent 70%);
--brandBlue-a40: color-mix(in srgb, var(--brandBlue), transparent 60%);
--brandBlue-a50: color-mix(in srgb, var(--brandBlue), transparent 50%);
--brandBlue-a60: color-mix(in srgb, var(--brandBlue), transparent 40%);
--brandBlue-a70: color-mix(in srgb, var(--brandBlue), transparent 30%);
--brandBlue-a80: color-mix(in srgb, var(--brandBlue), transparent 20%);
--brandBlue-a90: color-mix(in srgb, var(--brandBlue), transparent 10%);
}
3D Clock
https://codepen.io/bigxixi/pen/abjEMbg
Animation with View Transitions
- Getting started with View Transitions on multi-page apps: https://daverupert.com/2023/05/getting-started-view-transitions
- Example of view transitions for multi-page sites: https://mpa-view-transitions-sandbox.netlify.app
- Adam Argyle at SeattleJS Conf: https://seattlejs-view-transitions.netlify.app
- A collection of demos to show off View Transitions: https://view-transitions.chrome.dev
When a view transition occurs between two different documents it is called a cross-document view transition. This is typically the case in multi-page applications (MPA). Chrome 126 enables Cross-Document View Transitions triggered by a same-origin navigation. From now on, you no longer need rearchitect your app to an SPA to use View Transitions.
Animation CSS grid alignments: https://codepen.io/argyleink/pen/NWOEvro
<style>
body {
display: grid;
place-content: center;
}
.box {
/* Having a name means "hold onto this element and try to tween it" (otherwise you get the cross-fade) */
view-transition-name: box; /* whatever a unique name */
width: 100px;
height: 100px;
background: blue;
}
</style>
<div class="box"></div>
<script>
const positions = ['start', 'end', 'center']
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}
function setRandomAlignments() {
document.body.style.alignContent = positions[getRandomInt(3)]
document.body.style.justifyContent = positions[getRandomInt(3)]
}
document.body.addEventListener('click', e => {
if (!document.startViewTransition)
setRandomAlignments()
else
document.startViewTransition(() => {
setRandomAlignments()
})
})
</script>
Tag selection: https://codepen.io/dannymoerkerke/pen/VYZxYdy
<div class="search"></div>
<div class="tags">
<button>Docker<span>X</span></button>
<button>Kubernetes<span>X</span></button>
<button>AWS<span>X</span></button>
</div>
<script>
const tags = document.querySelectorAll('button');
const search = document.querySelector('.search');
tags.forEach((tag, index) => {
tag.style.viewTransitionName = `tag-${index}`;
tag.style.order = index;
});
const tagsContainer = document.querySelector('.tags');
tagsContainer.addEventListener('click', (e) => {
const tag = e.target.closest('button');
if (tag) {
document.startViewTransition(() => {
search.appendChild(tag);
});
}
});
search.addEventListener('click', (e) => {
const span = e.target.closest('span');
if (span) {
const tag = span.closest('button');
document.startViewTransition(() => {
tagsContainer.appendChild(tag);
});
}
});
</script>
https://codepen.io/argyleink/pen/GRPRJyM
Filter and backdrop filter
backdrop-filter has the same effect as filter, with one notable difference — backdrop filters apply only to areas behind the element instead of to the element and its children. Filters, on the other hand, apply directly to the element and its children, and don’t affect anything behind the element.
<div class="parent">
<div class="blur">Blur</div>
<div class="invert">Invert</div>
<div class="hue">Hue</div>
<div class="grayscale">Grayscale</div>
</div>
<style>
.parent {
background-image: url("/images/neue-donau.webp");
}
.blur {
backdrop-filter: blur(5px);
}
.invert {
backdrop-filter: invert(1);
}
.hue {
backdrop-filter: hue-rotate(260deg);
}
.grayscale {
backdrop-filter: grayscale(100%);
}
</style>
Scroll-driven animations
At its simplest, the animation-timeline
property lets us link any keyframe animation to the progress of scroll. They still run from 0-100%. But now, 0% is the scroll start position and 100% is the scroll end position.
@keyframes spin {
to {
transform: rotateY(5turn);
}
}
@media (prefers-reduced-motion: no-preference) {
@supports (animation-timeline: scroll()) {
div {
animation: spin linear both;
animation-timeline: scroll();
}
}
}
Next, change scroll()
to view()
, which means we can trigger animations when elements enter and exit the viewport. This time 0% is when the element is entering the scroll area and 100% is when it’s about to go out of that scroll area.
/* Animate images: https://codepen.io/una/pen/KKYZzJM */
@keyframes appear {
from { opacity: 0; scale: 0.8; }
to { opacity: 1; scale: 1; }
}
img {
animation: appear linear both;
animation-timeline: view();
animation-range: entry 25% cover 50%;
}
Because scroll-driven animations are only active when there is scrollable overflow, it is possible to use them as a mechanism to detect if an element can scroll or not.
.container {
height: 250px;
width: 250px;
overflow-y: auto;
--can-scroll: 0;
animation: detect-scroll;
animation-timeline: scroll(self);
}
@keyframes detect-scroll {
from, to {
--can-scroll: 1;
}
}
“Unleash the Power of Scroll-Driven Animations” from Google is a 10-part video course to learn all about scroll-driven animations.
Reveal hover effect
https://codepen.io/t_afif/pen/GRYEZrr
<img src="https://picsum.photos/seed/picsum/200/200" class="left">
<img src="https://picsum.photos/seed/picsum/200/200" class="right">
<style>
img {
--s: 200px; /* the image size */
width: var(--s);
height: var(--s);
box-sizing: border-box;
object-fit: cover;
transition: .5s;
}
img.left {
object-position: right;
padding-left: var(--s);
background: #542437;
}
img.right {
object-position: left;
padding-right: var(--s);
background: #8A9B0F;
}
img:hover {
padding: 0;
}
</style>
object-fit
property is used to specify how an<img>
should be resized to fit its container.fill
is default, which means the image is resized to fill the given dimension.object-position
is used together withobject-fit
to specify how an<img>
should be positioned with x/y coordinates inside its “own content box”.box-sizing: border-box
will make the size of the content box equal to 0. In other words, we don’t see the image, but we see the background color since it covers the padding area.
Gradient border card
The two layers stack on top of each other:
- The conic-gradient layer applies over the content and padding areas.
- The linear-gradient layer applies to the border area, visible outside the padding.
.box {
width: 100px;
height: 100px;
border: solid 4px #0000;
border-radius: 16px;
/* `0 0` means no transition resulting in a solid fill of the specified color. */
background: conic-gradient(rgb(0 0 0) 0 0) padding-box,
linear-gradient(45deg, #ffbc00, #ff0058) border-box;
}
/* conic-gradient(
rgb(0 0 0 / .75) 0deg,
rgba(255, 255, 255, 0.5) 90deg,
rgba(255, 0, 0, 0.75) 180deg
); */
The Periodic Table
https://dev.to/madsstoumann/the-periodic-table-in-css-3lmm
ol {
display: grid;
gap: 1px;
grid-template-columns: repeat(18, 1fr);
grid-template-rows: repeat(10, 1fr);
}
li {
&:nth-of-type(2) { grid-column: 18; } /* pushed to the last column */
}
/* filter */
body:has(#alk:checked) li:not(.alk) {
opacity: 0.2;
}
Double input range slider
https://codepen.io/alexpg96/pen/xxrBgbP
<style>
.container {
position: relative;
width: 300px;
height: 100px;
}
.slider-track {
width: 100%;
height: 5px;
position: absolute;
margin: auto;
top: 0;
bottom: 0;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 100%;
outline: none;
position: absolute;
margin: auto;
top: 0;
bottom: 0;
background-color: transparent;
pointer-events: none;
}
input[type="range"]::-webkit-slider-runnable-track {
-webkit-appearance: none;
height: 5px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 22px;
width: 22px;
background-color: blue;
cursor: pointer;
margin-top: -8px;
pointer-events: auto;
border-radius: 50%;
}
input[type="range"]:active::-webkit-slider-thumb {
background-color: #ffffff;
border: 1px solid blue;
}
</style>
<body>
<div class="values">
<span id="range1">0</span>
<span> ‐ </span>
<span id="range2">100</span>
</div>
<div class="container">
<div class="slider-track"></div>
<input type="range" min="0" max="100" value="30" id="slider-1" oninput="slideOne()">
<input type="range" min="0" max="100" value="70" id="slider-2" oninput="slideTwo()">
</div>
<script>
window.onload = function () {
slideOne();
slideTwo();
};
let sliderOne = document.getElementById("slider-1");
let sliderTwo = document.getElementById("slider-2");
let displayValOne = document.getElementById("range1");
let displayValTwo = document.getElementById("range2");
let sliderTrack = document.querySelector(".slider-track");
let sliderMaxValue = 100;
function slideOne() {
if (parseInt(sliderTwo.value) <= parseInt(sliderOne.value)) {
sliderOne.value = parseInt(sliderTwo.value);
}
displayValOne.textContent = sliderOne.value;
fillColor();
}
function slideTwo() {
if (parseInt(sliderTwo.value) <= parseInt(sliderOne.value)) {
sliderTwo.value = parseInt(sliderOne.value);
}
displayValTwo.textContent = sliderTwo.value;
fillColor();
}
function fillColor() {
percent1 = (sliderOne.value / sliderMaxValue) * 100;
percent2 = (sliderTwo.value / sliderMaxValue) * 100;
// The color gray starts from the beginning and transitions to percent1%
// At percent1%, the color changes to blue, and the color blue continues up to percent2%
// At percent2%, the color changes back to gray
sliderTrack.style.background = `linear-gradient(to right, lightgray ${percent1}%, blue ${percent1}%, blue ${percent2}%, lightgray ${percent2}%)`;
}
</script>
</body>
</html>