SVG Background Shaking when replaced - css

I have a div with an SVG background. The div height and width do not change. The only thing that changes is the SVG file used for the background when the div is hovered.
The issue seems to be that when the SVG background is replaced (even after the asset is preloaded) the image seems to "shake" before settling into place. I've triple checked and both SVG assets are the exact same height and width and viewport, the only difference is the coloring.
For reference, the issue can be seen on this page: https://tqt.uwaterloo.ca/.
When hovering the "hamburger" icon the issue can be seen.
When hovering the "search" icon there is no issue, however the "search" icon has the exact same SVG replacement on it and it's working as expected.
Final note: This issue is only replicatable on retina (2x+) monitors.

There are two bugs here, one in Blink (Chrome and Chromium based browsers) and one in webkit, both caused by the transitioning of the background-image property.
Here is a minimal repro of these bugs:
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34 26">
<path d="M0,0h34v4H0V0z M0,22h34v3.9H0V22z M0,11h34v4H0V11z" fill="black"/>
</svg>`;
const url = 'url(data:image/svg+xml,' + encodeURIComponent( svg ) + ')';
const urls = [ url, url.replace( 'black', 'red' ) ];
let i = 0;
setInterval(() =>
document.querySelector( '.burger' ).style.backgroundImage = urls[ (++i % 2) ],
500
);
.burger {
height: 30px;
width: 100px;
background-position: center;
background-repeat: no-repeat;
transition: background-image .2s linear;
}
<div class="burger">
</div>
Chrome's bug is due to the fact your svg files don't have their own width and height attributes, setting one will fix it there.
Safari bug is caused by the retina scaling, on which they weirdly apply the transition too... For this one a fix would be harder, but setting the correct height will make it less visible since we'd only have antialiasing artifacts to move.
Here is the same snippet as above, with just the height and width attributes set:
const svg = `<svg width="34" height="26" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34 26">
<path d="M0,0h34v4H0V0z M0,22h34v3.9H0V22z M0,11h34v4H0V11z" fill="black"/>
</svg>`;
const url = 'url(data:image/svg+xml,' + encodeURIComponent( svg ) + ')';
const urls = [ url, url.replace( 'black', 'red' ) ];
let i = 0;
setInterval(() =>
document.querySelector( '.burger' ).style.backgroundImage = urls[ (++i % 2) ],
500
);
.burger {
height: 30px;
width: 100px;
background-position: center;
background-repeat: no-repeat;
transition: background-image .2s linear;
}
<div class="burger">
</div>
Now, there should probably be a note that if targeting only quite recent browsers is not an issue for you, you could achieve the same effect by using only CSS filters, saving one network request:
const svg = `<svg width="34" height="26" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34 26">
<path d="M0,0h34v4H0V0z M0,22h34v3.9H0V22z M0,11h34v4H0V11z"/>
</svg>`;
// This time we generate only a single black url
const url = 'url(data:image/svg+xml,' + encodeURIComponent( svg ) + ')';
document.querySelector( '.burger' ).style.backgroundImage = url;
.burger {
height: 30px;
width: 100px;
background-position: center;
background-repeat: no-repeat;
transition: filter .2s linear;
}
.burger:hover {
/*
Using formula from
https://stackoverflow.com/a/43959856/3702797
to safely fallback on black for browsers withtou support for filters
*/
filter: invert(10%) sepia(100%) saturate(10000%) ;
}
hover the icon to change its color.
<div class="burger">
</div>

Related

SVG line ROTATE transform animation takes a wierd path before reaching destination angle [ Works differently between laptop and extended monitors ]

I'm trying to recreate a meter in SVG.
When trying to rotate a <line> or a <path> using transform and a transition, the line / path takes a wierd route before reaching the destination angle.
Observed that it works differently between laptop and extended monitors.
How to fix this ?
CODEPEN : https://codepen.io/sparkeplug/pen/zYWZxvX
BEHAVIOR IN DIFFERENT MONITORS :
EXTENDED MONITOR BEHAVIOR :
LAPTOP MONITOR BEHAVIOR :
Im expecting the line to rotate with the lines bottom coordinates as transform origin ( like a typical meter ).
EXPECTED BEHAVIOR SAMPLE IMAGE :
Move the centre of rotation coords out of the transform and set them with a transform-origin property instead.
const btn = document.getElementById("btn");
const meter = document.getElementById("idMeter");
let flag = false;
btn.addEventListener("click", function () {
console.log("test");
const angle1 = "79";
const angle2 = "-48";
meter.setAttribute("transform", `rotate(${flag ? angle1 : angle2})`);
flag = !flag;
});
.container {
display: flex;
position: relative;
flex-direction: column;
justify-content: center;
align-items: flex-start;
height: 20rem;
width: 100%;
}
.svg {
border: 1px solid red;
}
.meter {
transition: all 0.2s ease-in-out;
transform-origin: 250px 350px;
}
<main class="container">
<svg class="svg" viewbox="0 0 500 500">
<g class="meter" id="idMeter">
<line x1="250" y1="350" x2="250" y2="100" stroke="black" stroke-width="3"></line>
</g>
</svg>
<button id="btn">Animate</button>
</main>

react-transition-group doesnt slideUp component React.js

I have a component where when o clicking the button, the div with more info will slideUp and slideDown.
Below the code and css style
import { CSSTransition } from "react-transition-group";
const Card = () => {
const [showMoreInfo, setShowMoreInfo] = useState(false);
return (
<div className="Card">
<ButtonShowMore isOpen={showMoreInfo} click={() => setShowMoreInfo(!showMoreInfo)} />
<CSSTransition in={showMoreInfo} classNames="Card-Details" timeout={1000}>
<div>
{showMoreInfo && (
<>
<p>details</p>
<p>details</p>
</>
)}
</div>
</CSSTransition>
</div>
);
};
.Card-Details-enter {
height: 0px;
}
.Card-Details-enter-active {
height: 100%;
-webkit-transition: height 1s ease;
-moz-transition: height 1s ease;
-o-transition: height 1s ease;
transition: height 1s ease;
}
.Card-Details-enter-done {
height: 100%;
}
.Card-Details-exit {
height: 100%;
}
.Card-Details-exit-active {
height: 0px;
-webkit-transition: height 1s ease;
-moz-transition: height 1s ease;
-o-transition: height 1s ease;
transition: height 1s ease;
}
.Card-Details-exit-done {
height: 0px;
}
But it doesnt work, I don't know why. I tred to put the transition to the parent element like here
and add transition to the class *-exit-done like here, and nothing helped.
The reason why it doesn't work is because transitioning on percentage height isn't quite what you would expect.
Percentage height and width in CSS refer to their parent's height and width rather than their own height.
MDN Percentage
The CSS data type represents a percentage value. It is
often used to define a size as relative to an element's parent object.
Numerous properties can use percentages, such as width, height,
margin, padding, and font-size.
Example with the same element transitioning using a percentage height/width. One has a container element with a height/width of 50px, and the other doesn't.
div.container {
width: 50px;
height: 50px;
}
div.transition {
width: 100%;
height: 100%;
background: red;
transition: width 2s, height 4s;
}
div.transition:hover {
width: 300%;
height: 500%;
}
Transition div with a 50px container
<div class="container">
<div class="transition">
<p>test</p>
</div>
</div>
Transition div without a container:
<div class="transition">
<p>test</p>
</div>
What we'd actually want is to transition from 0px to auto height. Unfortunately browsers don't support transitioning on auto height.
A good write up of this is in Using CSS Transitions Auto Dimensions include some approaches to get what you want and their downsides.
Why hasn’t this problem been fixed at the browser level?
According to the Mozilla Developer Network docs, auto values have been
intentionally excluded from the CSS transitions spec. It looks like
it’s been requested by a few people, but when you think about it, it
makes at least a little sense that it hasn’t been included. The
browser process that re-calculates the sizes and positions of all
elements based on their content and the way they interact with each
other (known as “reflow”) is expensive. If you were to transition an
element into a height of auto, the browser would have to perform a
reflow for every stage of that animation, to determine how all the
other elements should move. This couldn’t be cached or calculated in a
simple way, since it doesn’t know the starting and/or ending values
until the moment the transition happens. This would significantly
complicate the math that has to be done under the hood and probably
degrade performance in a way that might not be obvious to the
developer.
How can I transition height: 0; to height: auto; using CSS? also has some good workarounds, though there really is no magic bullet for this.
It is definitely a very well known issue, and there's a request for the spec to change to allow transitions on auto, though I don't think it's gone anywhere yet.
As for support for the type of transition you are working on in React Transition Group:
Slide Down Animation and Trying to fade out element then slide up both have the same answer overall: pointing at how React Bootstrap's Collapse component does it.
You need to rely on finding the dom node's actual height and using that as part of the transition:
getDimension() {
return typeof this.props.dimension === 'function'
? this.props.dimension()
: this.props.dimension;
}
// for testing
_getScrollDimensionValue(elem, dimension) {
return `${elem[`scroll${capitalize(dimension)}`]}px`;
}
/* -- Expanding -- */
handleEnter = (elem) => {
elem.style[this.getDimension()] = '0';
}
handleEntering = (elem) => {
const dimension = this.getDimension();
elem.style[dimension] = this._getScrollDimensionValue(elem, dimension);
}
handleEntered = (elem) => {
elem.style[this.getDimension()] = null;
}
/* -- Collapsing -- */
handleExit = (elem) => {
const dimension = this.getDimension();
elem.style[dimension] = `${this.props.getDimensionValue(dimension, elem)}px`;
triggerBrowserReflow(elem);
}
handleExiting = (elem) => {
elem.style[this.getDimension()] = '0';
}
A quick and dirty example of using the functionality from the Collapse class for a working example of the code using a less fully featured solution (note, based heavily on the Collapse.js code linked above):
const { Transition } = ReactTransitionGroup;
const { EXITED, ENTERED, ENTERING, EXITING } = Transition;
const { useState } = React;
// Quick and dirty classNames functionality
const classNames = (...names) => names.filter((name) => name).join(' ');
const ButtonShowMore = ({ isOpen, click }) => {
return <button onClick={click}>{isOpen ? 'Close' : 'Open'}</button>;
};
// Heavily based on https://github.com/react-bootstrap/react-bootstrap/blob/next/src/Collapse.js#L150
// for the purpose of demonstration without just pulling in the module:
function triggerBrowserReflow(node) {
node.offsetHeight; // eslint-disable-line no-unused-expressions
}
const collapseStyles = {
[EXITED]: 'collapse',
[EXITING]: 'collapsing',
[ENTERING]: 'collapsing',
[ENTERED]: 'collapse in',
};
const Collapse = ({ children, ...props }) => {
const handleEnter = (elem) => (elem.style.height = '0');
const handleEntering = (elem) =>
(elem.style.height = `${elem.scrollHeight}px`);
const handleEntered = (elem) => (elem.style.height = null);
const handleExit = (elem) => {
elem.style.height = `${elem.scrollHeight}px`;
triggerBrowserReflow(elem);
};
const handleExiting = (elem) => (elem.style.height = '0');
return (
<Transition
{...props}
onEnter={handleEnter}
onEntering={handleEntering}
onEntered={handleEntered}
onExit={handleExit}
onExiting={handleExiting}
>
{(state, innerProps) =>
React.cloneElement(children, {
...innerProps,
className: classNames(
props.className,
children.props.className,
collapseStyles[state]
),
})
}
</Transition>
);
};
const Card = () => {
const [showMoreInfo, setShowMoreInfo] = useState(false);
return (
<div className="Card">
<ButtonShowMore
isOpen={showMoreInfo}
click={() => setShowMoreInfo(!showMoreInfo)}
/>
<Collapse in={showMoreInfo} className="Card-Details" timeout={1000}>
<div style={{ height: 0 }}>
<p>details</p>
<p>details</p>
</div>
</Collapse>
</div>
);
};
ReactDOM.render(<Card />, document.querySelector('#root'));
.collapsing {
-webkit-transition: height 1s ease;
-moz-transition: height 1s ease;
-o-transition: height 1s ease;
transition: height 1s ease;
overflow: hidden;
}
.collapse {
overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-transition-group/4.4.1/react-transition-group.min.js"></script>
<div id="root" />

Styled components : smart Wrapper container

First of all, sorry for the ambiguous title.
I'm trying to make a spinner that has a label.
And my spinner component is like below.
const rotate360 = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`
const Circle = styled.div`
border: 5px solid ${({ theme }) => theme.color.grey};
border-radius: 50%;
border-top-color: #fff;
width: ${({ size }) => size}px;
height: ${({ size }) => size}px;
animation: ${rotate360} 1s ease-in-out infinite;
`
const Label = styled.p`
`
const Wrapper = styled.div`
// I'm not sure what to add here...
`
const Spinner = ({
size,
text
}) => {
return (
<Wrapper>
<Label>{text}</Label>
<Circle size={size} />
</Wrapper>
)
}
My question is how can I make my Wrapper components to know the width of child elements without making any Refs which is inconvenient and makes component larger.
My point is to calculate each of parent's children's width and then compare them and finally let the longest one be the parent's width.
It's like if one parent div has two children. One has a length of 100px and another has a length of 200px. Then parent's length would be 200px.
Think of my question, I realized that I don't have to calculate the width of child elements.
Instead, I just made them center aligned by the code below.
const Wrapper = styled.div`
display: block;
padding: 1em;
text-align: center;
& > ${Circle} {
margin: auto;
margin-bottom: 20px;
}
`
Lovely result, right ?

How to scale SVG path to fit the window size?

I am having troubles to scale the SVG to fit the window size.
In this example, I have a wavy path and a text element, what I want to achieve here is to move the text element along the wavy path from left to right (which is done by GSAP) and stop in the half way of the path in the initial load; it will move to the end when users start scrolling.
My problem is that, the wavy path created by SVG is too long, even the half way of the path will be off the window, I tried to scale down the wavy path by using viewBox, failed; using css width: 100vw, failed.
I also used the css transform property, it did scale the wavy path down, but that was a fixed size, I want to make it as responsive as possible, which means regardless the window width, the text element always stops in the middle of the screen first ( half way of the wavy path ), then moves to the right hand side of the screen. Is this possible when using inline SVG element? If so, please point me in the right direction.
Thank you in advance!
(Please view the example in full page mode, that will explain my problem perfectly, but the scroll function will be disabled because the height in that mode is 100vh, there is no room for scrolling)
document.getElementById("MyPath").setAttribute("d", document.getElementById("Path").getAttribute("d"));
var tl = new TimelineMax({
repeat: 0,
delay: 1
});
tl.to("#Text", 3, {
attr: {
startOffset: '50%',
opacity: 1
}
});
window.addEventListener('scroll', function() {
tl.to("#Text", 3, {
attr: {
startOffset: '100%',
opacity: 0
}
});
}, true);
#import url(https://fonts.googleapis.com/css?family=Oswald);
body {
background-color: #222;
}
svg {
overflow: visible;
width: 100%;
height: 100%;
background-color: #fff;
left: 0;
top: 0;
position: absolute;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.17.0/plugins/TextPlugin.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.17.0/TweenMax.min.js"></script>
<svg xml:space="preserve">
<defs><path id="MyPath"/></defs>
<path id="Path" fill="none" stroke="#000" stroke-miterlimit="10" d="M0,188.2c186.2,84,261.8,84.9,440,66.4s295.2-130,535.2-129.4c240,0.6,357,144.3,591.1,144.3
s450.1-141.2,651.1-141.2c271.7,0,354.2,141.2,612.1,141.2c240,0,423-141.2,669.1-141.2c119.1,0,202.3,33.8,281,68.7"/>
<text font-size="7em" >
<textPath id="Text" fill='#88CE02' font-family=Oswald xlink:href="#MyPath" opacity=0 startOffset="0%" letter-spacing="5px">Love the little things.</textPath>
</text>
</svg>
If you want your SVG to scale to fit the screen (or any parent container), it needs to have a viewBox attribute. This attribute tells the browser the dimensions of the SVG content. Without it, the browser has know way of knowing how much it needs to be scaled.
Your path is about 3780 width, and the bottom of it is at y=144. So a reasonable value of viewBox would be:
viewBox="0 0 3780 150"
document.getElementById("MyPath").setAttribute("d", document.getElementById("Path").getAttribute("d"));
var tl = new TimelineMax({
repeat: 0,
delay: 1
});
tl.to("#Text", 3, {
attr: {
startOffset: '50%',
opacity: 1
}
});
window.addEventListener('scroll', function() {
tl.to("#Text", 3, {
attr: {
startOffset: '100%',
opacity: 0
}
});
}, true);
#import url(https://fonts.googleapis.com/css?family=Oswald);
body {
background-color: #222;
}
svg {
overflow: visible;
width: 100%;
height: 100%;
background-color: #fff;
left: 0;
top: 0;
position: absolute;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.17.0/plugins/TextPlugin.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.17.0/TweenMax.min.js"></script>
<svg viewBox="0 0 3780 150" xml:space="preserve">
<defs><path id="MyPath"/></defs>
<path id="Path" fill="none" stroke="#000" stroke-miterlimit="10" d="M0,188.2c186.2,84,261.8,84.9,440,66.4s295.2-130,535.2-129.4c240,0.6,357,144.3,591.1,144.3
s450.1-141.2,651.1-141.2c271.7,0,354.2,141.2,612.1,141.2c240,0,423-141.2,669.1-141.2c119.1,0,202.3,33.8,281,68.7"/>
<text font-size="7em" >
<textPath id="Text" fill='#88CE02' font-family=Oswald xlink:href="#MyPath" opacity=0 startOffset="0%" letter-spacing="5px">Love the little things.</textPath>
</text>
</svg>
Here's what you could do: add a media query defining the desired width and then apply the following styles separately for the text and path elements inside the svg element:
text {
font-size: 2em;
}
path {
-webkit-transform: scale(.3);
-ms-transform: scale(.3);
transform: scale(.3);
}
See the snippet below:
document.getElementById("MyPath").setAttribute("d", document.getElementById("Path").getAttribute("d"));
var tl = new TimelineMax({
repeat: 0,
delay: 1
});
tl.to("#Text", 3, {
attr: {
startOffset: '50%',
opacity: 1
}
});
window.addEventListener('scroll', function() {
tl.to("#Text", 3, {
attr: {
startOffset: '100%',
opacity: 0
}
});
}, true);
#import url(https://fonts.googleapis.com/css?family=Oswald);
body {
background-color: #222;
}
svg {
overflow: visible;
width: 100%;
height: 100%;
background-color: #fff;
left: 0;
top: 0;
position: absolute;
}
#media only screen and (max-width: 500px) {
text {
font-size: 2em;
}
path {
-webkit-transform: scale(.3);
-ms-transform: scale(.3);
transform: scale(.3);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.17.0/plugins/TextPlugin.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/gsap/1.17.0/TweenMax.min.js"></script>
<svg xml:space="preserve">
<defs><path id="MyPath"/></defs>
<path id="Path" fill="none" stroke="#000" stroke-miterlimit="10" d="M0,188.2c186.2,84,261.8,84.9,440,66.4s295.2-130,535.2-129.4c240,0.6,357,144.3,591.1,144.3
s450.1-141.2,651.1-141.2c271.7,0,354.2,141.2,612.1,141.2c240,0,423-141.2,669.1-141.2c119.1,0,202.3,33.8,281,68.7"/>
<text font-size="7em" >
<textPath id="Text" fill='#88CE02' font-family=Oswald xlink:href="#MyPath" opacity=0 startOffset="0%" letter-spacing="5px">Love the little things.</textPath>
</text>
</svg>
I hope it helps!

Transition svg element with css

Problem
I'm trying to simply move an svg element using transform=matrix(a,b,c,d,e,f)
The element is moving fine but when I apply a css transition it has no effect.
Question
Is this possible on svg elements without an external library like d3?
Code I'm trying
html:
<svg width="100%" height="100%" viewbox="0 0 200 200">
<rect x="20" y="20" width="50" height="50"
style="fill: #3333cc"
transform="matrix(1,0,0,1,1,1)"
id="blueBox"
/>
</svg>
<button id="boxMover">
Move it
</button>
jQuery
$(function(){
$('#boxMover').click(function(){
var blueBox = $('#blueBox');
if(blueBox.attr('transform')=='matrix(1,0,0,1,1,1)'){
blueBox.attr('transform', 'matrix(1,0,0,1,100,30)');
} else {
blueBox.attr('transform', 'matrix(1,0,0,1,1,1)');
}
})
})
CSS
svg {
display: block
}
#boxMover {
position: absolute;
left: 20px;
top: 20px;
transition: transform .5s ease;
}
Here's a fiddle I created to test it
A simpler solution:
Create the following class:
.boxEase {
transform: matrix(1,0,0,1,100,30);
transition: all .5s ease;
}
Change your jQuery code to just attach the above class to your box when the button is clicked:
$(function(){
$('#boxMover').click(function(){
var blueBox = $('#blueBox');
blueBox.attr('class', 'boxEase');
})
})
Added dynamic case with variable ending (starting) positions
Use the following jQuery code where transform and transition properties are added to the box conditionally. I imagine you can adjust the conditional to something else but I have used your original example for this case:
`$(function(){
$('#boxMover').click(function(){
var startPos = 'matrix(1,0,0,1,1,1)',
endPos = 'matrix(1,0,0,1,100,30)';
var blueBox = $('#blueBox');
if(blueBox.attr('transform') == startPos){
blueBox.attr('transform', endPos);
blueBox.css({'transform': endPos, 'transition': 'all 0.5s ease'});
} else {
blueBox.attr('transform', startPos);
blueBox.css({'transform': startPos, 'transition': 'all 0.5s ease'});
}
})
}); `
There's a horrible grey area between the CSS and SVG namespaces where things like this happen all the time.
However, you can fix your problem as follows:
Move the transform CSS statement out of the #boxMover rules and put it somewhere it can actually affect the behaviour of your #blueBox SVG element.
Changing the transform attributes of the SVG element directly still won't work because you're talking to the SVG namespace, and the CSS rules aren't having any say in the matter. Instead, set up CSS classes containing the required transform attributes, and switch between those instead. There is no class attribute in the SVG namespace, so the CSS rules will come to life.
Also note that you can't use JQuery's addClass() and removeClass() methods to change the class of an SVG element, so use attr('class') instead
This should work:
$(function() {
$('#boxMover').click(function() {
var blueBox = $('#blueBox');
if (blueBox.attr('class') == 'overHere') {
blueBox.attr('class', 'overThere');
} else {
blueBox.attr('class', 'overHere');
}
})
})
svg {
display: block
}
#boxMover {
position: absolute;
left: 20px;
top: 20px;
}
#blueBox {
transition: transform .5s ease;
}
.overHere {
transform: matrix(1, 0, 0, 1, 1, 1);
}
.overThere {
transform: matrix(1, 0, 0, 1, 100, 30);
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<svg width="100%" height="100%" viewbox="0 0 200 200">
<rect x="20" y="20" width="50" height="50"
style="fill: #3333cc" class="overHere" id="blueBox" />
</svg>
<button id="boxMover">
Move it
</button>

Resources