Tag: width

Let’s Make a Multi-Thumb Slider That Calculates The Width Between Thumbs

HTML has an <input type="range">, which is, you could argue, the simplest type of proportion slider. Wherever the thumb of that slider ends up could represent a proportion of whatever is before and whatever is after it (using the value and max attributes). Getting fancier, it’s possible to build a multi-thumb slider. But we’ve got another thing in mind today… a proportion slider with multiple thumbs, and sections that cannot overlap.

Here’s what we’ll be building today:

This is a slider, but not just any slider. It’s a proportion slider. Its various sections must all add up to 100% and the user can adjust the different sections in it.

Why would you need such a thing ? 

Maybe you want to build a budget app where a slider like this would consist of your various planned expenses:

I needed something like this to make an anime movie suggestions platform. While researching UX patterns, I noticed that other anime movie suggestion websites have this sort of thing where you select tags to get movie recommendations. That’s cool and all, but how much cooler would it be to add weight to those tags so that recommendations for a tag with a larger weight are prioritized over other tags with lighter weights. I looked around for other ideas, but didn’t find much.

So I built this! And believe it or not, making it is not that complicated. Let me take you through the steps.

The static slider

We’ll be building this in React and TypeScript. That’s not required though. The concepts should port to vanilla JavaScript or any other framework.

Let’s make this slider using dynamic data for the start. We’ll keep the different sections in a variable array with the name and color of each section. 

const _tags = [   {     name: "Action",     color: "red"   },   {     name: "Romance",     color: "purple"   },   {     name: "Comedy",     color: "orange"   },   {     name: "Horror",     color: "black"   } ];

The width of each tag section is controlled by an array of percentages that add up to 100%. This is done by using the Array.Fill() method to initialize the state of each width:

const [widths, setWidths] = useState<number[]>(new Array(_tags.length).fill(100 / _tags.length))

Next, we’ll create a component to render a single tag section:

interface TagSectionProps {   name: string   color: string   width: number } 
 const TagSection = ({ name, color, width }: TagSectionProps) => {   return <div     className='tag'     style={{ ...styles.tag, background: color, width: width + '%' }} >     <span style={styles.tagText}>{name}</span>    <div      style={styles.sliderButton}      className='slider-button'>              <img src={"https://assets.codepen.io/576444/slider-arrows.svg"} height={'30%'} />     </div>   </div > }

Then we’ll render all of the sections by mapping through the _tags array and return the TagSection component we created above:

const TagSlider = () => {   const [widths, setWidths] = useState<number[]>((new Array(_tags.length).fill(100 / _tags.length)))   return <div     style={{       width: '100%',       display: 'flex'     }}>     {     _tags.map((tag, index) => <TagSection       width={widths[index]}       key={index}       name={tag.name}       color={tag.color}     />)     }   </div> }

To make rounded borders and hide the last slider button, let’s the :first-of-type and :last-of-type pseudo-selectors in CSS:

.tag:first-of-type {   border-radius: 50px 0px 0px 50px; } .tag:last-of-type {   border-radius: 0px 50px 50px 0px; } .tag:last-of-type>.slider-button {   display:none !important; }

Here’s where we are so far. Note the slider handles don’t do anything yet! We’ll get to that next.

Adjustable slider sections

We want the slider sections to adjust move when the slider buttons are dragged with either a mouse cursor or touch, We’ll do that by making sure the section width changes correspond to how much the slider buttons have been dragged. That requires us to answer a few questions:

  1. How do we get the cursor position when the slider button is clicked?
  2. How do we get the cursor position while the slider button is being dragged?
  3. How can we make the width of the tag section correspond to how much the tag section button has been dragged ?

One by one…

How do we get the cursor position when the slider button is clicked?

Let’s add an onSliderSelect event handler to the TagSectionProps interface:

interface TagSectionProps {   name: string;   color: string;   width: number;   onSliderSelect: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void; }

The onSliderSelect event handler is attached to the onPointerDown event of the TagSection component:

const TagSection = ({   name,   color,   width,   onSliderSelect // Highlight }: TagSectionProps) => {   return (     <div       className="tag"       style={{         ...styles.tag,         background: color,         width: width + "%"         }}     >       <span style={styles.tagText}>{name}</span>       <span style={{ ...styles.tagText, fontSize: 12 }}>{width + "%"}</span>          <div         style={styles.sliderButton}         onPointerDown={onSliderSelect}         className="slider-button"       >       <img src={"https://animesonar.com/slider-arrows.svg"} height={"30%"} />       </div>     </div>   ); };

We use onPointerDown instead of onMouseDown to catch both mouse and touch screen events. Here’s more information about that. 

We’re using e.pageX in the onSliderSelect prop function to get the cursor’s position when the slider’s button has been clicked:

<TagSection   width={widths[index]}   key={index}   name={tag.name}   onSliderSelect={(e) => {     const startDragX = e.pageX;   }} />

One down!

How do we get the cursor position while the slider button is being dragged?

Now we need to add an event listener to listen for the drag events, pointermove and touchmove. We’ll use these events to cover mouse cursor and touch movements. The section widths need to stop updating once the user’s finger raises from the screen (thus ending the drag): 

window.addEventListener("pointermove", resize); window.addEventListener("touchmove", resize); 
 const removeEventListener = () => {   window.removeEventListener("pointermove", resize);   window.removeEventListener("touchmove", resize); } 
 const handleEventUp = (e: Event) => {   e.preventDefault();   document.body.style.cursor = "initial";   removeEventListener(); } 
 window.addEventListener("touchend", handleEventUp); window.addEventListener("pointerup", handleEventUp);

The resize function gives the X coordinate of the cursor while the slider button is being dragged:

const resize = (e: MouseEvent & TouchEvent) => {   e.preventDefault();   const endDragX = e.touches ? e.touches[0].pageX : e.pageX }

When the resize function is triggered by a touch event, e.touches is an array value — otherwise, it’s null, in which case endDragX takes the value of e.pageX

How can we make the width of the tag section correspond to how much the tag section button has been dragged?

To change the width percentages of the various tag sections, let’s get the distance the cursor moves in relation to the entire width of the slider as a percentage. From there, we’ll assign that value to the tag section.

First, we need to get the refof TagSlider using React’s useRef hook:

const TagSlider = () => { const TagSliderRef = useRef<HTMLDivElement>(null);   // TagSlider   return (     <div       ref={TagSliderRef} // ...

Now let’s figure out the width of the slider by using its reference to get the offsetWidth property, which returns the layout width of an element as an integer:

onSliderSelect={(e) => {   e.preventDefault();   document.body.style.cursor = 'ew-resize'; 
   const startDragX = e.pageX;   const sliderWidth = TagSliderRef.current.offsetWidth; }};

Then we calculate the percentage distance the cursor moved relative to the entire slider:

const getPercentage = (containerWidth: number, distanceMoved: number) => {   return (distanceMoved / containerWidth) * 100; }; 
 const resize = (e: MouseEvent & TouchEvent) => {   e.preventDefault();   const endDragX = e.touches ? e.touches[0].pageX : e.pageX;   const distanceMoved = endDragX - startDragX;   const percentageMoved = getPercentage(sliderWidth, distanceMoved); }

Finally, we can assign the newly calculated section width to its index on the _widths state variable:

const percentageMoved = getPercentage(sliderWidth, distanceMoved); const _widths = widths.slice(); const prevPercentage = _widths[index]; const newPercentage = prevPercentage + percentageMoved 
 _widths[index] = newPercentage; setWidths(_widths);

But this isn’t the end! The other sections aren’t changing widths and the percentages can wind up being negative or adding up to more than 100%. Not to mention, the sum of all section widths isn’t always equal to 100% because we haven’t applied a restriction that prevents the overall percentage from changing.

Fixing up the other sections

Let’s make sure the width of one section changes when the section next to it changes.

const nextSectionNewPercentage = percentageMoved < 0    ? _widths[nextSectionIndex] + Math.abs(percentageMoved)   : _widths[nextSectionIndex] - Math.abs(percentageMoved)

This has the effect of reducing the width of the neighboring section if the section increases and vice-versa. We can even shorten it:

const nextSectionNewPercentage = _widths[nextSectionIndex] - percentageMoved

Adjusting a section percentage should only affect its neighbor to the right. This means that the maximum value of a given section maximum percentage should be its width plus the width of its neighbor width when it’s allowed to take up the entire neighbor’s space.

We can make that happen by calculating a maximum percentage value:

const maxPercent = widths[index] + widths[index+1]

To prevent negative width values, let’s restrict the widths to values greater than zero but less than the max percentage: 

const limitNumberWithinRange = (value: number, min: number, max: number):number => {   return Math.min(Math.max(value,min),max) }

The limitNumberWithinRange function both prevents negative values and instances where the sum of sections results ina value higher than the maximum percentage. (Hat tip to this StavkOverflow thread.)

We can use this function for the width of the current section and its neighbor:

const currentSectionWidth = limitNumberWithinRange(newPercentage, 0, maxPercent) _widths[index] = currentSectionWidth 
 const nextSectionWidth = limitNumberWithinRange(nextSectionNewPercentage, 0, maxPercent); _widths[nextSectionIndex] = nextSectionWidth;

Extra touches

Right now, the slider calculates the width of each section as a percentage of the entire container to some crazy decimal. That’s super precise, but not exactly useful for this sort of UI. If we want to work with whole numbers instead of decimals, we can do something like this:

const nearestN = (N: number, number: number) => Math.ceil(number / N) * N; const percentageMoved = nearestN(1, getPercentage(sliderWidth, distanceMoved))

This function approximates the second parameter’s value to the nearest N (specified by the first parameter). Setting  N to 1 like the this example has the effect of making the percentage change in whole numbers instead of tiny incremental decimals.

Another nice to touch is consider additional handling for sections with a zero percentage value. Those should probably be removed from the slider altogether since they no longer take up any proportion of the overall width. We can stop listening for events on those sections:

if (tags.length > 2) {   if (_widths[index] === 0) {      _widths[nextSectionIndex] = maxPercent;     _widths.splice(index, 1);     setTags(tags.filter((t, i) => i !== index));     removeEventListener();   }   if (_widths[nextSectionIndex] === 0) {     _widths[index] = maxPercent;     _widths.splice(nextSectionIndex, 1);     setTags(tags.filter((t, i) => i !== nextSectionIndex));     removeEventListener();   } }

Voila!

Here’s the final slider:

The post Let’s Make a Multi-Thumb Slider That Calculates The Width Between Thumbs appeared first on CSS-Tricks.

CSS-Tricks

, , , , , ,

Equal Width Columns in CSS Grid are Kinda Weird

Everything is flexible these days. If you write grid-template-columns: 200px 200px 200px;, sure, you’d have equal-width columns, but that’s a rare day. What you usually mean is three columns of equal fluid width.

We’ve got fractional units for that, like grid-template-columns: 1fr 1fr fr;. That’s usually fine, but they aren’t very sturdy like pixels. A large bit of media (or something like a <pre>, or long bit of text like a URL) can cause those columns to stretch and that’s almost never what you want. I’ve called that a grid blowout. The big idea is that the minimum width of a 1fr column is auto, not 0. In other words,. those widened columns are just being as narrow as they know how to be!

To fix that, we can do like:

.el {   grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr); }

..or we could shorten it:

.el {   grid-template-columns: repeat(3, minmax(0, 1fr)); }

It’s a little awkward, but OK. You only gotta learn it once. You might not even ever run into this if you’re always setting max-width on your media and handling line breaks.

If you want to make your columns sturdy again without the minmax dance, you could use percentages rather than pixels and stay flexible. But what percentage do you use? 33.33%? That’s fine as long as you don’t have any gap — otherwise, the gap will add to the width and overflow the container. You could fake gaps by putting padding inside the columns, but that’s a little janky and uneven.

This whole thing comes from a great tweet from Wes Bos:

I know a ton of people run into this — based on the number of emails I get about the grid blowout article — so it’s worth kinda internalizing why all this is the way it is. It probably should be easier but I don’t have any particular suggestions on how it could be.

The post Equal Width Columns in CSS Grid are Kinda Weird appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]

Fluid Width Video

IN A WORLD of responsive and fluid layouts on the web, ONE MEDIA TYPE stands in the way of perfect harmony: video. There are lots of ways in which video can be displayed on your site. You might be self-hosting the video and presenting it via the HTML5 <video> tag. You might be using YouTube, Vimeo, or some other video provider that provides <iframe> code to display videos. Let’s cover how to make them all fluid width while maintaining an appropriate height based on their aspect ratio.

In each of these video-embedding scenarios, it is very common for a static width and height to be declared.

<video width="400" height="300" controls ... ></video>  <iframe width="400" height="300" ... ></iframe>  <!-- maybe even super old school --> <object width="400" height="300" ... /> <embed width="400" height="300" ... />

Guess what? Declaring static widths isn’t a good idea in fluid width environments. What if the parent container for that video shrinks narrower than the declared 400px? It will bust out and probably look ridiculous and embarrassing.

breakout
Simple and contrived, but still ridiculous and embarassing.

So can’t we just do this?

<video width="100%" ... ></video>

Well, yep, you can! If you are using standard HTML5 video, that will make the video fit the width of the container. It’s important that you remove the height declaration when you do this so that the aspect ratio of the video is maintained as it grows and shrinks, lest you get awkward “bars” to fill the empty space (unlike images, the actual video maintains it’s aspect ratio regardless of the size of the element).

You can get there via CSS (and not worry about what’s declared in the HTML) like this:

video {   /* override other styles to make responsive */   width: 100%    !important;   height: auto   !important; }

<iframe> Video (YouTube, Vimeo, etc.)

Our little trick from above isn’t going to help us when dealing with video that is delivered via <iframe>. Forcing the width to 100% is effective, but when we set height: auto, we end up with a static height of 150px1, which is far too squat for most video and makes for more R&E (Ridiculous and Embarrassing).

Fortunately, there are a couple of possible solutions here. One of them was pioneered by Thierry Koblentz and presented on A List Apart in 2009: Creating Intrinsic Ratios for Video. With this technique, you wrap the video in another element which has an intrinsic aspect ratio, then absolute position the video within that. That gives us fluid width with a reasonable height we can count on.

<div class="videoWrapper">   <!-- Copy & Pasted from YouTube -->   <iframe width="560" height="349" src="http://www.youtube.com/embed/n_dZNLr2cME?rel=0&hd=1" frameborder="0" allowfullscreen></iframe> </div>
.videoWrapper {   position: relative;   padding-bottom: 56.25%; /* 16:9 */   height: 0; } .videoWrapper iframe {   position: absolute;   top: 0;   left: 0;   width: 100%;   height: 100%; }

There is a clever adaptation of this that allows you to adjust the aspect ratio right from the HTML, like:

<div class="videoWrapper" style="--aspect-ratio: 3 / 4;">   <iframe ...>
.videoWrapper {   ...   /* falls back to 16/9, but otherwise uses ratio from HTML */   padding-bottom: calc(var(--aspect-ratio, .5625) * 100%);  }

Some old school video embedding uses <object> and <embed> tags, so if you’re trying to be comprehensive, update that wrapper selector to:

.videoWrapper iframe, .videoWrapper embed, .videoWrapper object { }

But, but… aspect ratios, legacy content, non-tech users, etc.

The above technique is awesome, but it has several possible limitations:

  1. It requires a wrapper element, so just straight up copy-and-pasting code from YouTube is out. Users will need to be a bit savvier.
  2. If you have legacy content and are redesigning to be fluid, all old videos need HTML adjustments.
  3. All videos need to be the same aspect ratio. Otherwise, they’ll be forced into a different aspect ratio and you’ll get the “bars”. Or, you’ll need a toolbox of class names you can apply to adjust it which is an additional complication.

If either of these limitations applies to you, you might consider a JavaScript solution.

Imagine this: when the page loads all videos are looked at and their aspect ratio is saved. Once right away, and whenever the window is resized, all the videos are resized to fill the available width and maintain their aspect ratio. Using the jQuery JavaScript Library, that looks like this:

// Find all YouTube videos // Expand that selector for Vimeo and whatever else var $ allVideos = $ ("iframe[src^='//www.youtube.com']"),    // The element that is fluid width   $ fluidEl = $ ("body");  // Figure out and save aspect ratio for each video $ allVideos.each(function() {    $ (this)     .data('aspectRatio', this.height / this.width)      // and remove the hard coded width/height     .removeAttr('height')     .removeAttr('width');  });  // When the window is resized $ (window).resize(function() {    var newWidth = $ fluidEl.width();    // Resize all videos according to their own aspect ratio   $ allVideos.each(function() {      var $ el = $ (this);     $ el       .width(newWidth)       .height(newWidth * $ el.data('aspectRatio'));    });  // Kick off one resize to fix all videos on page load }).resize();

That’s sorta what became FitVids.js

Except rather than deal with all that resizing business, FitVids.js loops over all the videos and adds the aspect-ratio enabling HTML wrapper and CSS necessary. That’s way more efficient than needing to bind to a window resize handler!

Plain JavaScript instead

jQuery is rather out of favor these days. Fortunately, Dave has a Vanilla version (that is BYO CSS):

  1. Literally all browsers will render iframe, canvas, embed, and object tags as 300px × 150px if not otherwise declared. Even if this isn’t present in the UA stylesheet.

The post Fluid Width Video appeared first on CSS-Tricks.

CSS-Tricks

, ,
[Top]

Animating CSS Width and Height Without the Squish Effect

The first rule of animating on the web: don’t animate width and height. It forces the browser to recalculate a bunch of stuff and it’s slow (or “expensive” as they say). If you can get away with it, animating any transform property is faster (and “cheaper”).

Butttt, transform can be tricky. Check out how complex this menu open/close animation becomes in order to make it really performant. Rik Schennink blogs about another tricky situation: border-radius. When you animate the scale of an element in one direction, you get a squishy effect where the corners don’t maintain their nice radius. The solution? 9-slice scaling:

This method allows you to scale the element and stretch image 2, 4, 6, and 8, while linking 1, 3, 7, and 9 to their respective corners using absolute positioning. This results in corners that aren’t stretched when scaled. 

It’s like the 2020 version of sliding doors.

Direct Link to ArticlePermalink

The post Animating CSS Width and Height Without the Squish Effect appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]

Oh Hey, Padding Percentage is Based on the Parent Element’s Width

I learned something about percentage-based (%) padding today that I had totally wrong in my head! I always thought that percentage padding was based on the element itself. So if an element is 1,000 pixels wide with padding-top: 50%, that padding is 500 pixels. It’s weird having top padding based on width, but that’s how it works — but only sorta. The 50% is based on the parent element’s width, not itself. That’s the part that confused me.

The only time I’ve ever messed with percentage padding is to do the aspect ratio boxes trick. The element is of fluid width, but you want to maintain an aspect ratio, hence the percentage padding trick. In that scenario, the width of the element is the width of the parent, so my incorrect mental model just never mattered.

It doesn’t take much to prove it:

See the Pen
% padding is based on parent, not itself
by Chris Coyier (@chriscoyier)
on CodePen.

Thanks to Cameron Clark for the email about this:

The difference is moot when you have a block element that expands to fill the width of its parent completely, as in every example used in this article. But when you set any width on the element other than 100%, this misunderstanding will lead to baffling results.

They pointed to this article by Aayush Arora who claims that only 1% of developers knew this.

The post Oh Hey, Padding Percentage is Based on the Parent Element’s Width appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]

Making width and flexible items play nice together

The short answer: flex-shrink and flex-basis are probably what you’re lookin’ for.

The long answer

Let’s say you want to align an image and some text next to each other with like this:

A small image of a yellow illustrated bird to the left of a block of text.

Now let’s say you reach for flexbox to make it happen. Setting the parent element to display: flex; is a good first start.

.container {    display: flex;  }

And this results in…

See the Pen
Flex-Shrink Example 1
by Robin Rendle (@robinrendle)
on CodePen.

Yikes! Well, that’s kinda okay, I guess. It makes sense that the image would bump right up against the text like that because we haven’t set a width on the image. Ideally, though, we’d like that image to have a fixed width and then the text should take up whatever space is left over.

Okay, so let’s go do that!

.container {    display: flex;  }  img {    width: 50px;   margin-right: 20px;  }

See the Pen
Flex-Shrink Example 2
by Robin Rendle (@robinrendle)
on CodePen.

This looks great in Chrome. But wait, what? If we inspect the image tag in Firefox DevTools, we’ll find that it’s not the width value that we set at all:

We could use min-width to force the image to the 50px width we want:

img {   min-width: 50px;   margin-right: 20px; }

Buuuuuuut, that only sets helps with the width so we’ve got to put a margin in as well.

img {   min-width: 50px;   margin-right: 20px; }

There we go. That’s better in Firefox and still works in Chrome.

The even longer answer

I realized the image is getting the squished treatment because we need to use the flex-shrink property to tell flex items not to decrease in size, regardless of whether or not they have a width.

All flex-items have a flex-shrink value of 1. We need to set the image element to 0:

.container {    display: flex;  }  img {   width: 50px;   margin-right: 20px;   flex-shrink: 0; }

See the Pen
Flex-Shrink Example 3
by Robin Rendle (@robinrendle)
on CodePen.

Getting better! But we can still do more to improve this.

The director’s cut answer

We can tidy things up further because flex-shrink is included in the flex shorthand property.

flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]

If we set the flex-shrink value to 0 and the flex-basis value to the default width we want the image to be, then we can get rid of the width property altogether.

.container {    display: flex;  }  img {   flex: 0 0 50px;   margin-right: 20px; }

Oh yeah:

See the Pen
Flex-Shrink Example 2
by Geoff Graham (@geoffgraham)
on CodePen.

Another example

That flex-shrink property solves a ton of other problems and is pretty dang important if you want to start using flexbox. Here’s another example why: I stumbled upon yet another problem like the one above and I mentioned it in a recent edition of the newsletter. I was building a navigation component that would let users scroll left and right through multiple items. I noticed the following problem when checking my work:

See the Pen
flex-shrink nav item 1
by Robin Rendle (@robinrendle)
on CodePen.

That longer navigation item shouldn’t break into multiple lines like that — but I finally understood why this was happening, thanks to the previous issue. If you set the flex-shrink property to 0 then it will tell each item in this navigation not to shrink and instead assume the width of the content instead, like this:

See the Pen
flex-shrink nav item
by Robin Rendle (@robinrendle)
on CodePen.

And, yes, we can go the extra step once again to use the flex property instead, this time using auto as the flex-basis since we want the maximum amount of space for all items to be considered when divvying up space in the navigation container.

See the Pen
Setting flex for flexible nav elements
by Geoff Graham (@geoffgraham)
on CodePen.

Huzzah! We figured it out. Even though the answer is a single line of code, it’s is pretty essential one to making truly flexible elements.

CSS-Tricks

, , , , , ,
[Top]