Skip to content
Design and branding

Design and branding

Use Neatship's design system (tokens, components, icons, Storybook), light and dark themes, change the landing page's brand color and rename the product.

This guide shows how screens get their look, and how to make the starter yours: the design system you build with, light and dark themes, the landing page's brand color, and your product's name.

The design system: app-ui

packages/app-ui is Neatship's design system: a library of design tokens (named values for colors, spacing, font sizes, radius, shadows), components (buttons, inputs, dialogs, menus, toasts...) and icons. You own it: it started as a fork of an MIT-licensed open-source design system, and its LICENSE file must stay in the package.

Its look in short: almost everything is a neutral gray, and one blue means "you can act here". Body text is 13px. Spacing follows a 4px grid. Floating surfaces (menus, tooltips, toasts) are translucent with a blur. Every value has a designed light and dark version. The full description is in docs/conventions/06-design-system.md.

Import from the most specific entry point:

Import pathWhat it contains
app-ui/themeTokens (themeCssVariables), ThemeProvider, useTheme
app-ui/iconIcon components
app-ui/primitives/<family>Basic controls, grouped in families: input, surfaces, data-display, feedback, layout, navigation, typography, accessibility
app-ui/componentsBuilding blocks made from primitives (menus, toasts...)
app-ui/utilitiesHooks and helpers
tsx
import { Button } from 'app-ui/primitives/input';
import { AlertDialog } from 'app-ui/primitives/surfaces';
import { useToast } from 'app-ui/components';
import { IconTrash } from 'app-ui/icon';

The interactive controls are built on Base UI, a library that provides keyboard support and accessibility. The Button component, for example, accepts variant (solid, outline, soft, ghost), color (neutral, accent, danger, success) and size (sm, md).

Browse the components in Storybook

Storybook is a catalog where each component is shown in each of its states.

bash
npx nx run app-ui:storybook:serve:dev    # opens on http://localhost:6008
npx nx run app-ui:storybook:test         # runs the story tests in a browser (needs Playwright's browsers)

Every story runs an automatic accessibility check that fails on any violation, and a lint rule (app/no-storybook-a11y-disable) forbids switching it off.

After adding or removing a public component in app-ui, update the list in packages/app-ui/docs/module-ownership.json with npx tsx packages/app-ui/scripts/checkModuleOwnership.ts --write, then review the change. npx nx test app-ui fails when the list is out of date.

Style your own screens with tokens

The web app styles its components with Linaria (styles written in the component file, turned into plain CSS at build time) and takes every value from themeCssVariables:

tsx
import { styled } from '@linaria/react';
import { themeCssVariables } from 'app-ui/theme';

const StyledHeader = styled.div`
  align-items: center;
  border-bottom: 1px solid ${themeCssVariables.border.color.light};
  display: flex;
  gap: ${themeCssVariables.spacing[2]};
  padding: 0 ${themeCssVariables.spacing[3]};
`;

Each value is a CSS variable, such as var(--t-spacing-2). The light and dark values live in two generated stylesheets, so a component never needs to know which theme is active.

Lint enforces three habits:

  • styled components are named Styled<Something>;
  • CSS properties are sorted alphabetically (npx nx lint app-front --configuration=fix sorts them for you);
  • no color is written by hand (no hex, rgb() or hsl()): that is what makes dark mode work everywhere.

When JavaScript needs a real number (an icon size, for example), use useTheme().

Where tokens come from

The tokens are written in TypeScript in packages/app-ui/design-tokens/. A generator turns them into theme-light.css, theme-dark.css and themeCssVariables.ts in packages/app-ui/src/theme/. Never edit those generated files by hand.

bash
npx nx generateTokens app-ui          # regenerate the theme files after editing design-tokens/
npx nx run app-ui:generate:check      # fails if the theme files are out of date

Icons

Icons come from Tabler Icons, through a curated list in app-ui/icon. Importing @tabler/icons-react directly is a lint error in the web app.

Each product concept has one canonical icon, listed in packages/app-ui/src/icon/icon-dictionary.md with when to use it and when not to. For example: workspace IconBuildingSkyscraper, members IconUsers, billing IconCreditCard, permissions IconLock. The Markdown file is generated from packages/app-ui/src/icon/constants/IconDictionary.ts with npx nx generateIconDictionary app-ui.

Icons are passed to components as components, not elements: Icon={IconPlus}.

Light and dark

  • Every semantic token has its own light and dark value. ThemeProvider puts a light or dark class on the page's root element, and the matching CSS variables apply.
  • In the app, Settings → Appearance offers System, Light and Dark. The choice is saved in the browser (local storage). The command menu (Cmd/Ctrl + K) can switch it too.
  • Clerk's sign-in screens read the same CSS variables, so they follow the theme.
  • The landing page is light only, on purpose.

The landing page's brand color

The landing page (packages/app-website) wears one brand color, taken from one of the design system's color scales. It is set by one line, in packages/app-website/src/modules/brand-color/constants/BrandColorScale.ts:

ts
export const BRAND_COLOR_SCALE: BrandColorScale = 'blue';

The scales on offer are blue (the default, the app's own blue), cyan, turquoise, grass, tomato, red, bronze and gray. The type accepts only these names. A test checks, for each of them, that white text on the brand fill and brand-colored text on the white page keep a contrast ratio of at least 4.5 to 1 (the accessibility minimum for body text). Orange, yellow and amber are not offered because their readable shades fail that test or turn brown.

To try colors before you choose:

  1. Start the landing page: npx nx start app-website, then open http://localhost:5190.
  2. Click the "Brand color" button in the bottom-left corner.
  3. Pick a scale. The page recolors at once, and your pick is remembered in this browser.
  4. The palette shows the exact line to paste into BrandColorScale.ts, with a copy button.

The picker only exists in development. The build and the published pages never contain it.

The app itself uses the design system's blue as its accent. It has no one-line switch like the landing page: changing it means editing the tokens in packages/app-ui/design-tokens/ (the accent steps are in accent.ts), regenerating the theme, and reviewing both themes in Storybook.

Rename the product

The product name is one constant, PRODUCT_NAME, in packages/app-shared/src/constants/ProductName.ts:

ts
export const PRODUCT_NAME = 'Neatship';

Change it, then rebuild the shared package: npx nx build app-shared --skip-nx-cache. The constant feeds:

  • the web app's browser tab title, and its description for search engines and shared links ("The <name> web app."). packages/app-front/index.html has neither: the plugin product-name-in-html in packages/app-front/vite.config.ts adds both when the web app is built or served, and that is where you change the description's wording;
  • the name above the sign-in card, and its first letter used as a small logo;
  • the "about" panel on the app's home page;
  • the landing page (header, footer, pricing, page titles);
  • the names and ids of your Stripe products (<name> Pro, <name>_pro) and of the customer portal configuration.

Warning: Rename the product before you run npx nx run app-server:billing:sync-catalog for the first time. The Stripe product ids contain the name: a rename afterwards creates new products and prices, and leaves the old products in Stripe.

A few places don't read the constant. Change them by hand:

PlaceWhere
The name in Clerk's sign-in texts and emailsthe application name in the Clerk dashboard
The business name on Stripe Checkout and the portalStripe Dashboard → Settings → Public details
The repository's descriptionREADME.md, and the first lines of CLAUDE.md

The landing page content

packages/app-website holds the landing page, in English (/) and French (/fr/), prerendered so search engines and visitors without JavaScript get the full page. Its copy currently presents Neatship itself: replace it with your product's story.

  • Each section has its own folder in packages/app-website/src/modules/landing/ (for example hero, features, pricing, faq). Its words are in the section's constants/ folder, written as Lingui msg messages so they can be translated.
  • The price shown is in pricing/constants/LicenseOffer.ts, and what the offer includes in pricing/constants/PricingContent.ts.
  • The figures (components, icons, lint rules, tests) are in key-figures/constants/KeyFiguresContent.ts. Update them when your product changes.
  • The page title and description for search engines are in packages/app-website/src/modules/seo/constants/SeoContent.ts.
  • Three things are this website's own and must become yours before going live: the contact address CONTACT_EMAIL (packages/app-website/src/modules/landing/constants/ContactEmail.ts), the "Built by" line in the footer's FooterContent.ts, and the legal pages (legal notice, privacy policy, terms of sale): one Markdown file per page and language in packages/app-website/legal/, to replace with your company's, ideally checked by a lawyer.

The landing page reads four settings from packages/app-website/.env:

VariableUsed for
VITE_WEBSITE_URLwhere the site is served: canonical links, language alternates, sitemap
VITE_APP_URLwhere the app runs; the "Live demo" links use it when VITE_DEMO_URL is empty
VITE_DEMO_URLa public demo, if it runs somewhere else
VITE_PURCHASE_URLthe payment link; empty shows the button as "Available soon"
bash
npx nx start app-website                 # development server on http://localhost:5190
npx nx build app-website                 # prerendered site in packages/app-website/build
npx nx preview app-website               # build, then serve it on http://localhost:5191
npx nx run app-website:lingui:extract    # after changing copy
npx nx run app-website:lingui:compile

After changing copy, translate the new French entries in packages/app-website/src/locales/fr.po (home page, header, footer) and packages/app-website/src/locales/feature-pages/fr.po (the feature pages); see Translations. yarn verify lists any entry still missing.

Host the website on Vercel instead

The production stack serves the website from your server (the website container). If you prefer Vercel, packages/app-website/vercel.json already holds the build settings and the same security headers. In Vercel, import your repository as a new project, then:

  1. Root Directory: packages/app-website.
  2. Environment variables: VITE_WEBSITE_URL, VITE_APP_URL (or VITE_DEMO_URL) and VITE_PURCHASE_URL, with your real addresses, plus ENABLE_EXPERIMENTAL_COREPACK=1 (Vercel then installs the exact Yarn version of the repository).
  3. Domains: add your website's domain to the project.

Every push to main then rebuilds the website. The website container still runs on your server, but nothing reaches it once WEBSITE_DOMAIN points at Vercel.