GeistHaus
log in · sign up

https://n12v.com/posts.atom

atom
7 posts
Polling state
Status active
Last polled May 19, 2026 05:25 UTC
Next poll May 20, 2026 04:49 UTC
Poll interval 86400s
ETag "a98702a0ed66f2d5a2c9da58127b97ae"
Last-Modified Wed, 24 Jun 2020 03:21:44 GMT

Posts

Jekyll on Amazon S3
Show full content

This website runs on Jekyll and it’s deployed to Amazon S3. This is a guide on how I run development and staging environments, gzip and upload only modified files.

Production and Development configs

On production, I combine all my JS files into one and then minify it with Google Closure Compiler.

On development, I include non-minified JS files one-by-one using plain-old script elements. I also include extra debug scrips.

I use different Jekyll configs for production and development:
_config.yml for production deployment
dev_config.yml for local development

dev_config.yml is generated by merging _dev_config.yml (starting with underscore) into _config.yml by:

./tolya_deployer dev_config
Jekyll and Modified Time

After building Jekyll, I run a script to preserve modified time of unchanged files.

Let’s say a publish a new post. This results in the following files being modified:

/jekyll-on-amazon-s3/index.html
/index.html

However, Jekyll regenerated the entire website including copying non-modified files. In our example, source/main.css hasn’t changed, but Jekyll copied it and set modified time to the last build time.

I wrote a script that reverts modified time of Jekyll-copied files back to the one set on the source file. The script looks at public/main.css and checks if it matches source/main.css. When it does, it transfers modified time from source file to the generated file.

./tolya_deployer jekyll
Git

The build directory, public/, is a separate Git repository. After each deploy I make a commit.

What’s the point of it?

I parse git status --porcelain=v1 output to see what files were added, deleted, or modified. Then I gzip only added and modified files, then copy them to staging/. I don’t gzip unchanged or removed files; I copy them to staging/ unchanged.

I gzip files locally. I use Zopfli with high iteration count for minimal file size. This takes time. The early version of my deployment script gzipped all files before each deploy — it was painfully slow.

Git workflow has several added benefits: git status and git diff to see what about to get deployed. git log to see deployment history.

Deploying to Amazon S3

I use s3cmd:

s3cmd sync --verbose --no-mime-magic --cf-invalidate --acl-public staging/ s3://n12v.com/

sync command is sort of like Rsync.

--cf-invalidate create Amazon CloudFront request specifically with changed paths.

--no-mime-magic don’t look at the content of the file to guess MIME-type. Deciding MIME-type based on file extension is more predictable.

--acl-public make files accessible via HTTP.

s3cmd sync doesn’t know which files are gzipped and which aren’t. When gzipping, I store each path in an array. For each of these files, I set Content-Encoding: gzip header:

s3cmd modify --add-header='Content-Encoding: gzip' s3://n12v.com/index.html
s3cmd modify --add-header='Content-Encoding: gzip' s3://n12v.com/jekyll-on-amazon-s3/index.html

I tried using the official Amazon S3 & CloudFront CLI but it didn’t seem to provide a convenient way to invalidate only CloudFront files that I changed on S3.


Here’s the source code of my deployment script. Feel free to ask questions and share your Jekyll or Amazon S3 tips!

https://n12v.com/jekyll-on-amazon-s3/
Two-Way Data Binding
Show full content

Let’s build a temperature converter app in Backbone, React, Angular, Meteor and vanilla JavaScript.

#article_2-way-data-binding h2 { margin: 2em 0 1em; } .temperature-converter, .temperature-converter input { cursor: default; } .temperature-converter input { width: 5em; text-align: right; border: none; background: none; color: inherit; height: 2em; vertical-align: baseline; padding-right: .4em; } .temperature-converter .arrows { font-size: 20px; vertical-align: middle; } .temperature-converter input:focus { outline: none; } .celsius-wrap, .fahrenheit-wrap { display: inline-block; border: 1px solid currentColor; outline-color: currentColor; padding: 0 4px 0 0; border-radius: 4px; } .fahrenheit-wrap {border-color: hsl(151, 21%, 77%)} .celsius-wrap {border-color: hsl(34, 43%, 72%)} #article_2-way-data-binding b { color: #000; font-weight: normal; } #article_2-way-data-binding .celsius-wrap, #article_2-way-data-binding .c > b, #article_2-way-data-binding b.c { background: hsl(46, 52%, 90%); color: hsl(31, 100%, 30%); } #article_2-way-data-binding i.c { background: hsl(46, 52%, 90%); color: hsl(31, 30%, 53%); } #article_2-way-data-binding .f {background: hsl(151, 24%, 90%)} #article_2-way-data-binding .fahrenheit-wrap, #article_2-way-data-binding b.f { color: hsl(151, 60%, 30%); background: hsl(151, 24%, 90%); } #article_2-way-data-binding i.f { color: hsl(151, 20%, 54%); background: hsl(151, 24%, 90%); } #article_2-way-data-binding .f > b {color: hsl(151, 60%, 30%)} #article_2-way-data-binding .hover-highlight { outline: 4px solid yellow; } #article_2-way-data-binding pre code { color: hsl(0, 0%, 50%); } #article_2-way-data-binding input::-webkit-inner-spin-button { font-size: 166%; opacity: 1; } Vanilla JS

°C ⇄ °F

function c2f(c) { return 9/5 * c + 32 } function f2c(f) { return 5/9 * (f - 32) } var celsius = document.getElementById('celsius') var fahrenheit = document.getElementById('fahrenheit') fahrenheit.value = c2f(celsius.value) celsius.oninput = function(e) { fahrenheit.value = c2f(e.target.value) }; fahrenheit.oninput = function(e) { celsius.value = f2c(e.target.value) };

Vanilla JS is our baseline. Input values are synchronised using two event handlers, one on each input field.


function c2f(c) {
	return 9/5 * c + 32
}
function f2c(f) {
	return 5/9 * (f - 32)
}

var celsius = document.getElementById('celsius')
var fahrenheit = document.getElementById('fahrenheit')
fahrenheit.value = c2f(celsius.value)

celsius.oninput = function(e) {
	fahrenheit.value = c2f(e.target.value)
};

fahrenheit.oninput = function(e) {
	celsius.value = f2c(e.target.value)
};
Backbone.js

°C ⇄ °F

$LAB .script('//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js') .script('//cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone-min.js') .wait() .script('/2-way-data-binding/backbone-app.js');

var Temperature = Backbone.Model.extend({
	defaults: {
		celsius: 0
	},
	fahrenheit: function(value) {
		if (typeof value == 'undefined') {
			return c2f(this.get('celsius'))
		}
		this.set('celsius', f2c(value))
	}
})

var TemperatureView = Backbone.View.extend({
	el: document.getElementById('tc-backbone'),
	model: new Temperature(),
	events: {
		'input .celsius': 'updateCelsius',
		'input .fahrenheit': 'updateFahrenheit'
	},
	initialize: function() {
		this.listenTo(this.model, 'change:celsius', this.render)
		this.render()
	},
	render: function() {
		this.$('.celsius').val(this.model.get('celsius'))
		this.$('.fahrenheit').val(this.model.fahrenheit())
	},
	updateCelsius: function(event) {
		this.model.set('celsius', event.target.value)
	},
	updateFahrenheit: function(event) {
		this.model.fahrenheit(event.target.value)
	}
})

var temperatureView = new TemperatureView()

Temperature is our model. Note that it only stores °C values, it doesn’t store °F. We can always convert one to another so there is no need to store both.

View→Model→View Blowback

Changing the value in the text field moves the cursor to the end. The problem is that data flows from an input field to a model, and then back to the same input field, overriding the current value even if it’s exactly the same.

There are workarounds.

React.js

C° ⇄ F°

$LAB .script('//cdnjs.cloudflare.com/ajax/libs/react/0.10.0/react.min.js') .wait() .script('/2-way-data-binding/react-app.js');
var TemperatureConverter = React.createClass({
	getInitialState: function() {
		return {c: 0}
	},
	render: function() {
		var celciusValueLink = {
			value: this.state.c.toString(),
			requestChange: this.onCelsiusChange
		}
		var fahrenheitValueLink = {
			value: c2f(this.state.c).toString(),
			requestChange: this.onFahrenheitChange
		}
		return <div>
			<input type="number" valueLink={celciusValueLink}/>°C
			<span>  </span>
			<input type="number" valueLink={fahrenheitValueLink}/>°F
		</div>
	},
	onCelsiusChange: function(data) {
		this.setState({c: parseFloat(data)})
	},
	onFahrenheitChange: function(data) {
		this.setState({c: f2c(data)})
	}
})

React.renderComponent(
	<TemperatureConverter/>,
	document.body
)

React.js doesn’t have Backbone’s problem with moving the cursor position. Its virtual DOM, a layer between the actual DOM and React’s state, prevents React from unnecessary DOM changes.

setState schedules re-rendering on next requestAnimationFrame. render method updates the virtual DOM, calculates the difference between the current and the previous virtual DOM objects, and applies the changes to the actual DOM.

However, here is another bug (Backbone has it too):

Double Conversion

Instead of 2 we get 1.9999999999999964, because:

c2f(f2c(2)) === 1.9999999999999964

The problem is in the double conversion: Fahrenheits to Celsius, and then back to Fahrenheits. In many programming languages, including JavaScript, all arithmetic operations are performed in floating point, and floating point operations aren't necessarily precise.

0.2 + 0.1 = 0.30000000000000004

Sophie Alpert, a former core developer of React, suggested two different solutions.

Angular.js

°C ⇄ °F

(function() { if (window.jQuery) { jQueryLoaded(); } else { document.addEventListener('DOMContentLoaded', jQueryLoaded, false); } function jQueryLoaded() { $LAB .script('//ajax.googleapis.com/ajax/libs/angularjs/1.2.17/angular.min.js') .wait() .script('/2-way-data-binding/angular-app.js') .wait(function() { angular.bootstrap(document.getElementById('tc-angular'), ['temperature-converter']); }); } })();

Angular.js doesn't have the problems mentioned previously since it doesn’t update the input field that changed.

HTML
<div ng-app="temperature-converter">
	<input type="number" ng-model="c">°C 
	<input type="number" ng-model="c" converter="c2f">°F
</div>
JS
var app = angular.module('temperature-converter', []);

app.directive('converter', function(converters) {
	return {
		require: 'ngModel',
		link: function(scope, element, attr, ngModel) {
			var converter = converters[attr.converter]
			ngModel.$formatters.unshift(converter.formatter)
			ngModel.$parsers.push(converter.parser)
			$scope.c = 0
		}
	}
})

app.value('converters', {
	c2f: {
		formatter: c2f,
		parser: f2c
	}
})
Meteor

Meteor, like Angular, doesn’t have the mentioned problems either.

HTML
<body>
	{{> temperatureConverter}}
</body>

<template name="temperatureConverter">
	<input type="number" value="{{celsius}}" class="celsius">°C 
	<input type="number" value="{{fahrenheit}}" class="fahrenheit">°F
</template>
JS
Session.setDefault('c', 0)

Template.temperatureConverter.celsius = function() {
	return Session.get('c')
};
Template.temperatureConverter.fahrenheit = function() {
	return c2f(Session.get('c'))
};

Template.temperatureConverter.events({
	'input .celsius': function(e) {
		Session.set('c', parseFloat(e.target.value))
	},
	'input .fahrenheit': function(e) {
		Session.set('c', f2c(e.target.value))
	}
})
Summary

Backbone doesn’t support two-way data binding out of the box. It’s the only library here that overwrites the currently edited input field with the same value.

React, Angular and Meteor all support two-way data binding. Although, in my example React needed a little extra logic to handle conversion errors.

Fortunately, none of the mentioned libraries go into an infinite loop updating values back and forth between model and view.


Feel free to remake the vanilla.js example using your favorite framework and comment bellow.


Syntax highlighting in the article was inspired by “Coding in color: How to make syntax highlighting more useful”.

Thanks to Nick Porter for helping with Angular, Yuriy Dybskiy for reviewing my Meteor code, and Adam Solove for copy editing the whole thing.

https://n12v.com/2-way-data-binding/
CSS, Retina, and Physical Pixels
Show full content

On retina screens CSS pixels don’t match device (physical) pixels. A device pixel ratio of 2 means one CSS pixel equals two device pixels. Here are few ways to draw a one-device-pixel border.

Half-pixel border #article_css-retina-and-physical-pixels pre { display: inline-block; padding: .4em 1em; }
border: 0.5px solid black;

Cons:

  • Works only in Firefox and Safari 8 (introduced in OS X Yosemite).
border-image
border-width: 1px;
border-image: url(border.gif) 2 repeat;

border.gif is a 6×6 pixel image:


border-image.com
demonstrates how it’s sliced.

Pros:

  • It works!

Cons:

  • An external image. It’s only 51 bytes and it can be inlined using Data URI. You’d need to fire up Photoshop (or whatever you use) to change the border color, which isn’t very convenient.

Multiple background images
background:
	linear-gradient(180deg, black, black 50%, transparent 50%) top    left  / 100% 1px no-repeat,
	linear-gradient(90deg,  black, black 50%, transparent 50%) top    right / 1px 100% no-repeat,
	linear-gradient(0,      black, black 50%, transparent 50%) bottom right / 100% 1px no-repeat,
	linear-gradient(-90deg, black, black 50%, transparent 50%) bottom left  / 1px 100% no-repeat;

How to target physical pixels on retina screens with CSS” describes how to draw a line. Draw 4 lines and we have a border.

Pros:

  • No external images.

Cons:

  • Cumbersome syntax, although it can be abstracted out with CSS preprocessors.
Scale up and down #article_css-retina-and-physical-pixels .border-scale { position: relative; } #article_css-retina-and-physical-pixels .border-scale:before { -webkit-transform: scale(0.5); -webkit-transform-origin: 0 0; content: ''; border: 1px solid black; transform: scale(0.5); transform-origin: 0 0; width: 200%; height: 200%; position: absolute; left: 0; top: 0; pointer-events: none; } #article_css-retina-and-physical-pixels .rounded-corners:before { border-radius: 12px; }
.retina-border-scale {
	position: relative;
}
.retina-border-scale:before {
	content: '';
	border: 1px solid black;
	transform: scale(0.5);
	transform-origin: 0 0;
	width: 200%;
	height: 200%;
	position: absolute;
	left: 0;
	top: 0;
	pointer-events: none;
}

Yes We Can Do Fraction of a Pixel” explains the method in great detail.

Pros:

  • Supports rounded-corners, although it looks unpleasant on retina.
  • No external images.

Cons:

  • Forces relative or absolute positioning; doesn’t work for <td>.
Are those non-retina backward compatible?

No, none of them are. Media queries to the rescue:

#article_css-retina-and-physical-pixels .thin-border { border: 1px solid rgba(0, 0, 0, 0.5); } @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { #article_css-retina-and-physical-pixels .thin-border { border-image: url(/css-retina-and-physical-pixels/border.gif) 2 repeat; } }
.thin-border {
	border: 1px solid rgba(0, 0, 0, 0.5);
}
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
	.thin-border {
		border-image: url(border.gif) 2 repeat;
	}
}
Browsers supported

I tested in Chrome 31, Safari 7, Firefox 26, and iOS 7 — all worked.

https://n12v.com/css-retina-and-physical-pixels/
Focus Transition: Part 2
Show full content
#keyboard-focus-demo { width: 100%; max-width: 600px; margin: 0 0 2em 0; padding: .5em 1em; background: hsl(38, 33%, 86%); border: 1px solid hsl(38, 30%, 80%); border-top-color: hsl(38, 33%, 93%); } #keyboard-focus-demo h3 { text-align: center; letter-spacing: .3em; margin: .6em 0 .2em; } #keyboard-focus-demo table { width: 100%; max-width: 600px; margin: 0; padding: .5em; } #keyboard-focus-demo td { padding: .2em 0; vertical-align: middle; } #keyboard-focus-demo label+label { margin-left: 1em; } #keyboard-focus-demo .dummy { display: inline-block; } Playground

Play Focus Snail Focus Zoom Focus Hug

UI UX accessibility CSS JS SVG

I performed 3 experiments to address some of the problems of Flying Focus.

Focus Snail

Flying Focus received good critique on Hacker News.

This demo seems to move faster than eye tracking speed, and the animation appears to skip or flicker, and it go [sic] too fast to follow, especially if the widgets are far apart.

True. I tried to increase animation duration but it seemed dull. Focus Snail doesn’t have this problem since it leaves a “trail”, which becomes longer for controls that are spread out, thus easier to notice.

Focus Zoom
Good idea, but the particular visualization (moving shadow) is a bit annoying. I would go with something less in-your-face. Maybe a light shadow that blinks couple of times on focus? I don't think the directional aspect of this is very important. Users just need to know where the focus is now, not where it came from.

I agree, but I don’t like the blinking shadow idea. I experimented with Focus Zoom, which magnifies the focused element then gradually returns it to its original size.

Focus Zoom uses CSS transforms that unfortunately don’t work with inline elements, which led me to my next experiment:

Focus Hug

This prototype only changes the size of the focus box, keeping the size of the focused element intact. It’s not as aesthetically appealing as Focus Zoom, but it works well with inline elements.

Install

All three scripts are on GitHub: Focus Snail, Focus Zoom, Focus Hug.

Focus Snail is also available as a Chrome extension.

Disclaimer

I hope my experiments will serve as inspiration to implement similar effects on an OS level. I implemented the prototypes using web technologies such as CSS, JS, and SVG (for Focus Snail) only because those are tools I’m familiar with. There is no real reason to just limit them to the web.

(function() { var dummies = document.querySelectorAll('#keyboard-focus-demo .dummy'); var currentEffect = 'focusSnail'; var prevFocused = null; window.addEventListener('load', function() { selectEffect('focusSnail'); }, false); window['playFocusDemo'] = function(playButton) { playButton.disabled = true; focusSnail.enabled = false; focusZoom.enabled = false; focusHug.enabled = false; function set(name) { setUI(name); currentEffect = name; } var list = [].slice.call(dummies, 0); function nextInQueue(next) { var inputs = list.slice(0, 3).concat(list[rand(3, 5)], list.slice(6)); playQueue(inputs, next); } prevFocused = playButton; sequence([ function(next) { set('focusSnail'); doFocus(id('focus-snail_toggler')); next(); }, 500, nextInQueue, 500, function(next) { set('focusZoom'); doFocus(id('focus-zoom_toggler')); next(); }, 500, nextInQueue, 500, function(next) { set('focusHug'); doFocus(id('focus-hug_toggler')); next(); }, 500, nextInQueue, 500, function() { playButton.disabled = false; selectEffect('focusSnail'); doFocus(id('focus-snail_toggler')); } ]); }; window['selectEffectFromUI'] = function selectEffectFromUI(name) { window[name].enabled = true; var names = ['focusSnail', 'focusZoom', 'focusHug']; for (var i = 0; i < names.length; i++) { if (names[i] !== name) { console.info(names[i]); window[names[i]].enabled = false; } } }; function selectEffect(name) { selectEffectFromUI(name); setUI(name); } function setUI(name) { window[name].enabled = true; var radios = document.querySelectorAll('[name="focus_radio"]'); for (var i = 0; i < radios.length; i++) { var radio = radios[i]; if (radio.value === name) { radio.checked = true; break; } } } function doFocus(current) { current.focus(); var fn = window[currentEffect].trigger; if (fn.length === 2) { fn(prevFocused, current); } else { fn(current); } prevFocused = current; } function playQueue(inputs, onFinish) { inputs = [].slice.call(inputs, 3); shuffle(inputs); var length = inputs.length; function next(i) { if (i >= length) { onFinish(); return; } var current = inputs[i]; doFocus(current); setTimeout(function() { next(i + 1); }, 400); } next(0); } function sequence(list) { var length = list.length; var i = 0; function next() { if (i >= length) { return; } var current = list[i]; i++; if (typeof current == 'function') { current(next); } else if (typeof current == 'number') { setTimeout(next, current); } } next(); } function shuffle(array) { var m = array.length, t, i; while (m) { i = Math.floor(Math.random() * m--); t = array[m]; array[m] = array[i]; array[i] = t; } } function rand(low, high) { return low + Math.round((high - low) * Math.random()); } function id(name) { return document.getElementById(name); } })();
https://n12v.com/focus-transition-2/
Focus transition
Show full content
Playground #article_focus-transition td {vertical-align: middle; text-align: center; padding: 1em .2em} #article_focus-transition_webkit {outline: 5px auto -webkit-focus-ring-color} #article_focus-transition_osx_focusring {display: inline-block; box-shadow: 0 0 2px 3px #78aeda, 0 0 2px #78aeda inset; border-radius: 2px} CSS JS HTML Placebo button Why?

Keyboard navigation has a major downside: it’s not clear where the focus moves upon pressing the Tab key. Animation makes the transition more apparent.

How?

Flying Focus creates an element that is moving when the focus event happens.

outline: 5px auto -webkit-focus-ring-color makes the element look like it has a focus. It only works in Safari and Chrome.

Firefox doesn’t support outline-style: auto so box-shadow is used instead:

box-shadow: 0 0 2px 3px #78aeda, 0 0 2px #78aeda inset;
border-radius: 3px;

Previously, I actually focused on the moving element. It affected focus and blur events on web pages, making some dropdown menus unusable. Faking the focus solves the problem.

pointer-events: none achieves click-through effect.

Install

Flying Focus is available as a standalone JS file that you can include on your website. Chrome and Safari extensions enable Flying Focus on all websites. It’s all free and open-source.

https://n12v.com/focus-transition/
How to setup local hosts on Mac OS X
Show full content

When I develop this website locally I use n12v.dev virtual host. n12v.dev/index.html is just ~/Sites/n12v.com/public/index.html on the file system.

You can achieve this by either using Pow or /etc/hosts and Apache.

The easy way: Pow Install
curl get.pow.cx | sh
Use
ln -s ~/Sites/n12v.com ~/.pow/n12v

Now n12v.dev serves index.html.

To serve a directory with a name different from “public”:

mkdir ~/.pow/usercss
ln -s ~/Sites/usercss/www ~/.pow/usercss/public
The hard way: /etc/hosts and Apache

Open /etc/hosts in Sublime or your favourite text editor and add:

127.0.0.1	n12v.dev
fe80::1%lo0	n12v.dev

fe80::1%lo0 is an IPv6 address. Without it your local site might run too slow.

Check if it works correctly:

ping n12v.dev
PING n12v.dev (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.040 ms
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.071 ms

Press ctrl C to stop it. If you see ping: cannot resolve n12v.dev: Unknown host then run

sudo dscacheutil -flushcache
Apache

Mac OS X Mountain Lion ships with Apache 2.2.22. You don’t need to install it.

Open /etc/apache2/extra/httpd-vhosts.conf and add the following code to the bottom

<VirtualHost *:80>
  ServerName n12v.dev
  DocumentRoot /Users/nv/Sites/n12v.com/public
  ErrorLog /Users/nv/Sites/n12v.com/error.log
  CustomLog /Users/nv/Sites/n12v.com/access.log common
</VirtualHost>

Note that “nv” is my username; yours is different.

Check if you haven’t made any typos:

apachectl configtest
Syntax OK

Restart Apache:

sudo apachectl restart

That’s it. http://n12v.dev should serve ~/Sites/n12v.com/public/index.html

Troubleshooting 500 Internal Server Error
  1. Open /etc/apache2/httpd.conf
  2. Find <Directory "/Library/WebServer/Documents"> section
  3. Replace
      Order deny,allow
      Deny from all
    with
      Order allow,deny
      Allow from all
  4. Set DocumentRoot to /Users/nv/Sites
https://n12v.com/how-to-setup-local-hosts-on-mac-os-x/
CSS transition from/to auto values
Show full content
.bar-wrap { position: relative; margin-top: .75em; margin-bottom: .75em; font-family: "Helvetica Neue", Helvetica, sans-serif; font-size: 13px; background: hsl(36, 100%, 83%); border: 1px solid hsl(36, 64%, 72%); } #article_css-transition-to-from-auto .radio-input { display: none } #article_css-transition-to-from-auto .radio-label { display: inline-block; width: 99px; text-align: center; border-radius: 12px; background: hsl(36, 100%, 83%); color: hsl(36, 100%, 40%); border-bottom: 1px solid rgba(204, 122, 0, 0.5); border-top: 1px solid rgba(255,255,255,0.3); cursor: pointer; line-height: 19px; } #article_css-transition-to-from-auto_css { -moz-transition: width .5s ease-in-out; -webkit-transition: width .5s ease-in-out; transition: width .5s ease-in-out; } .article_css-transition-to-from-auto_active { background: yellow; } .article_css-transition-to-from-auto_fixed { width: 200px; } .article_css-transition-to-from-auto_auto { width: auto; } .article_css-transition-to-from-auto_bar { white-space: nowrap; background: hsl(36, 100%, 61%); border: 1px solid hsl(36, 100%, 40%); line-height: 36px; padding: 0 .5em; margin: -1px; } .snake { background: hsl(36, 100%, 61%); border: 1px solid hsl(36, 100%, 40%); } #article_css-transition-to-from-auto .radio-input:checked + .radio-label { box-shadow: 0 0 0; color: #000; margin-top: 1px; background: hsl(36, 100%, 61%); border-top: 1px solid hsl(36, 100%, 40%); border-bottom: 1px solid rgba(255,255,255,0.3); } #article_css-transition-to-from-auto .l-dark { background: hsl(36, 100%, 61%); color: hsl(36, 100%, 31%); } #article_css-transition-to-from-auto_toggler .radio-label { position: absolute; top: 26px; }

None of the browsers handle CSS transition from/to auto values correctly.

width: 200px width: auto CSS transitions Expected (function() { function jQueryLoaded() { function changed(element) { var base = 'article_css-transition-to-from-auto_'; var $css = $('#' + base + 'css'); var $fixed = $('#' + base + 'fixed'); var AUTO = base + 'auto'; var FIXED = base + 'fixed'; if (element.value === 'auto') { $css.removeClass(FIXED); $css.addClass(AUTO); $fixed.transition({width: 'auto'}, 500, 'in-out'); } else { $css.removeClass(AUTO); $css.addClass(FIXED); $fixed.transition({width: '200px'}, 500, 'in-out'); } } $('[name="width_toggler"]').on('change', function(e) { changed(e.target); }); var checked = $('[name="width_toggler"]:checked'); if (checked.length) { changed(checked[0]); } } if (window.jQuery) { jQueryLoaded(); } else { document.addEventListener('DOMContentLoaded', jQueryLoaded, false); } })();

I hope one day transitions to/from auto values will work out of the box in all major browsers. Meanwhile, read on.

Bug reports for WebKit and Firefox.

From auto width: 200px width: auto  
element.style.width = getComputedStyle(element).width
element.style.transition = 'width .5s ease-in-out'
element.offsetWidth // force repaint
element.style.width = '200px'
function setTransition(element, value) { element.style.webkitTransition = element.style.mozTransition = element.style.transition = value; } function fromAuto_run() { var element = document.getElementById('from-auto') element.style.width = getComputedStyle(element).width element.offsetWidth; // force repaint setTransition(element, 'width .5s ease-in-out') element.style.width = '200px' setTimeout(function() { setTransition(element, '') }, 500); } function fromAuto_reset() { var element = document.getElementById('from-auto'); element.style.width = ''; } function toAuto_run() { var element = document.getElementById('to-auto'); var prevWidth = element.style.width; element.style.width = 'auto'; var endWidth = getComputedStyle(element).width; element.style.width = prevWidth; element.offsetWidth; // force repaint setTransition(element, 'width .5s ease-in-out'); element.style.width = endWidth; element.addEventListener('transitionend', function transitionEnd(event) { if (event.propertyName == 'width') { setTransition(element, ''); element.style.width = 'auto'; element.removeEventListener('transitionend', transitionEnd, false) } }, false) } function toAuto_reset() { var element = document.getElementById('to-auto'); element.style.width = '200px'; } To auto width: 200px width: auto  
var prevWidth = element.style.width
element.style.width = 'auto'
var endWidth = getComputedStyle(element).width
element.style.width = prevWidth
element.offsetWidth // force repaint
element.style.transition = 'width .5s ease-in-out'
element.style.width = endWidth
element.addEventListener('transitionend', function transitionEnd(event) {
	if (event.propertyName == 'width') {
		element.style.transition = ''
		element.style.width = 'auto'
		element.removeEventListener('transitionend', transitionEnd, false)
	}
}, false)

That’s a lot of code for a simple transition and haven’t even covered vendor prefixes to make it work in Firefox, WebKit, IE and Opera. To automate this I use jquery.transit.

jquery.transit

jquery.transit is a jQuery plugin that provides neat JS API for creating animations using CSS transitions.

I forked jquery.transit to make it work with transitions from/to auto values.

element.transition({width: 'auto'}, 500, 'in-out')
element.transition({width: '200px'}, 500, 'in-out')
Pure CSS max-height (max-width) workaround

The most popular answer to "CSS transition height: 0; to height: auto;" question on Stackoverflow is:

Use max-height in the transformation and not height. And set a value on max-height to something bigger than your box will ever get.
#to-from-max-width { -moz-transition: max-width 1s ease-in-out; -webkit-transition: max-width 1s ease-in-out; transition: max-width 1s ease-in-out; } #to-from-max-width .radio-label { width: 140px; } max-width: 160px max-width: 4000px  
#to-from-max-width {
	transition: max-width 1s ease-in-out;
}

In the example above I set max-width to 4000px. Who has such a wide screen, right?

Here goes the problem:

The width of the orange box in your browser is currently Xpx.
Transition from 160px to 4000px takes 1 second.
Transition from 160px to Xpx takes (X-160)/4000 second; animation is 4000/(X-160)× faster than expected.
Transition from Xpx to 160px is delayed for 1-((X-160)/4000) second; that’s the time to go from 4000px to Xpx which has no visible effect whatsoever.

Resize you browser window, see how values change. Narrower the window, more screwed up the animation.

(function() { var MIN = 160; var MAX = 4000; window.maxWidth_toFixed = function() { document.getElementById('to-from-max-width').style.maxWidth = MIN + 'px'; }; window.maxWidth_toAuto = function() { document.getElementById('to-from-max-width').style.maxWidth = MAX + 'px'; }; function round(x) { return Math.round(x * 100) / 100; } function updateWidth() { var el = document.getElementById('to-from-max-width_wrap'); var width = el.offsetWidth; var w = document.querySelectorAll('.article_css-transition-to-from-auto_width'); for (var i = 0; i < w.length; i++) { var item = w[i]; if (!item.dataset.formula) { item.dataset.formula = item.textContent; } var expression = item.dataset.formula.replace(/X/g, width); var value = eval(expression); item.textContent = round(value); } } updateWidth(); window.addEventListener('resize', function() { updateWidth(); }, false); })();
https://n12v.com/css-transition-to-from-auto/