border-radius: 50% on a rectangle gives you an ellipse, because 50% is
resolved against width and height separately. You wanted a pill. Use
border-radius: 9999px, or calc(infinity * 1px) if you like to live dangerously.
[Hello]
Casper Juel
[Creative developer]
converts pixels to bits.
Fifteen years of making things for the web, and still
mildly annoyed about border-radius: 50%.
COPENHAGEN, DK [55.67°N, 12.57°E]
Who [float: left; float: right; clear: both]
[Currently]
- Building MCP servers for things that should have had an API.
- Shipping tiny native apps with Tauri.
- Fishing. Badly. With data.
I'm a developer who cares about the last 2%: the easing curve, the focus ring, the way a layout survives a 320px viewport. I work at Signifly, where I build websites, apps and the odd thing that does not fit a category.
Work [hobby projects, mostly]
- Sponos NEW! A fast macOS controller for Sonos with Spotify search, a queue you can drag around, and a floorplan of your rooms.
- aula-mcp POPULAR MCP server for Aula, the Danish school platform. Ask your assistant what's for lunch on Thursday. MitID auth included.
- Fondfinder.dk Natural-language search across every Danish fund and grant. Describe a project, get ranked matches with reasons.
- Byggebørsen.dk A marketplace for reused building materials. Web and app, so the leftover tiles from your renovation find a new floor.
- Materialepasset.dk Digital material passports for reused construction materials. Create them with AI, share them with a QR code, and read a material's full history.
- Hjem Home Assistant wall panels for the Shelly Wall Display. 26 KB gzipped, 58 fps on a 1 GB Cortex-A7, because Lovelace is 5 MB.
- abrp-mcp Unofficial MCP server for A Better Route Planner. EV routes, charging stops and range estimates as tools.
- OrchTerm WIP / HIDDEN A macOS terminal that knows about Claude Code: task panes, diffs and git worktrees. Work in progress.
More on GitHub [54 repos, half of them forks, don't judge]
Sandbox [things I know about the web]
A few deliberately ugly demos. Each one is a bug I have shipped at least once.
If the inner corner uses the same radius as the outer, the corners look wrong.
The inner radius should be outer − padding. Tick the box and watch it stop fighting you.
On mobile the browser chrome comes and goes, and 100vh uses the biggest
possible viewport. Use dvh for things that should follow the toolbar,
svh for things that must never jump.
One anchor, eleven nested spans. The rim is a rotating rainbow gradient masked to a 1.5px
ring with mask-composite: exclude. Two CSS variables, --mx and
--my, carry the pointer position and every layer reads them. No canvas, no images.
Bigger playgrounds: radius lab · dither lab
Graveyard [here lies the code we all wrote]
Ten years of hacks, each one load-bearing at the time. Click a stone to exhume the body.
-
.clearfix2004 – 2017 Killed bydisplay: flow-root[exhume]
.clearfix::after { content: ""; display: table; clear: both; } .clearfix { *zoom: 1; } -
-webkit-font-smoothing2010 – ∞ Not dead. Still haunting this page. Seebody {}.[exhume]
body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } /* mac only. non-standard. everyone ships it anyway. */ -
-webkit- -moz- -ms- -o-2008 – 2016 Killed by Autoprefixer, then by browsers agreeing[exhume]
-webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; -webkit-transition: all .3s; -moz-transition: all .3s; -o-transition: all .3s; transition: all .3s; -
$(document).ready()2006 – 2017 Killed by<script defer>[exhume]
$(document).ready(function() { $('.btn').click(function(e) { e.preventDefault(); $(this).toggleClass('active'); }); }); -
var self = this;2006 – 2015 Killed by arrow functions[exhume]
var self = this; // or: var that = this; setTimeout(function() { self.render(); }, 100); -
(function($){ })(jQuery);2008 – 2015 Killed by ES modules[exhume]
;(function($, window, undefined) { 'use strict'; // "undefined" was passed so nobody could redefine undefined. // yes. that was a thing people worried about. })(jQuery, window); -
<!--[if lt IE 9]>2009 – 2014 Killed by IE 11's retirement, 15 June 2022[exhume]
<!--[if lt IE 9]> <script src="html5shiv.js"></script> <script src="respond.min.js"></script> <![endif]-->
-
*zoom: 1; _height: 1%;2005 – 2012 Killed by the death of hasLayout[exhume]
.box { *zoom: 1; /* IE7 and below */ _height: 1%; /* IE6 only */ *display: inline; /* inline-block for IE7 */ } /* a parser bug, used as a feature, for seven years */ -
filter: progid:...2008 – 2013 Killed by actual gradients[exhume]
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ff0000', endColorstr='#0000ff', GradientType=0 ); /* -ms-filter: "progid:..." for IE8, because of course */
-
text-indent: -9999px2003 – 2012 Killed by SVG and, honestly, by sanity[exhume]
.logo { background: url(logo.png) no-repeat; text-indent: -9999px; overflow: hidden; } /* the browser allocated 9999px of inline box. every time. */ -
padding-bottom: 56.25%2009 – 2021 Killed byaspect-ratio[exhume]
.video { position: relative; padding-bottom: 56.25%; /* 16:9 */ height: 0; } .video iframe { position: absolute; inset: 0; width: 100%; height: 100%; } -
translateZ(0)2010 – 2018 Killed bywill-change, then by not needing it[exhume]
.animated { -webkit-transform: translateZ(0); -webkit-backface-visibility: hidden; -webkit-perspective: 1000; } /* "force GPU acceleration". it mostly forced memory usage. */ -
.no-js / Modernizr2009 – 2017 Killed by@supports[exhume]
<html class="no-js"> <script>document.documentElement.className = document.documentElement.className.replace('no-js', 'js');</script> /* then: .csstransforms .box { ... } */ -
FastClick2011 – 2016 Killed bytouch-action: manipulation[exhume]
// iOS waited 300ms after a tap to see if it was a double-tap. // every app shipped a library to not wait. FastClick.attach(document.body);
-
font-size: 62.5%2004 – 2016 Killed byremsupport and calculators[exhume]
html { font-size: 62.5%; } /* 10px, so 1.4em = 14px */ body { font-size: 1.4em; } /* until something nested and 1.4em became 19.6px */ -
document.all1997 – 2011 Killed byquerySelector, finally[exhume]
var el = document.all ? document.all['nav'] : document.layers ? document.layers['nav'] : document.getElementById('nav'); // three browsers, three DOMs, one developer crying -
onclick="return false"1998 – 2012 Killed byaddEventListenerand good taste[exhume]
<a href="javascript:void(0)" onclick="doThing(); return false;"> Click here </a> <!-- an anchor that goes nowhere, on purpose -->
-
@font-face ?#iefix2009 – 2017 Killed by WOFF2 and IE's funeral[exhume]
@font-face { font-family: 'Foo'; src: url('foo.eot'); src: url('foo.eot?#iefix') format('embedded-opentype'), url('foo.woff') format('woff'), url('foo.ttf') format('truetype'), url('foo.svg#foo') format('svg'); } -
<table> layouts1996 – 2005 Killed by CSS Zen Garden[exhume]
<table width="100%" border="0"> <tr> <td width="200" valign="top" bgcolor="#CCCCCC">nav</td> <td valign="top">content</td> </tr> </table> <!-- nested four deep. with spacer.gif. see next stone. --> -
<img src="spacer.gif">1996 – 2004 Killed by CSS. Eventually.[exhume]
<table cellpadding="0" cellspacing="0" border="0"> <tr><td width="10"><img src="spacer.gif" width="10" height="1"></td></tr> </table> <!-- a 1×1 transparent gif, holding up the entire layout -->
Gone, not forgotten. Pour one out. †
Toolkit
Mostly TypeScript. Occasionally Rust. Always HTML.
- [1] TypeScript
- [2] React & Next.js
- [3] Vanilla CSS
- [4] Three.js & WebGL
- [5] Tauri
- [6] Node & Bun
- [7] Postgres
- [8] MCP & Claude