Carl Ejlers Design system

Introduction

A ledger, not a portfolio. The style is subtractive. Things were taken out until only the ones doing work were left, and the removal kept going further than most designers are comfortable with.

This page is not linked from anywhere on the site and asks not to be indexed. It reads ../css/styles.css directly, so every value below is the value the portfolio is actually using right now.

The style

Five decisions, and every one of them is a thing taken away rather than added. Any of them alone would read as austerity; together they are the design, and removing one leaves the others looking accidental rather than chosen.

What survives the subtraction is a list, a hairline and one gesture. Everything further down this page is a consequence of keeping only those.

One size 16px, everywhere The stylesheet declares one reading size. Hierarchy is carried by weight and grey value, never by scale. Almost nobody does this, and it is the most characteristic thing about the site.
No colour Six greys, no hue Not one hue in the interface. The screenshots are the only colour on the page. A frame with an opinion about colour competes with the picture.
Lines, not containers 1.5px, top edge only Zero corner radius, zero shadows. Nothing is on a card, nothing floats, nothing is elevated. Structure is a rule and some space.
The list is the layout Five rows No thumbnail grid, no hero, no scroll narrative. A table of contents that happens to contain the contents.
One gesture A row opens The entire interaction vocabulary. No tabs, no modals, no page loads. Even the cursor stays default, so the site refuses to look interactive anywhere it isn't.

Where this is easy to get wrong. The same discipline fails in three predictable places, all one degree past the line rather than a different mistake each time. Grey text drifts below 4.5:1 and stops being readable in daylight. The one interactive mark gets quietened until nobody can see there is anything to press. And with no size above the body copy, nothing anchors a page. The system answers each once: Ink Muted for text, two legal greys for icons, one display line per page.

Principles

Six, each with a test you can fail. A principle that cannot be failed is decoration. "Be simple" settles no argument at 4pm on a Friday.

01 One size, many weights

Hierarchy is carried by weight and colour, never by scale. This is the hardest rule to keep. Every new page will try to introduce a second size.

Test. Reaching for a new font size means you have not decided what matters. Change the weight or the colour instead.

02 Rules, not boxes

Structure comes from hairlines and space. No cards, no radii, no shadows, no filled panels. A list reads as a ledger, not a stack of objects.

Test. If a group only holds together inside a container, the spacing is wrong. Fix the spacing; delete the container.

03 The work is the only ornament

The interface is greyscale so the screenshots can be the only colour. Project images run the full width of their column, unframed and uncropped.

Test. Squint at the page. If anything other than the work catches your eye first, remove it.

04 Say it once

No element repeats what is already on screen. No decorative icon beside a label that already says the word. No count restating a list you can see.

Test. Delete it. If nothing is lost, it stays deleted.

05 One gesture, always signalled

A single interaction: a row opens. Marked the same way every time: a chevron, right-aligned, rotating on open. Anything that does not open does not get a chevron.

Test. Without hovering anything, can a stranger tell what is clickable? If the mark is below 3:1, they cannot.

06 Quiet is not the same as faint

Restraint is a compositional choice, not a licence to drop contrast. Reduce the amount of text before you reduce its contrast.

Test. Every text colour clears 4.5:1 on the paper behind it. Every icon that carries meaning clears 3:1. No exceptions for elegance.

Colour

Six greys and a paper, in two schemes. Each token has exactly one job, written once. The job is the same in both schemes and only the value moves, so nothing below is a second palette. No value appears twice under a different name.

Dark is the same design the other way up. Paper and Ink swap ends and every other grey is the value that holds its light ratio against the new paper, so the ladder reads 9.4, 5.6 and 3.4 either way and no rule about which grey may carry text changes between them. Ink is the one deliberate difference. It eases off pure white, because 300-weight type on near-black blooms.

Both columns are read out of the stylesheet itself, each ratio measured against its own scheme's paper. The device picks which scheme you are in; that one is inked below and the other recedes.

The threshold depends on the job. Text clears 4.5:1, an interface mark clears 3:1, and a surface is not measured against itself.

Selected text is the palette turned over: paper type on ink ground. Without saying so, the one hue the site could ever show would be the browser's selection blue.

Token Light Dark Job

Type

One typeface and one reading size. Inter, with system-ui behind it, loaded in weights 300 to 500. The system has no use for anything heavier, so b and strong are brought back to 500 rather than left at the browser's 700.

Three roles at that one size, plus the display line. Weight and grey carry every distinction: nothing is set in capitals, nothing is letter-spaced, and meta that needs to recede goes to Ink Muted rather than to a smaller size.

Typeface Read from the stylesheet, fallbacks included. One family; there is no second face and no monospace.
Display 34 / 1.25 / 300 / −0.01em One statement per page The only addition to one size. Used once per page, never for a section heading.
Strong 16 / 1.5 / 500 Donkey Republic Anything that anchors a row: project names, active navigation, your own name.
Body 16 / 1.5 / 300 Everything anyone actually reads. Capped at a 600px measure regardless of how wide the viewport gets.

Space, line and measure

One base unit of 5px and eight steps off it. Every padding, margin and gap is one of these values. Widths, measures and touch targets are sized by what they hold, not by the ramp.

Rule 1.5px solid, top edge A group is closed by one final bottom rule, so rules never double up.
Radius 0 Everywhere, including images and avatars. The one exception is a pill filter, which is fully round, never partially.
Shadow none There is not one box-shadow in the stylesheet, and nothing is translucent. A sticky header sits on solid paper and lets its hairline do the separating: depth is stacking order, not an effect.

Layout

A page is a stack of bands, and a band is one idea. Nothing else decides page structure. There is no template, no hero, no sidebar that some pages have and others don't.

A band lays out on twelve columns with one gutter. Twelve because it divides by two, three, four and six, which is every split this site has needed. Five spans are legal and no others, so a column is always a share of the page rather than a width somebody picked, and two pages built a year apart still line up.

Below 1100px the columns collapse and a band's children stack in source order, the same width the drawer changes shape at. Write the source in reading order and the phone is already handled.

Columns Measured from a real band at load rather than stated here, and the ruler below is drawn from the same reading, so this page cannot show a grid the stylesheet is not using.
Gutter One value, never overridden. A band that needs a different gap is two bands.
Legal spans 12 · 8 · 6 · 4 · 3 Nine, ten and five are not on this list. If a shape needs one of them, the shape is wrong or the content belongs in two bands.
Band rhythm Above and below, with a hairline between bands and never above the first. The same way the work list divides its rows.
Full 12
Anything full width
Media, an index of rows, a table. The default when the content has no second thing to sit beside.
Statement 8
A sentence worth the room
A section opener in the display line. Eight columns so it is plainly not body copy. A heading is short enough that the measure rarely bites, and if it is not, it is not a heading.
Reading 6
Body copy
Prose anyone has to read in order. Six columns lands near the measure on its own, which is the point of choosing it.
Label and body 4 · 8
Label
What it is
The ledger row, at page scale. A short left-hand term and the thing it names. Every specification on this page is this shape.
Aside 4 sticky · 8
Holds still
Scrolls past
Notes beside something long. The left column takes stick and holds while the right one scrolls, which is the shape an open project already uses.
Split 6 · 6
One
The other
Two things of equal weight, and only then. An uneven pair dressed as a split tells the reader they matter the same when they don't.
Gallery 4 · 4 · 4 or 3 · 3 · 3 · 3
One
Two
Three
Short blocks of the same kind: principles, roles, numbers. Three or four across; beyond four they stop being read and start being counted.
measure max-width Caps a block at the reading measure inside whatever span holds it. Put it on the text, not on the column. The column is a share of the page; the measure is a property of prose.
stick position: sticky Makes a column hold still while the one beside it scrolls. Only above 1100px, because below that there is no column beside it. An open project's text column is this.
bottom align-self: end Sits a column on the bottom of its band instead of hanging from the top. For the short thing beside the long one, like a contact block next to a page opener, where starting at the top strands it against nothing.
label Sticky row below 1100px A band's label column, composed with stick. On a desk, stick already holds it beside its content. In a pocket the columns are gone, so the label rides the top of the window on solid paper while its band scrolls past, the same move the work list's headings make, and the next band's label relieves it.
stack flex column, 20px Stacked blocks inside one column: a heading and its paragraphs, a label and its value. The only vertical rhythm inside a band; anything larger is a second band.
One band, one idea. If a band needs a subheading to explain its second half, it is two bands. Never nest a band in a band. A column holds content, not another grid. Text is capped at the measure whatever span holds it. Twelve columns of prose is a wall, not a layout. The gutter is never overridden. A band that wants a different gap is two bands. A hairline goes between bands, never above the first and never around one. Source order is reading order, because below 1100px that is the whole layout.

Pages

The architecture above the bands. Every page on the site is assembled the same way, from the same three things in the same order: the trail, then bands, then nothing. A new page is not designed so much as declared.

The trail One fixed point The breadcrumb opens every page, 40px down on the page inset, the front page included. There it is one step long and the h1 carries it. Going deeper adds steps to the right of the name, and the name itself never moves: the whole site turns on that one fixed point.
Then bands To the end After the trail a page is bands until it stops. No masthead, no footer, no menu, no shell the pages share. What makes them one site is the tokens, the grid and the trail, not a frame around them.
The portrait 142px, top right One size per pocket and one corner: 142px on a desk, 100px below 600px, beside the trail in the page opener rather than in a band of its own, so no page decides again. Beside and not inside: the trail is a line of text, the portrait is 211px, and nesting them would give the trail two different shapes depending on the page. It appears where a page introduces the person, which is the front page and the CV, and nowhere else. It is the only photograph the interface carries; everything else is a screenshot of work.
Title Page · Carl Ejlers The browser tab reads the trail backwards: the page, a middot, the name. Home is the name alone.
The map / · /work · /cv · /notes The front page is an index and nothing else: it introduces the person and points at the pages that hold the substance. /work is the project list, /cv is the history and /notes is the writing, each one step below; a note sits one step further, the only second level on the site. /system is unlisted: linked from nowhere and asking not to be indexed, because it documents the site rather than belonging to it. This row is the sitemap; a page that is not on it does not exist, and the 404 page is where not existing is said out loud.
Every page starts with the trail. A page without one has no address. The portrait sits beside the trail, one size and one corner, and only where a page introduces the person. Work shows work. The name never moves and never restyles between pages. It is the one thing a visitor can hold onto, and in the browsers that can cross pages softly, it literally holds still while everything else fades. A trail longer than its line loses the tail of its current step to an ellipsis, never a link. The way back never shrinks, and where you are is written on the page below anyway. Depth is one level until a section carries pages of its own. /notes is the first that does, and its trail takes the third step. A page is either on the map or unlisted. Unlisted means linked from nowhere and noindex, not merely forgotten.

Components

The whole inventory, and it should stay close to it. A system this quiet earns its consistency by having almost nothing to be inconsistent about. Every specimen below is the site's own markup and classes.

List row 15px vertical, sticky The full row is the target, not the words in it. Hover changes the fill; nothing lifts, grows or shifts.
  • Donkey Republic Bike Rental Platform
  • Hover The fill changes, nothing moves
  • Open Chevron rotates, drawer unfolds below
Breadcrumb Muted, ink at the end Where you are and the way back, at the top of a page above its first band. The separator is the chevron turned a quarter. The site has one icon and this is it, pointing the way it is read. Every page starts its trail in the same place, 40px down and on the page inset, the front page included: there it is one step long, the h1 carries it, and the name holds still while the steps grow to the right of it.
Index A row per page The site pointing at itself. The same row the work list uses, at reading size and never the display size, so the page keeps exactly one display line. Its rows sit on the floor of the window, which is what turns the empty middle into intent. The front page is this component and a headline, and nothing else.
Section heading Strong, normal case A rule above and space below. Never capitals, never bordered, never boxed.
Meta and link Ink Muted Meta is never a filled chip. Links are underline-free, like ejlers@me.com, and darken to Ink Strong on hover.
Media 100% wide, no radius No border, no caption. Images in a set sit flush; a new set is separated by one step of the ramp.

Do and don't

The ways this system breaks. Each right-hand example is a real temptation. Most are what a component library hands you by default.

Do Donkey Republic Bike Rental Platform Separate two things with weight and grey. One size, two jobs.
Don't Donkey Republic Bike Rental Platform Introduce a size for the same distinction. Now there are three scales to maintain and no rule for the next one.
Do
Integrations
Dashboard
Group with a rule and space.
Don't
Integrations Dashboard
Reach for a card. Radius, fill and shadow arrive together and the page stops being yours.
Do At Donkey Republic I was part of the team creating a global bike-rental platform. Ink Muted. Reads as quiet, passes AA.
Don't At Donkey Republic I was part of the team creating a global bike-rental platform. Grey at 3.4:1. Quiet enough to look tasteful in a mockup, unreadable in daylight.
Do Visible before anyone hovers The one gesture is signalled at a legible contrast.
Don't Invisible until hover Restraint applied to the one thing that must never be quiet.

Motion

Motion is the only part of the system that can explain a change while it is happening, so it is spent on two things. Interaction, where it describes the change the person caused. And the arrival: once per visit, a page develops. The ink settles from the muted grey, the index's terms take their weight with it on the font's variable axis, the hairlines draw left to right, and the portrait and the marks come up from grey. The page never moves while it happens: layout is set from the first frame, and what arrives is ink and line.

Did they cause it? Every animation after the arrival traces back to one click. The develop is the one exception, spent before reading begins: once per visit, on first sight, never on a timer.
Does removing it lose information? Cut it and jump to the end state. If nothing is harder to understand, it was decoration.
Does it end where they are looking? Movement is a promise about where to look next. Finishing off screen misleads rather than guides.

Two easing keywords, no custom curves. Things arriving use ease-out; things leaving use ease-in and run shorter, because nobody watches something go. Under prefers-reduced-motion every duration above drops to 0ms. Nothing is removed; it simply arrives already done.

Movement on arrival. The develop is colour and line only: nothing slides, scales or reflows, because the layout was always there. Replays. A piece develops on its first sight this visit and never again. Scrolling back is reading, not an interaction. Anything that loops. A permanent animation is a permanent interruption. Images. They are the content, so they never scale, fade or zoom on hover. Layout under the cursor. Rows never lift or grow. Only the fill changes, and the arrow's shaft reaches, its head holding the row's edge, because nothing crosses an edge. Motion in the periphery while something else is being read.

Buttons

Buttons are where a system this quiet is most likely to break, because a button is the one element meant to be louder than its surroundings. So the shape is fixed and only the fill changes: one box, 1.5px border, no radius, label at 16px weight 500.

Bordered The default, and the most at home here: a rule bent into a rectangle. The border is Ink Muted, not the rule grey: a divider may be decorative, a control may not.
Grey A filled block carries more weight than a hairline, so this sits above Bordered even though it is paler. For repeated or low-stakes actions.
Solid The loudest thing the system can produce: Ink on Paper, which means black in daylight and near-white after dark. One per view, never two side by side.
Disabled One flat treatment for all three. A disabled button has no hierarchy left to express. The label stays readable.
One solid button per view. If a second action feels equally important, one of them is not. There is no small button. If it needs to be smaller, it is a link. No icons in buttons. The chevron means one thing, and that is a row opening. Sentence case, verb first, one to three words. Never inside a list row header. The row is already the control. A button changes something, a link goes somewhere. An email address stays a link.

Icons

Seven glyphs, all descended from the chevron: a 20px box, a 14px live area inset 3px, a 1.5px stroke, round cap and join, no fill. Line ends are round; shapes are not. No rounded rectangles anywhere.

Chevron Arrow Close Menu Search Play Download
Rotation does the work Chevron and arrow cover eleven jobs between them at four angles each, including external link as the arrow at −45°. That is why the set is seven and not fourteen.
Two greys, no more Grey at rest, Ink Strong when active or when the icon is the only control in reach. Grey clears 3:1 for an interface mark. As text it fails.
20px mark, 44px target The glyph stays 20px; what you press is the whole row, or a 44px box around it. An icon that is also the entire hit area fails on a phone.
No loading spinner. It loops, which section 07 rules out. Say "Loading" in words. No tick. Selection is already shown by the control filling in. No plus or minus. The chevron owns expansion. No info, warning or error marks. A sentence is clearer and never ambiguous. No social logos. They are other companies' brands and drag colour onto a greyscale page. No icon drawn to decorate a heading. Rewrite the heading instead.

How this stays true

A design system kept in a separate document drifts, because nothing stops it. This one is built so the parts that can drift, can't.

The rule Never stray Never stray from the design system unless deliberately asked to. Not "usually follow it". A new value, a new size, a new colour or a new gesture is a decision to take out loud, not one to arrive at while solving something else. Everything below exists to make breaking this rule visible rather than to prevent it.
It loads the site's CSS ../css/styles.css Not a copy. Every swatch, ramp bar, row, button and icon above is drawn by the same rules the portfolio uses. Change a token and this page changes with it. There is no second place to update.
Numbers are computed system.js Contrast ratios, hex values, the space ramp, the page metrics and the motion durations are all read from the live stylesheet at load. None of them are typed into this page, so none of them can quietly go stale.
The prose is deliberate by hand Principles, rationale and tradeoffs are judgement, not data. They should only change when you decide they do, so those are the words to revisit when the design moves, and the only ones.
A check that fails npm run check tools/check-tokens.mjs reads styles.css and reports any colour or spacing value used outside the token block. It also holds the two schemes together: a colour that exists in one and not the other, or a scheme that invents a name of its own, is an error. Run it before a commit and drift shows up as an error rather than as something you notice months later.