Tripbooks design system / lab 13 / icons and motion

Lab 13 / the biggest gap, closed

Lucide, on our terms.

Every product this size ends up with a library of two hundred glyphs and no rule about when to use one. That happens because icons get treated as decoration for a control that already has a word on it.

This system needs far fewer, and the reason is a good one: it already encodes state in fill and colour. A status has a mark. A severity has an edge. A deadline has a border style. None of those want an icon on top, because a second encoding of the same fact is noise wearing a uniform. What is left is the small set of things a word cannot carry at a glance, and there are thirty-three of them.

They come from Lucide, which the wireframes already ship, brought onto our grid with a three property CSS override. The nav names below are the same ones lib/nav.ts imports today, so nothing has to be re-picked when this reaches the build.

01 / the rule

An icon earns its place three ways, and no others.

Earns itWhy
NavigationFifteen destinations in a rail, scanned dozens of times a day by shape before the word is read. This is the classic case and the only one where recognition alone is the job.
Kind of thingFlight, hotel, transfer, dining, venue, rail, cruise. Repeated across hundreds of rows, and the alternative is a text column that costs ninety pixels of width to say a word the icon says in twenty.
Kind of documentPassport, visa, licence, insurance, national ID. Same argument, plus these appear as tabs where a shape distinguishes faster than five similar nouns.
Marking an inputThe magnifier inside the search field. It is not a label for the control, it is the sign that the box is a search box, and the placeholder beside it is content rather than a name. This is the only icon that sits inside a field.
Does not earn itWhy
StatusThe disposition mark already carries it. An icon beside "Confirmed" says the same thing twice and steals the width the word needs.
SeverityThe leading edge carries it, positionally and without hue, which is the whole reason it was chosen.
DeadlinesBorder style separates contractual from revisable. A clock glyph would say "deadline", which the countdown already said.
Empty and error statesA large sad glyph above a sentence is an apology. The sentence names the next action, and that is the whole state.
Buttons with words on themIssue items, Save enquiry, Request credit release. If the word is right the icon is redundant, and if the icon is needed the word is wrong.
Ruled: the toolbar is words

Filter, Views and Columns are words with no icon. The search field keeps a magnifier because it is an input, not a button, and a bare box with a placeholder is ambiguous in a way a labelled button is not. The rule never bent: an icon never duplicates a word, and the magnifier is not duplicating one, it is marking a field.

02 / construction

Three properties bring a borrowed glyph onto the grid.

Lucide draws on a 24 unit grid with a 2 unit stroke, round caps and round joins. The round caps are the thing that would read as borrowed next to a hairline rule and a zero radius table. CSS beats the presentation attributes Lucide ships, so one rule fixes it everywhere, for every icon, including ones nobody has picked yet.

24 unit grid

Lucide's own. Rendered at 15, 16 or 20 pixels and nothing between, so the geometry lands on whole pixels at every size we use.

1.75 stroke

Lucide ships 2. At our render sizes that sits heavier than the 12.5px body text beside it, so it comes down a notch: present next to a hairline rule, never shouting over a name.

Square caps, mitre joins

The one real change. Lucide is round on both; the platform has zero radius everywhere else, and a rounded stroke end beside a square table cell is the thing that gives a borrowed set away.

Outline only

Lucide has no filled variant, which happens to be exactly right here. Fill means settled on this platform, and an icon is never a state.

/* the whole override */
.tb-icon {
  stroke-linecap: square;
  stroke-linejoin: miter;
  stroke-width: 1.75;
}
In React it is a prop, not a stylesheet

<Plane strokeLinecap="square" strokeLinejoin="miter" strokeWidth={1.75} size={16} /> works too, but a wrapper component that applies the three defaults once is better than three props on every call site. Either way there is no fork of the library and no drawing to maintain.

03 / the set

The thirty-three.

Navigation15 · the names lib/nav.ts already imports
dashboard
customers
enquiries
quotations
bookings
marketplace
analytics
library
hotels
queue
service
archive
settings
workstation
accounting
Kind of thing9 · these replace a text column
flight
stay
transfer
activity
venue
dining
rail
cruise
insurance
Documents4
passport
visa
national id
gated
Inside a field1
search
Controls with no word4 · these are the control, not a picture of it
more
caret
close
drag

04 / what the set buys immediately

The kind column stops being a column.

Same booking, same nine items, same 31 pixel rows. The Kind column carried "Flight · 24 pax", "Hotel · 7 nights", "Transfer · 24 pax" in ninety pixels of width. Now the shape carries the kind and the figure carries the quantity, in twenty-eight.

Before · kind as a word
ItemKindDatesConfirmationSellState
Tokyo Haneda to Osaka Itami ITM-2K9DQ4XWFlight · 24 pax14 Sepnot ticketed38,900.00Awaiting issue
Park Hyatt Kyoto · 12 rooms ITM-7WB3N8LMHotel · 7 nights14 - 21 SepHTL-9920481761,300.00Issued
Airport transfers · 4 vehicles ITM-3XN5V9TCTransfer · 24 pax14 Sepno driver assigned4,800.00Confirming
After · kind as a shape, and the width goes back to the name
ItemPartyDatesConfirmationNetSellMarginState
Tokyo Haneda to Osaka Itami ITM-2K9DQ4XW 2414 Sepnot ticketed31,880.0038,900.0018.0% Awaiting issue
Park Hyatt Kyoto · 12 rooms ITM-7WB3N8LM 1214 - 21 SepHTL-9920481752,140.0061,300.0014.9% Issued
Airport transfers · 4 vehicles ITM-3XN5V9TC 2414 Sepno driver assigned3,960.004,800.0017.5% Confirming
Gala dinner · Kikunoi ITM-5JR9B3FS 2418 SepDIN-119387,100.009,400.0024.5% Issued
Conference room · 3 days ITM-8DK2W6PY 3015 - 17 SepVEN-773209,600.0012,400.0022.6% Issued
Net and margin fitted, and that is the point

One column of shapes bought back enough width for net cost and margin per item, which an agent previously had to open the Commercials card to see. This is the same trade the funnel made when it collapsed three count columns into one: an icon is only worth its pixels when it lets a fact onto the screen that could not fit before. If the width is not spent on something, the icon is decoration.

04b / the toolbar, settled after three passes

Words for the buttons, a magnifier for the field.

Three attempts, and the third is right. Icon beside word was duplication. Icon instead of word looked tidy in a specification and thin in the actual product, because a bookmark and three vertical bars do not tell somebody what they do while a word always does. What survives is the field.

Settled
Search customers, references, travellers Filter3 Views Columns 43 of 214
Why the field is genuinely different

Filter, Views and Columns are buttons, and a button's word is its name. A search field has no name, it has a placeholder, and a placeholder is content that disappears the moment somebody types. The magnifier is the only thing that still says what the box is once a person has typed into it. That is not a label duplicating a word, it is the affordance surviving the content.

What three passes proved about the rule

The rule was tested from both sides in the same week. Adding an icon beside a word broke it. Removing the word to keep the icon technically satisfied it and made the product worse, which is the more dangerous failure, because a rule that can be satisfied while damaging the thing it protects is a rule stated at the wrong level. It is now stated at the right one: an icon may not replace a word that is doing work. The magnifier passes because no word was doing that work.

05 / motion

Motion confirms that something happened. Nothing else.

An operator opens this platform at nine and closes it at six. Anything that moves for pleasure is a thing they will watch four hundred times, and by the third week it is friction. So the rule is narrow: motion exists to tell a person their action landed, and to stop a surface appearing out of nowhere.

The scale

TokenValueUsed by
instant0msAnything driven by a keystroke. Filter chips, table sorts, search results, tab switches. A person typing must never wait for a frame.
quick90ms · linearHover and focus. Fast enough to feel like a property of the surface rather than an event.
enter140ms · cubic-bezier(.2,.7,.3,1)Anything appearing: dialog, popover, toast, the sticky header bar, a peek card. Decelerating, so it arrives rather than lands.
panel180ms · same curveAnything sliding from an edge: an edit sheet, the notification centre. Longer only because the distance is longer.
exit90ms · linearEverything leaving. A dismissal should feel finished before the hand is off the mouse. Never the same duration as the entry.

Try them

enter · 140ms

Dialog

Hover the panel. Opacity and five pixels of rise. No scale, because a dialog that grows implies it came from somewhere it did not.

panel · 180ms

Sheet

Hover the panel. From the edge it belongs to, at the distance it actually travels.

quick · 90ms

Nav item

Hover the panel. The active mark grows from its own baseline. This is the only place a scale transform is allowed.

never

$196,300.00 appears at its value, instantly

Numbers never count up, charts never draw in, table rows never stagger, skeletons never shimmer.

The laws

M1Nothing on data moves

No counting up, no chart draw-in, no row stagger, no shimmer. A figure that animates is a figure a person cannot read yet, and a board opened from a link must be correct the instant it appears.

M2Keystrokes are instant

Filtering, sorting, searching and tab switching happen in the same frame as the input. Animating a filter makes a fast action feel slow and makes a slow one feel broken.

M3Exit is faster than entry

90 against 140. Arriving deserves a moment, leaving does not, and matching them makes dismissal feel sticky.

M4Motion comes from where the thing lives

A sheet slides from its edge. A popover rises from its trigger. Nothing fades in from the middle of the screen, because that teaches a person nothing about where it came from or how to get back.

M5Reduced motion means zero

Not slower, not gentler. Under prefers-reduced-motion every duration is 0 and every transform is removed. The interface is fully usable with no motion at all, which is also the proof that none of it was load bearing.

Ruled

Lucide, with a three property override.

No custom drawing, no fork, no maintenance line.

DecisionReason
Take the libraryAlready installed in the wireframes at version 1.25, MIT licensed, and comprehensive. When somebody needs a glyph in 2027 that nobody predicted, they import it and it matches, rather than waiting on a drawing.
Keep the names the codebase already usesThe fifteen navigation icons here are the same imports in lib/nav.ts today. Nothing is re-picked, nothing is renamed, and the build inherits a decision it has already made.
Override three properties, globallySquare caps, mitre joins, 1.75 stroke. CSS beats the presentation attributes Lucide ships, so it applies to every icon including ones not chosen yet. That is the whole customisation.
The rule that staysNot which icons, but when. Navigation, kind of thing, kind of document, one magnifier inside the search field, and four controls with no word. Nothing on a status, a severity, a deadline, an empty state or a button that already has the right word on it. The library is large; the licence to use it is not.
Where the toolbar landed, after three passes

Words for Filter, Views and Columns. A magnifier inside the search field and nowhere else. Export and Remind keep their words and take no icon. The rule was tested from both sides in one week and is now stated at the level that survives: an icon may not replace a word that is doing work. The magnifier passes because no word was doing that work; a placeholder disappears the moment somebody types.