Module 2 - Rendering Logic II
- Positioned layout is a layout mode that allows elements to overlap
- The position property can be set to relative, absolute, fixed or sticky. The default value is static
Relative Positioning
- Constrains certain children
- Enables additional HTML properties to be used: top, left, right, bottom
- Can set the values of these directional properties to shift the element around
- The values are relative to the element's natural position
- Layout is not affected with position, whereas with margin, sibling elements get pulled along
- Relative positioning can be applied to both block and inline elements
Absolute Positioning
-
Can position elements wherever desired, breaking the orderly fashion of laying elements out next to one another
-
Generally used for:
- UI elements that need to float above the UI, such as tooltips or dropdowns
- Decorative elements that need to be in certain positions, such as illustrations
- Stacking multiple elements in one place, like a deck of cards
<style> .box { position: absolute; top: 0px; right: 0px; width: 100px; height: 100px; border: solid; } </style> <div class="box"></div> <p>Hello World</p>
- Typically, in a flow layout, the box would sit above the paragraph because that's their order in the DOM
- However, with absolute positioning, the box is in the top-right corner and the paragraph has shifted up to fill the space where the box would normally be
-
Can use the properties top, left, right, bottom to position the element based off the constraints of the container, ignoring its natural position
- Here, the box is positioned 20px below the top edge and 25% away from the left edge
- Percentages are based off the total amount of space available in the container
<style> .box { position: absolute; top: 20px; right: 25%; width: 100px; height: 100px; border: solid; } </style> <div class="box"></div>
-
If position: absolute is set but a directional property isn't applied, then the element sits in its default position from flow layout
- In the following snippet, the box would stack on top of surrounding text. Even if position: absolute was removed, not much will change because it's not used properly
<style> .box { position: absolute; border: solid; } </style> <p> This is a bunch of dummy text that makes no sense at all. <span class="box">Hello World</span> This is more dummy text that also makes no sense at all. </p>
-
Elements are pulled out of flow with absolute positioning. When the browser lays out the elements, it pretends absolutely-positioned elements don't exist
- Here, the first paragraph is placed at the top-left, its natural position
- But since the paragraph is absolutely-positioned, the browser thinks the space is still empty
- This causes the other two paragraphs to be placed in the exact same spot
<style> p { position: absolute; } </style> <p>This is a paragraph.</p> <p>Another paragraph, with different words.</p> <p>Finally, to complete the set, a third.</p>
-
Collapsing parents
- This is an example when absolute positioning is applied, the parent collapses because it's technically empty
- It's empty because the child <div> is a "hologram", elements that use absolute positioning don't "exist"
- Here, the child is a square box within a rectangular parent box. When position: absolute is set, the rectangle collapses and has a height of 8px given by only the border
<style> .child { position: absolute; width: 200px; height: 200px; background: blue; } .parent { border: 4px solid; } </style> <div class="parent"> <div class="child"></div> </div>
-
With absolute positioning, the property display pretty much has no effect on the layout of the element
- The exception is relative positioning, which can behave similarly to another layout it could be using like grid or flex layout
- The keyword none hides the element, and that behaviour is consistent in all algorithms
- An absolutely positioned element determines its size by applying the concept of using as least space as possible
- If an element like <p> hits the edge of the viewport, it forces a line-wrap
- If a directional property is applied to add distance from the edges, it will still line-wrap but earlier
- If you can't line-wrap (no spaces), the element will still have the same size, but its contents leave the container and possibly the viewport. This introduces a horizontal scrolling bar
Centering Trick
-
We can adjust the directional properties to centre an element
- Here, the size of the box is dynamically calculated by taking a 100% of the width or height of the container, then subtracting its corresponding pixel distances
.box { position: absolute; top: 50px; left: 50px; right: 50px; bottom: 50px; border: solid; }
- But, what if the size of the box to be strict?
- In the modified example, the box is no longer equidistant from all edges because it's difficult to obey all the declarations given
- The box is now 100px tall and wide, and it is positioned 50px from the top-left (origin point) which has priority in this type of positioning dilemma
.box { position: absolute; top: 50px; left: 50px; right: 50px; bottom: 50px; width: 100px; height: 100px; border: solid; }
- If margin: auto is set, the box will be centered but it disobeys all the values of the directional properties given
-
The inset property will set all 4 edge properties (top, left, right and bottom) to the provided value
- It's basically short-hand for specifying the four directional properties
.box { top: 50px; left: 50px; right: 50px; bottom: 50px; }
.box { inset: 50px; }
Containing Blocks
- Every HTML element has a "containing block", which is a rectangle that forms the bounds of the container
- In flow layout, elements are contained by their parents
- In positional layout, absolute elements can only be contained by other elements using positional layout as well
- If there are no other elements using positional layout, it will be positioned according to the initial containing block (box the size of the viewport)
Stacking Contexts
-
Elements that use positional layout can overlap each other
-
The element that's shown on top is dependent on the DOM order, the last element to be added is shown
- Generally, positioned elements will always render on top of non-positioned ones, ignoring DOM order
-
To specify the layering order to be different from the DOM order, use the z-index property to manually reorder
- The default value is auto, equivalent to 0
- Works on positioned elements and can be used with flex/grid children
- The z refers too the z axis, which is forward/backward
- x is left/right, y is up/down
<style> .box { position: relative; } .first.box { z-index: 2; } .second.box { z-index: 1; } </style> <div class="first box"> A </div> <div class="second box"> B </div>
- z-index can be a negative integer,but can introduce complexity so it's not recommended
-
Other declarations for creating a new stacking context
- Setting opacity to a value less than 1
- Setting position to fixed or sticky
- Apply a mix-blend-mode other than normal
- Adding a z-index to a child inside a display: flex or display: grid container
- Using transform, filter, clip-path or perspective
- Explicitly creating a context with isolation: isolate
-
Debugging stacking contexts
- Free Chrome/Firefox extension called CSS Stacking Context Inspector
- Displays if an element creates its own stacking context
- If an existing z-index property is having any effect
- Where does an element sit relative to other elements in the same stacking context
Managing z-index
-
Can avoid increasing the value of z-index to high numbers by relying on DOM order instead
- We can swap the DOM order to omit z-index in some situations
- Here, we want a card displayed in front of the background image with blob decorations
<style> .wrapper { position: relative; } .card { position: relative; z-index: 2; } .decoration { position: absolute; z-index: 1; } </style> <div class="wrapper"> <div class="card"> Hello World </div> <img alt="" src="/decorative-blob-1.svg" class="decoration" style="top: -20px; left: -70px;" /> </div>
- Consider if there were multiple elements we wanted to layer, the z-index would skyrocket
- We can refactor the HTML above to render the card last, putting it at the top of the stacking context
<style> .wrapper { position: relative; } .card { position: relative; } .decoration { position: absolute; } </style> <div class="wrapper"> <img alt="" src="/decorative-blob-1.svg" class="decoration" style="top: -20px; left: -70px;" /> <div class="card"> Hello World </div> </div>
-
Consider if we wanted a middle element to be emphasised more than its adjacent two elements, the DOM order swap trick can't be applied
- We can assign a higher z-index to the middle element, as long as it's in an isolated stacking context so it doesn't interfere with potentially the same z-index values from other elements
- Here, primary card and <header> have the same z-index, and since the card comes later in the DOM, it overlaps the header
- We can see pricing is a wrapper and we can give it the position: relative and z-index: 1 properties to create a stacking context for the cards
- Now all the cards in the wrapper move as a group, and there won't be any individual overlapping of the card over the header
<header>Hello World</header> <main> <section class="pricing"> <article class="card" /> <article class="primary card" /> <article class="card" /> </section> </main>
- We can omit the two properties mentioned before and just use the isolation property instead
.pricing { isolation: isolate; }
Fixed Positioning
- Similar to absolute positioning, but it can only be contained by viewport, not containing blocks
- Elements are immune to scrolling, it will still persist in the same position despite scrolling
- There are certain CSS properties that mess with fixed positioning when applied to a parent element
- When transform is used, the element stops being locked to viewport
- The same thing occurs with filter and will-change
- Diagnosing the problem can be frustrating in large applications, there may be 10s of ancestor elements
- The following code can be run in the browser console to find any elements with interfering properties
// Replace “.the-fixed-child” for a CSS selector // that matches the fixed-position element: const selector = '.the-fixed-child'; function findCulprits(element) { if (!element) { throw new Error('Could not find element with that selector'); } let parent = element.parentElement; while (parent) { const { transform, willChange, filter } = getComputedStyle(parent); if (transform !== 'none' || willChange === 'transform' || filter !== 'none') { console.warn('🚨 Found a culprit! 🚨\n', parent, { transform, willChange, filter }); } parent = parent.parentElement; } } findCulprits(document.querySelector(selector));
Overflow
-
Overflow is a condition that happens when content doesn't fit into its parent's content box
-
Typically, block elements have dynamic height to contain their child elements
- If you explicitly set a constraining height, you create an impossible condition to fulfill
- In this case, the contents would spill out the bounding box, it overflows
- However, layout is not affected by this mess
-
The property overflow is set to visible by default
-
scroll adds two scrollers to the containing box, removing the overflow of contents
-
overflow is shorthand for the two properties, overflow-x and overflow-y
- We can be more specific about single directional scrolling by specifying the overflow direction
.info { overflow-y: scroll; }
-
auto is another value recommended for most situations when you want a given element to be scrollable
-
It's smart in regards to only adding a scrollbar when it knows the content can't fit, such as resizing the window to be small
-
hidden truncates any content that extends beyond the boundaries of the container
- Useful for truncating text with an ellipsis
- Great for cosmetic effect, can hide overflow for decorative elements
-
Scroll Containers
- Setting scroll, auto or hidden creates a scroll container
- hidden is identical to scroll but with the scrollbars removed
- When a container becomes a scroll container, it manages overflow in both x and y directions
- A scroll container is like a portal to another dimension. When an element is contained inside, it's guaranteed to be stuck inside. It won't overflow beyond the boundaries of the container
- overflow: clip works similar to hidden, but it doesn't create a scroll container
- This is how hidden should have originally worked
Horizontal Overflow
- Consider if we want to overflow horizontally rather than vertically
- Images are inline by default, and they line-wrap when they can't fit
- We can use the white-space property with nowrap on the container to disable line breaks
<style> .wrapper { overflow: auto; border: 3px solid; white-space: nowrap; } .wrapper img { width: 32%; } </style> <div class="wrapper"> <img alt="Description" src="URL" /> <img alt="Description" src="URL" /> <img alt="Description" src="URL" /> <img alt="Description" src="URL" /> </div>
Positioned Layout
-
If you want to contain an element that has absolute positioning by adding the overflow property, then the parent element must use a positional layout
- Otherwise, the child element ignores the parent container and it will overflow
- Here, the box has a larger height than the wrapper, causing it to peek outside. Without the position: relative on .wrapper, it would overflow
<style> .wrapper { position: relative; overflow: hidden; width: 150px; height: 150px; border: solid black; } .box { position: absolute; top: 25px; left: 25px; background: blue; width: 150px; height: 200px; } </style> <div class="wrapper"> <div class="box" /> </div>
- Using overflow: auto also works, adding scrollbars to view the whole box
- Generally, absolute positioning is ignored by standard layout algorithms but overflow: auto treats it like every other element
-
If an element uses fixed positioning, overflow can't be triggered because it's contained by the initial containing block which exists outside the DOM
Sticky Positioning
-
position: sticky is a mix of relative and fixed positioning
- As you scroll, if the element touches the edge of the viewport, it sticks and becomes fixed
- One of the directional properties must also be declared to specify which edge to stick to. It's commonly done with top: 0
-
It was mentioned sticky elements follow the scroll as it becomes fixed to the edge. Despite this, they will never follow outside of its parent container
- Therefore, if the container containing the sticky element goes out of view, then so will the sticky element
-
Directional properties behave differently in all the positional layouts
- In sticky positioning, the value given specifies the minimum gap between the element and the edge of the viewport while the contaienr is in frame
- Setting a value of 0 means the element will stick right against the edge
- Negative values can add an aesthetic cushioning effect, basically there's a delay before an element starts sticking to the edge (even though visually it has)
<style> .sticky { position: sticky; top: 25px; } </style>
-
Elements of other positional layouts don't block out layout space, but sticky elements are different
- They're laid out in-flow, taking up real space even when it's stuck to an edge during scrolling
-
Troubleshooting
-
When a scroll container is created, sticky elements can stick to the closest scroll container
- position: sticky can only stick in one context
-
Ensure the container is large enough so the sticky element can move
-
If there's a thin gap above a sticky header element, this might be a rounding issue with fractional pixels. The following snippet resolves this:
header { position: sticky; top: -1px; }
- Similar to before, the following is a code snippet that's runnable in the browser console to find ancestor elements creating scroll containers
// Replace “.the-sticky-child” for a CSS selector // that matches the sticky-position element: const selector = '.the-sticky-child'; function findCulprits(element) { if (!element) { throw new Error('Could not find element with that selector'); } let parent = element.parentElement; while (parent) { const { overflow } = getComputedStyle(parent); if (['auto', 'scroll', 'hidden'].includes(overflow)) { console.log(overflow, parent); } parent = parent.parentElement; } } findCulprits(document.querySelector(selector));
- If the culprit uses hidden, switch to clip instead
- If it uses auto or scroll, you might be able to remove the property or lower it in the DOM. There's no quick solution
-
Hidden Content
- display: none hides elements by effectively removing it from the DOM. Quite useful for media queries when toggling between mobile/desktop variants
- visbility: hidden hides elements but they still exist, taking up space
- opacity can take fractional values between 0 and 1 to adjust an element's transparency. You can still interact with elements, even if they are invisible