Skip to content

feat(website): world-class brand and design system - #1179

Draft
vivek7405 wants to merge 17 commits into
mainfrom
feat/brand-design-system
Draft

feat(website): world-class brand and design system#1179
vivek7405 wants to merge 17 commits into
mainfrom
feat/brand-design-system

Conversation

@vivek7405

@vivek7405 vivek7405 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Rebuilds the site on a real design system and finishes the brand direction that was sitting uncommitted in the primary checkout.

Closes #1178

Grounded in the shared claude.ai/design design-system project and benchmarked against nextjs.org (which is pure black, no colour wash, small understated controls, and a very large tight headline).

Landed

Colour is rationed. Neutrals carry the page at near-zero chroma; the brand ramp is reserved for the mark, focus, and live state. The primary CTA is plain foreground on background, not an accent pill. The glow wash dropped from 0.24 to 0.07 and the closing-CTA panel lost its tint, so the last thing on a page is no longer the loudest thing on it.

Live-component hero. Source on the left, the component that source declares running on the right, SSR'd and upgraded in place. It is the one claim on the page a screenshot cannot fake, and no competing framework site has an equivalent.

Real brand assets. The draft wordmark was 76 KB because it embedded a base64 woff2 and drew the name as live text. It is now outlined paths at 3.5 KB, extracted from Inter Tight at display weight, with light and dark variants, a monogram, a favicon, and a zip. The W was redrawn: the slice was thick and sat just below the middle apex, fragmenting the letter at large sizes, and is now thin and low so the mark stays whole and degrades to a plain W below favicon size. The slice is a mask, not a rect painted in the page background, so it is correct on every surface.

One source for the recipes. lib/design.ts replaces four copy-pasted BTN strings; lib/brand.ts holds the marks; lib/brand-tokens.ts holds the palette as data, and /brand paints its swatches from it rather than restating hex (the draft captioned the palette "electric cyan" beside amber chips).

/brand rebuilt with what the Vercel guidelines have and the draft lacked: a clear-space rule quantified in one unit and drawn rather than described, a minimum size with the fallback mark named, a naming section for the casing rule, and trademark terms. Added to the sitemap and the footer.

Eyebrow labels removed site-wide, and the raster icons regenerated (swapping only favicon.svg would have left raster-preferring browsers on the old mark).

Verification

webjs check clean. All routes 200 and reviewed in both themes at 1440px. Screenshots taken against a live dev server throughout.

Not done yet

  • /docs and /ui reviewed only at the route level, not section by section
  • public/og.png social card still shows the previous brand
  • No automated test pins the design tokens

vivek7405 added 17 commits July 30, 2026 00:07
The site had no design system in the real sense: colour, type sizes, and
button shapes were decided per page, and the palette leaned on a warm accent
applied decoratively (glowing pill CTAs, gradient eyebrows, tinted section
labels). At nextjs.org scale that reads as a landing-page advert rather than
as a tool.

Three changes, in order of consequence.

Colour is now rationed. Neutrals carry the page at near-zero chroma
(0.003..0.008, enough to warm them off engine grey) and the brand ramp is
reserved for the mark, focus, and live state. The primary action is plain
foreground on background, the highest contrast pairing the palette has,
which is what keeps the one accented element on a page reading as special.

The hero shows instead of telling. Source on the left, the very component
that source declares running on the right, SSR'd into the HTML and upgraded
in place. It is the one claim on the page a screenshot cannot fake, and no
competing framework site has an equivalent.

The marks moved into lib/brand.ts. The slice through the W is now a mask
rather than a rect painted in the page background, which only looked right
on the single surface it was sampled from and drew a dark stripe across the
mark on every other one.
…ing pages

The home page, /why-webjs, /what-is-webjs, and /brand each carried their own
copy of the button, install-command, and panel class strings. That is how the
site drifted: a change to the control shape landed on whichever page the
author happened to have open, and the four slowly stopped matching.

lib/design.ts is now the single definition, and the four pages import it. The
closing CTA panel loses its accent tint and glow along the way, so the last
thing on a page is no longer the loudest thing on it, and the remaining
uppercase mono micro-labels become sentence case.
The draft /brand shipped a 76 KB wordmark that embedded a base64 woff2 and
drew the name as live text, so the one asset a third party downloads depended
on a font they may not have. It is now outlined paths at 3.5 KB, extracted
from Inter Tight at the display weight, in variants for light and dark
backgrounds plus a monogram and a favicon, zipped as one package.

The W itself is redrawn. The slice was thick and sat just below the middle
apex, which severed the letter into floating fragments at large sizes; it is
now thin and low, so the mark stays whole and degrades to a plain W below
favicon size instead of to mush. The slice is a mask, so it is correct on
every background rather than only on the one it was sampled from.

The page gains what the Vercel guidelines have and the draft did not: a
clear-space rule quantified in one unit and drawn rather than described, a
minimum size with the fallback mark named, a naming section for the WebJs
casing rule, and trademark terms. Its swatches now paint the token values
from lib/brand-tokens.ts, so the palette on the page cannot drift from the
palette in the layout. The draft described the palette as electric cyan next
to amber chips, which is exactly that drift.
app/layout.ts declares favicon-192.png and apple-touch-icon.png, and both
still carried the previous logo. Swapping only favicon.svg would have left
every browser that prefers a raster icon, and Google's favicon crawler, on
the old mark indefinitely.
I redrew the mark instead of adopting it, and lost the three things that made
the original work. The wordmark is ITALIC, so it leans with the W rather than
standing upright beside it. The monogram IS the W of WebJs, not a badge next
to the name. And one horizontal slice cuts the lockup as a single object. My
version was an upright lowercase wordmark beside a separate sliced W, which
kept none of that.

The authored file is restored verbatim and is now the source of truth. The
light variant flips only the two PAINTED fills; the #fff inside the mask is
the mask channel and stays white in both, or the mark vanishes. Bare W files
are added for placing on your own background, and everything is greyscale, so
the identity depends on no accent colour at all.

Re-outlining the embedded italic face is deliberately NOT done here. The font
is variable, and opentype.js applies its deltas wrongly: glyph.getPath ignores
the variation entirely and returns wght=400, while font.getPath applies it but
drops the Js and deforms the b. A corrupted logo is a worse outcome than a
76 KB one, so the file stays authored until the face can be subset properly.

/brand loses the signature-ramp section along with this. With a greyscale
identity it documented a gradient the brand no longer uses.
Three lines survived the logo restore and no longer matched what the page
shows. The colour section still described a three-stop ramp reserved for the
mark, which is now greyscale. The naming section claimed the wordmark is set
lowercase by design, while the lockup plainly reads WebJs. And the clear-space
guides were tinted with the accent, introducing a colour the identity does not
use into the one diagram meant to describe it.

This is the same drift the page was rebuilt to avoid, arriving from the other
direction: the swatches are generated and stayed correct, the prose around
them was hand-written and did not.
Rationing the accent was right; removing it was not. The primary button and
the closing call to action are the two surfaces on a page whose whole job is
to be clicked, and they are exactly where the brand colour earns its place.
Both go back to amber with the glow, from one shared recipe (BTN_PRIMARY and
the new PANEL_CTA) rather than the four copies they used to have.

The light theme goes back to its warmer neutrals too. Dropping the chroma to
near zero read cold and clinical in light mode, where the warmth was carrying
most of the character.

What deliberately does NOT change is the page backdrop: --glow-strength drives
a fixed full-page layer, and raising it tints the whole site rather than the
call to action. The CTA's glow comes from --shadow-glow on the panel itself,
so the glow is where it was asked for and nowhere else.

Everything above those two surfaces stays neutral, which is what makes them
land. The dead ramp tokens (--grad, --accent-mid, --accent-2) and the
.grad-text / .grad-bg helpers go with this: the identity is greyscale, so
nothing consumed them any more. /brand's colour copy is corrected to match.
…n /brand

Two unrequested changes are reverted here.

The dark theme had drifted off what webjs.dev serves: pure black became a warm
charcoal (oklch 0.115 0.003 60), the neutrals picked up chroma, and the accent
moved from hue 52 to 65 at a higher lightness. That came from the shared
design-system foundation rather than from anything asked for, and changing a
live site's background is not a side effect to slip into a brand task. Every
dark value now matches main exactly.

The page backdrop goes back with it: the dot-grid texture and the third
magenta radial are removed, leaving the two radials off --glow-a that the live
site already had. The glow that was actually wanted belongs to the closing CTA
panel, and it comes from --shadow-glow on that panel.

/brand gains what it was missing: the accent is now SHOWN, not just described.
It has its own swatch group (--accent, --accent-fg, --accent-live, both
themes) and a row demonstrating the three places it is allowed to appear, so
the rule is visible rather than asserted. Its asset-card backdrops also used
the near-monochrome values, which meant the page previewed the marks against a
page colour the site does not use.
…py bar

Three fixes to the dark theme only. Light is untouched.

The backdrop tint read brown. That is not a hue problem, it is a lightness
one: brown IS dark orange, and it only exists relative to something lighter.
--glow-a sat at 0.63 lightness, so mixed into black at 16% it had nothing to
be lighter than and settled as a stain. Over white the same hue reads as the
pale pink that works in light mode. Raising it to 0.85 makes it behave like
light instead of pigment, and the hue stays at 52.

The primary button gains real contrast headroom. Amber at 0.7 lightness gave
7.32:1 against its own label; at 0.82 it gives 9.93:1, and 10.88:1 against the
page. Hue stays 52 rather than drifting toward 65, so dark and light remain
the same colour family: a higher-contrast option at hue 65 measured barely
better (10.25:1) and was not worth splitting the two themes apart.

The install command goes back to the live site's proportions. The tightened
version read as a caption, and this bar is a primary call to action in its own
right.
Chasing contrast by raising lightness alone washed the button out: amber at
0.82 lightness measured 9.93:1 but read as a pale peach, not as the brand
colour. Chroma is the other axis, and it buys saturation without the wash.

oklch(0.74 0.19 52) is #ff8101, a fully saturated orange, and it still measures
8.29:1 against its label and 8.37:1 against the page, comfortably past the 7:1
AAA bar and better than the 7.32:1 the live site ships. Hue stays 52, so dark
and light remain one colour family.

Also note for anyone chasing a change that will not appear: the copy bar was
already correct in lib/design.ts for two commits, but the dev server was
serving a stale module for it, so the page kept rendering the old class
string. Restarting the server was the fix, not another edit. Measured against
production afterwards, both are now 342x52 with identical padding, radius, and
background.
The backdrop no longer carries a visible colour. --glow-a moves from 0.85 to
0.42 lightness in dark, so the tint sits just above the page's own black and
reads as depth in the upper corners rather than as a wash. The multiplier goes
up (0.16 to 0.30) as the colour comes down, which keeps the falloff shape
while removing the cast.

Full removal is one value away if the flat version is preferred: set
--glow-strength to 0 in the two dark blocks. It was rendered and compared, and
the only difference is that the top corners go completely dead.

The accent glows are unaffected. Those come from --accent-live through
--shadow-glow on the button and the CTA panel, which is a separate token from
the backdrop, so the two can be tuned independently.
The install bar sat at 16px while the two buttons beside it were 10px, so the
hero's three controls read as three unrelated shapes. That came in with the
restore from production, where the buttons are full pills and 16px looked
correct next to them; it stopped being correct the moment the buttons became
10px rectangles.

Rather than hard-code another number, the radius is now a named step. Three
exist, chosen by what a thing IS and not by how big it is: RADIUS_CONTROL
(10px) for anything you click, 2xl (16px) for cards and code windows, 20px for
full-width panels. The bar keeps every other production property, the padding,
the blur, the shadow, since those are what make it read as a call to action
rather than a caption.

Measured after the change: primary button, ghost button, and install bar all
report 10px.
Dark carries no backdrop tint at all now. --glow-strength is 0 in both dark
blocks, so the fixed layer paints nothing and the page is flat black. Light
keeps 0.16, where the warm wash is wanted and reads as pink rather than as a
stain, so the layer stays in place rather than being deleted.

Verified in a browser: dark resolves --glow-strength to 0, light to 0.16.
…token

Batches several fixes plus a review of another agent's concurrent edits.

New: the preview pane gains a rendered/usage flip, built from a checkbox and
two labels rather than a component. It needs no JavaScript, works with
scripting off, and cannot tear down the live element the way a re-rendering
toggle would, so the count you clicked survives a flip and back. The two stage
header bars now share a fixed 42px height: the left carries 11px traffic dots
and the right only text, so equal padding produced unequal heights and the
seam between the panes visibly stepped. /brand gains a light monogram, since
the lockup shipped both variants and the monogram only the dark one.

Hover state moves to a --hover-surface token holding an ALPHA overlay. The
solid --bg-subtle was a 0.09 lift on a black page, real in numbers and
invisible to the eye, while the same token in light was a 0.025 step that read
clearly. An overlay also composes over the header's translucent blur instead
of fighting it.

Reverted from the concurrent edits, both against explicit direction:
--shadow-glow was set to none in dark, which removed the glow from the primary
BUTTON as well as the CTA panel; and the light accent had drifted from 0.54 to
0.58 lightness, taking it to 4.35:1 as text on the page, under the 4.5:1
floor, on a theme that was declared settled. Four --accent-btn-* tokens were
declared and never referenced by any rule; a dead token is worse than none,
because the next reader assumes the button reads it.

Kept from those edits: the CTA fill is transparent in dark (the literal ask)
while light keeps its tint, and the stage now sits in the progressive
enhancement section rather than the hero.
The free-standing actions are pills again, matching the live site. Horizontal
padding goes 18px to 20px with them: a pill eats more of its own width at the
ends, so the same padding reads tighter than on a rectangle.

The radius scale in lib/design.ts is updated to say so rather than leaving the
comment describing the previous decision. It now has two rules instead of one
number. A free-standing action is a pill, so its shape says press me before its
colour does. Everything that is a SURFACE (the install bar, cards, panels)
keeps a radius scaled to its own height, roughly a quarter of it, which is why
the 52px bar stays at 16px and the 28px copy button at 7px.
…ting it

Three long-form hubs (/blog, /articles, /compare) carried byte-identical
headers, and two marketing pages hand-assembled the closing CTA out of the
same parts. Both are now single functions in lib/design.ts, which is where
cross-cutting composition belongs: module-specific UI stays in its module, and
these are shared across modules that otherwise have nothing to do with each
other.

The duplication was already costing correctness. The two CTAs had drifted to
different lede widths (56ch and 48ch) and different arrow icons (14px at 2.6
stroke, 15px at 2.4). Both now render identically because there is one
definition. The three hub headers still carried the uppercase mono eyebrow
that was removed from every other page, so the pages sharing a header were
also the last ones carrying a device the site had dropped.

Also adds the layout scale the system was missing (WIDE, READING, SECTION) and
removes three exports that had zero consumers. An unused recipe in a shared
module is a claim of reuse that is not true, and it invites the next reader to
assume a page reads it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ship a world-class brand and design system for the website

1 participant