Skip to main content
Ben Nadel at BFusion / BFLEX 2009 (Bloomington, Indiana) with: Nathan Acuff
Ben Nadel at BFusion / BFLEX 2009 (Bloomington, Indiana) with: Nathan Acuff

Using Margins With Four-Sided Positioning In CSS

By
Published in , Comments (8)

Thirteen years ago, Ryan Jeffords blew my mind when he introduced me to four-sided positioning of absolute/fixed position elements. Yesterday, Scott Tolinski and Ivor Padilla took that to the next level when they explained to me that margins also work with four-sided positioning. And, to be honest, this kind of broke my brain, especially with regard to, margin:auto. As such, I needed to sit down and try it out for myself.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

To be clear, I know that you can use CSS margin in conjunction with positioned elements. However, I'm quite certain that I've only ever applied a margin when using two-sided positioning. Before CSS Flexbox existed, I would often use janky techniques to center elements within the viewport.

This might be done with fixed-size margins:

.centered {
	position: fixed ;
	width: 600px ;
	height: 400px ;
	/* Position the top-left corner at center of screen. */
	top: 50% ;
	left: 50% ;
	/* Translate element back-up and back-over. */
	margin-top: -200px ; /* Half of 400. */
	margin-left: -300px ; /* Half of 600. */
}

Or, in more modern browsers, this might be done with an element-relative 3D-translation:

.centered {
	position: fixed ;
	width: 600px ;
	height: 400px ;
	/* Position the top-left corner at center of screen. */
	top: 50% ;
	left: 50% ;
	/* Translate element back-up and back-over. */
	transform: translate3d( -50%, -50%, 0px ) ;
}

Both approaches have pros-and-cons; but, both operate on an element that is only positioned on two sides. Where things get crazy is when you apply both four-sided positioning and margins:

.centered {
	position: fixed ;
	width: 600px ;
	height: 400px ;
	/* Position at edges of viewport. */
	top: 0 ;
	right: 0 ;
	bottom: 0 ;
	left: 0 ;
	/* Translate element to center of viewport. */
	margin: auto ;
}

The part that breaks my brain here is that the positioning offsets aren't even really applying to the element itself but more so to the element's "margin box". I don't know how to explain this well because I don't have the right words.

I mean, if the viewport is 1,000 px wide and the element is 600 px wide, what the heck does left:0 ; right:0 even mean?! It's straight-up bonkers! BONKERS!

And, what's even more bonkers is that you can then use margin:auto to center an element within that "margin box".

To examine this concept, I created an Alpine.js playground in which I can dynamically apply CSS styles to a position:absolute box that is contained within a position:relative parent element:

/* Creating a relative-position container. */
.container {
	position: relative ;
	width: 650px ;
	height: 200px ;
}

/* Creating an absolute-position element within the container. */
.box {
	position: absolute ;
	width: 50px ;
	height: 50px ;
	/*
		Set sensible defaults for all of these properties. Then, we will use the
		Alpine.js model bindings below to override a subset of these values and
		move the box around within its container.
	*/
	bottom: auto ;
	left: auto ;
	right: auto ;
	top: auto ;
	margin: 0 ;
}

I'm using the browser default values for the .box positioning. But then, I'm using Alpine.js to dynamically bind new CSS values to the style property:

<div class="container">
	<div
		class="box"
		x-bind:style="boxStyles[ selectedIndex ].styles">
	</div>
</div>

The boxStyles array is a collection of 25 different variations that move the box element around to the various areas of the container. For example:

var boxStyles = [
	{
		label: "Top-left",
		styles: {
			top: "10px",
			left: "10px"
		}
	},
	{
		label: "Center-center",
		styles: {
			top: "10px",
			left: "10px",
			right: "10px",
			bottom: "10px",
			margin: "auto"
		}
	},
	{
		label: "Mid-bottom-mid-right",
		styles: {
			top: "50%",
			left: "50%",
			right: "10px",
			bottom: "10px",
			margin: "auto"
		}
	},
	// ... more options ...
];

When I then run this Alpine.js demo and select different options, I can see the box moving around within the container:

A positioned box being moved around the screen as the inset values and margins are being adjusted dyamically with Alpine.js.

In this demo, the box has a fixed-size, which is needed in order for the margin values to work their magic. However, you can get around this constraint in very modern browsers by using CSS fit-content for the width and height. That said, I haven't used this property myself; and, I'm not sure if it's considered (by Google's Baseline project) be "widely available" yet since it still required a vendor-prefix in late 2021.

I'm going to need to let this all sink in a bit - it still hurts my brain. But, this is a very cool technique; and, having it in my back pocket is almost certainly going to be a value-add.

With that said, here's the full code for the HTML page:

<!doctype html>
<html lang="en">
<head>
	<link rel="stylesheet" type="text/css" href="./main.css" />
</head>
<body x-data="demo">

	<h1>
		Using Margins With Four-Sided Positioning In CSS
	</h1>

	<style type="text/css">

		/* Creating a relative-position container. */
		.container {
			position: relative ;
			width: 650px ;
			height: 200px ;
		}

		/* Creating an absolute-position element within the container. */
		.box {
			position: absolute ;
			width: 50px ;
			height: 50px ;
			/*
				Set sensible defaults for all of these properties. Then, we will use the
				Alpine.js model bindings below to override a subset of these values and
				move the box around within its container.
			*/
			bottom: auto ;
			left: auto ;
			right: auto ;
			top: auto ;
			margin: 0 ;
		}

	</style>

	<div
		x-ref="container"
		tabindex="1"
		@mousedown="$refs.container.focus()"
		@keydown="moveBox( $event )"
		class="container">
		<div
			class="box"
			x-bind:style="boxStyles[ selectedIndex ].styles">
		</div>
	</div>

	<!--
		We're using the Alpine.js X-MODEL directive to choose from a collection of style
		options. As the selectedIndex is updated, the new set of styles will be applied
		to the box above (changing its top/bottom/left/right/margin properties).
	-->
	<div class="tools">
		<select x-model.number="selectedIndex">
			<template x-for="( option, i ) in boxStyles">
				<option x-text="option.label" :value="i"></option>
			</template>
		</select>
		<button @click="prevOption()">
			Prev
		</button>
		<button @click="nextOption()">
			Next
		</button>
	</div>

	<!-- Output the currently-selected styles (for debugging). -->
	<textarea
		readonly
		class="debugger"
		x-text="JSON.stringify( boxStyles[ selectedIndex ].styles, null, 4 )">
	</textarea>

	<script type="text/javascript" src="./main.js" defer></script>
	<script type="text/javascript" src="../../vendor/alpine/3.13.5/alpine.3.13.5.min.js" defer></script>

</body>
</html>

And, here's the full code for my Apline.js demo component:

function demo() {

	var boxStyles = [
		// Top row.
		{
			label: "Top-left",
			styles: {
				top: "10px",
				left: "10px"
			}
		},
		{
			label: "Top-mid-left",
			styles: {
				top: "10px",
				left: "10px",
				right: "50%",
				marginInline: "auto"
			}
		},
		{
			label: "Top-center",
			styles: {
				top: "10px",
				left: "10px",
				right: "10px",
				marginInline: "auto"
			}
		},
		{
			label: "Top-mid-right",
			styles: {
				top: "10px",
				left: "50%",
				right: "10px",
				marginInline: "auto"
			}
		},
		{
			label: "Top-right",
			styles: {
				top: "10px",
				right: "10px"
			}
		},
		// Mid-top row.
		{
			label: "Mid-top-left",
			styles: {
				top: "10px",
				left: "10px",
				bottom: "50%",
				marginBlock: "auto"
			}
		},
		{
			label: "Mid-top-mid-left",
			styles: {
				top: "10px",
				left: "10px",
				right: "50%",
				bottom: "50%",
				margin: "auto"
			}
		},
		{
			label: "Mid-top-center",
			styles: {
				top: "10px",
				left: "10px",
				right: "10px",
				bottom: "50%",
				margin: "auto"
			}
		},
		{
			label: "Mid-top-mid-right",
			styles: {
				top: "10px",
				left: "50%",
				right: "10px",
				bottom: "50%",
				margin: "auto"
			}
		},
		{
			label: "Mid-top-right",
			styles: {
				top: "10px",
				right: "10px",
				bottom: "50%",
				marginBlock: "auto"
			}
		},
		// Center row.
		{
			label: "Center-left",
			styles: {
				top: "10px",
				left: "10px",
				bottom: "10px",
				marginBlock: "auto"
			}
		},
		{
			label: "Center-mid-left",
			styles: {
				top: "10px",
				left: "10px",
				right: "50%",
				bottom: "10px",
				margin: "auto"
			}
		},
		{
			label: "Center-center",
			styles: {
				top: "10px",
				left: "10px",
				right: "10px",
				bottom: "10px",
				margin: "auto"
			}
		},
		{
			label: "Center-mid-right",
			styles: {
				top: "10px",
				left: "50%",
				right: "10px",
				bottom: "10px",
				margin: "auto"
			}
		},
		{
			label: "Center-right",
			styles: {
				top: "10px",
				right: "10px",
				bottom: "10px",
				marginBlock: "auto"
			}
		},
		// Mid-bottom row.
		{
			label: "Mid-bottom-left",
			styles: {
				top: "50%",
				left: "10px",
				bottom: "10px",
				marginBlock: "auto"
			}
		},
		{
			label: "Mid-bottom-mid-left",
			styles: {
				top: "50%",
				left: "10px",
				right: "50%",
				bottom: "10px",
				margin: "auto"
			}
		},
		{
			label: "Mid-bottom-center",
			styles: {
				top: "50%",
				left: "10px",
				right: "10px",
				bottom: "10px",
				margin: "auto"
			}
		},
		{
			label: "Mid-bottom-mid-right",
			styles: {
				top: "50%",
				left: "50%",
				right: "10px",
				bottom: "10px",
				margin: "auto"
			}
		},
		{
			label: "Mid-bottom-right",
			styles: {
				top: "50%",
				right: "10px",
				bottom: "10px",
				marginBlock: "auto"
			}
		},
		// Bottom row.
		{
			label: "Bottom-left",
			styles: {
				left: "10px",
				bottom: "10px"
			}
		},
		{
			label: "Bottom-mid-left",
			styles: {
				left: "10px",
				right: "50%",
				bottom: "10px",
				marginInline: "auto"
			}
		},
		{
			label: "Bottom-center",
			styles: {
				left: "10px",
				right: "10px",
				bottom: "10px",
				marginInline: "auto"
			}
		},
		{
			label: "Bottom-mid-right",
			styles: {
				left: "50%",
				right: "10px",
				bottom: "10px",
				marginInline: "auto"
			}
		},
		{
			label: "Bottom-right",
			styles: {
				right: "10px",
				bottom: "10px"
			}
		}
	];

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	return {
		boxStyles: boxStyles,
		selectedIndex: 0,
		/**
		* I initialize the component.
		*/
		init() {

			this.$refs.container.focus();

		},
		/**
		* I select the previous styles option.
		*/
		prevOption() {

			if ( ! this.boxStyles[ --this.selectedIndex ] ) {

				this.selectedIndex = ( this.boxStyles.length - 1 );

			}

		},
		/**
		* I select the next styles option.
		*/
		nextOption() {

			if ( ! this.boxStyles[ ++this.selectedIndex ] ) {

				this.selectedIndex = 0;

			}

		},
		/**
		* I move the box around by mapping the style options onto a two-dimensional grid
		* and then calculating row/column changes.
		*/
		moveBox( event ) {

			var optionCount = this.boxStyles.length;
			var columnCount = 5;
			var rowCount = ( optionCount / columnCount );

			// Calculate the row/column based on the selected index.
			var columnIndex = ( this.selectedIndex % columnCount );
			var rowIndex = Math.floor( this.selectedIndex / columnCount );

			// Move to the next row or column based on keyboard event.
			switch ( event.key ) {
				case "ArrowUp":
					if ( --rowIndex < 0 ) {

						rowIndex = ( rowCount - 1 );

					}
				break;
				case "ArrowDown":
					if ( ++rowIndex === rowCount ) {

						rowIndex = 0;

					}
				break;
				case "ArrowLeft":
					if ( --columnIndex < 0 ) {

						columnIndex = ( columnCount - 1 );

					}
				break;
				case "ArrowRight":
					if ( ++columnIndex === columnCount ) {

						columnIndex = 0;

					}
				break;
				default:
					return;
				break;
			}

			// Map the new row and column onto the selected style index.
			this.selectedIndex = ( ( rowIndex * columnCount ) + columnIndex );

		}
	};

}

Happy Friday!

Want to use code from this post? Check out the license.

Reader Comments

226 Comments

It's Friday, man! Common! You shouldn't scramble our brains going into the weekend! That's just wrong 😜

It's awesome to know that center/center is not our only option! Though, it's where my brain goes e-v-e-r-y-time!

429 Comments

OK. I have no idea what:

top: 0;
right: 0;
bottom: 0;
left: 0;

Does, but, using my intuition, I can imagine each side, pulling the element with an equal amount of force.
So, logically this must centre the element, because no single positioning property should take priority?

However, it seems like margin: auto; is the magic sauce that seems to energise the pulling forces of each positioning property.
Anyway, this is the mental model, I will use to try and remember this. ☺️

I will now read the rest of your article.
This kind of exploration, really reignites my interest in CSS. 🙏

By the way, thanks for fixing the Safari bug. Everything works perfectly now, in your blog comment section. 👊

15,811 Comments

@Chris,

Mwwwaaa ha ha ha ha! 🤪

@Charles,

Nice to get confirmation that Safari iOS is working now for the comments. Yeah, that was a really frustrating bug. Originally, what was happening is that as you type, I was trapping the input event, and using it to requestSubmit() of the comment form using a hidden "Preview" button. And, that button was configured to target a Hotwire Turbo Frame. But, for whatever reason, on iOS, Turbo was submitting the form with the wrong action (save vs. preview).

I ended up using fetch() to grab the preview content and then just inserted it into the page using innerHTML, bypassing all the Turbo logic. Frustrating, but at least it's working now.

re: the CSS stuff, the part that feels so crazy to me is that I can have:

left: 50% ;
right: 0 ;
margin: auto ;

... and it will center it between the half-way point and the right-edge of the screen. I'm accepting that it works; but, my brain is still fighting me on the logic 😆

429 Comments

@Ben Nadel,

Cheers for the reply.

The interesting thing, is that it seems:

right: 0 ;
margin: auto ;

Is parsed first and then

left: 50%;

Seems to work as a sub property?

Which, quite frankly, is bonkers 😀

15,811 Comments

@Charles,

I am not sure what you mean by "parsed first." To be honest, I'm having a hard time building a mental model for how this works. The best I can do is that the top/right/bottom/left build a "containment box". And then the margin aligns the content within the containment box. But, I have no idea if this is anywhere near accurate, technically speaking.

Also, for anyone else where, there is a somewhat new CSS property, inset (introduced in 2021), that is a short hand for the other edge properties. The settings:

top: 10px ;
right: 20px ;
bottom: 30px ;
left: 40px ;

... can be written as:

inset: 10px 20px 30px 40px ;

I didn't want to use it in my post because I thought it might be distracting / confusing. I only recently learned about this CSS property from the Syntax FM podcast.

429 Comments

@Ben Nadel,

My initial thoughts were that the order of properties should be important.
Because left/right and top/bottom are two equal & opposite positioning properties, I would have thought that if both properties existed in the same selector, the second one would cancel the first one.

Therefore:

right: 0;
margin: auto;
left: 50%;

I would have expected only the left: 50% to have had any effect. So, my prediction would have been that the element might be positioned, in the centre, from the element's left hand corner.

However, in reality it seems that the second property, adjusts the positioning of the first. Maybe this is a positive thing, because it adds more flexibility, even if it isn't exactly intuitive.

15,811 Comments

@Charles,

I don't know very much about the technical under-pinnings of CSS, but I believe it gets parsed into an "Object Model" - so the order of the properties shouldn't matter too much (unless we start talking specificity).

As far as I'm concerned, it's half logic, half magic 😆

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel