NitroApplyDesign system

Design system

Every shared component in the shapes it actually ships in. Two color scales, one type scale, Tailwind utilities only — no custom CSS beyond the tokens in app.css. Build new screens out of what is on this page, and add anything new here first.

Colors

Two scales carry the whole product: background for every surface, border and piece of text, accent for anything interactive. The accent is green, so nothing else is: a state that went well is an accent state. Beyond those, amber and rose for feedback, and two hues the application statuses spend on themselves because a status list is only worth coloring if the colors tell it apart. A section that wants weight goes dark on the background scale rather than solid accent, which would leave the buttons on it a shade of their own color.

Background scale

bg-background-50 … bg-background-950

50

100

200

300

400

500

600

700

800

900

950

Accent scale

bg-accent-50 … bg-accent-950

50

100

200

300

400

500

600

700

800

900

950

Status colors

Feedback only — never for branding or navigation. Success is the accent.
warning
danger
info · statuses only
active · statuses only

Roles

Pagebg-background-100
Surfacebg-white
Muted texttext-background-600
Borderborder-background-300
Actionbg-accent-600
Action tintbg-accent-50
Inverted bandbg-background-900

Typography

Open Sauce One at every weight the app needs. Headings are bold and sit on background-900, supporting copy steps down to background-600.

Type scale

Hero

Applications that look the part

text-4xl font-bold sm:text-5xl

Section

Applications that look the part

text-3xl font-bold sm:text-4xl

Page

Applications that look the part

text-2xl font-bold

Card

Applications that look the part

text-lg font-semibold

Lead

Applications that look the part

text-lg text-background-600

Body

Applications that look the part

text-base text-background-700

Small

Applications that look the part

text-sm text-background-600

Caption

Applications that look the part

text-xs text-background-500

Eyebrow

Applications that look the part

text-sm font-semibold tracking-wide uppercase text-accent-600

Brand

md in every bar, lg above the account cards
NitroApplyNitroApply

Prose

Motivation letter

Body copy sits on text-background-700 and stays below max-w-2xl so lines never run too long to scan comfortably.

Secondary copy drops a step in both size and contrast, which keeps a card readable without a second color.

Buttons

One primary action per view. Secondary carries the alternative, ghost the low-stakes action, danger anything destructive — and dangerGhost the same intent quietly, for the removes that repeat once per row down a list, where solid red would make deleting look like the point of the page. ButtonLink is the same surface for navigation, IconButton for compact toolbars.

Variants

variant="primary | secondary | ghost | danger | dangerGhost"

Sizes

size="sm | md | lg"

With icons

States

loading renders a spinner and blocks the click

Links

ButtonLink — same surface, renders a router Link

Switches

NavSwitch — one page out of a group, on a recessed track

Icon buttons

label is required and becomes the accessible name

Forms

Every control is a plain presentational component with a Formik-connected wrapper next to it. The wrapper owns the label, the error and the accessible wiring, so a route only names the field.

PasswordRequirements

app/components/PasswordRequirements.tsx — the rules, ticking off as they are met
  • At least 12 charactersstill missing
  • A lowercase lettermet
  • An uppercase lettermet
  • A digitstill missing

Formik fields

FormikInput · FormikSelect · FormikTextarea · FormikCheckbox

Shown in the header of your CV.

Optional, but at least 20 characters when you fill it in.

Nothing leaves this device until you export it.

Control states

Pass any Lucide icon as the icon prop.

Please enter a valid email address.

With an optional description.

Editing

Long forms are built out of these: a Disclosure per group of fields, an ItemCard per entry in a list, a Slider for the values that are easier to judge than to type, and RichText for the fields that may carry marks — inline only for a document field, blocks as well for the free-text fields a document is generated from. Every label the editors render is a translated string — the labels here are literal only because this page is not translated. The chrome around them — ApplicationSection, DocumentToolbar, EditorFrame, DocumentPreview and AutoSave — is shared by both document editors but cannot be shown here: it needs a live Formik context and a working preview endpoint.

Disclosure

app/components/Disclosure.tsx

Open by default. The panel is hidden, never unmounted.

ItemCard

app/components/ItemCard.tsx

Amberg Engineering GmbH

Since 2025

First in the list, so there is no move-up button.

Untitled entry

Nested one level down, on the tinted surface.

Slider

app/components/Slider.tsx · FormikSlider.tsx

Standalone

83%

RichText

app/components/RichText.tsx · RichTextToolbar.tsx · RichTextGuards.tsx · FormikRichText.tsx

Prints as a single line, so Enter is ignored here.

Enter starts another paragraph. Each one is stored on its own line.

Blocks as well as marks: three heading levels, two kinds of list and a line break, stored as line prefixes and a trailing backslash.

Standalone, invalid, with a placeholder

What gets stored

{
  "intro": "Frontend engineer with **ten years** in *product teams*, mostly around CO~2~ reporting.",
  "body": "Dear Ms Moreno,\nI read your posting for a **senior frontend engineer** with real interest.\nKind regards",
  "profile": "# Frederik Hummel\n## Experience\n### Amberg Engineering\n- **Senior frontend engineer**, since 2025\n- *Freelance*, 2019 to 2025\n## Education\n1. MSc Computer Science\n2. BSc Software Engineering\n## Contact\nBahnhofstrasse 1\\\n8001 Zurich"
}

LanguageField

app/components/LanguageField.tsx

The language the document is written in, not the language of this app.

FileInputButton

app/components/FileInputButton.tsx

Nothing picked yet.

Hidden label

labelHidden

For rows inside an already-labelled group, where a visible label per row would bury the content. The label still reaches screen readers.

Surfaces and feedback

Cards are the one container. Badges label a state in a list, alerts explain something that just happened, an empty state replaces a list with nothing in it yet, and a modal is a native dialog — so Escape, the focus trap and the inert page behind it come from the browser rather than from us.

Card

Plain card

Border, white surface, rounded-xl, p-6.

Hoverable card

Add a hover border through className when a card is clickable.

Frontend Engineer

Interview

Cards compose with everything else — this is a tracked application.

Badge

variant="neutral | muted | info | active | accent | accentSolid | warning | danger" — a state that went well is an accent state, there is no second green
DraftWithdrawnSubmittedInterviewingTailoredSentAccepted offerInterviewRejected

Alert

variant="info | success | warning | error" — info is neutral, success is the accent

Everything is stored locally

Your documents never leave this browser until you export them.

CV exported

The PDF has been generated and downloaded.

No backup yet

Download a JSON copy so you can restore your CV on another device.

Inverted band

bg-background-900 — for a section that wants weight, never a solid accent panel

Both editors are ready

The accent has the band to itself, so the primary button is the brightest thing in it.

Empty state

No motivation letters yet

Letters you write show up here, next to the CV they belong to.

Spinner

Sized through className, inherits the text color

Modal

app/components/Modal.tsx — a native dialog, opened with showModal()

Add an application

The description carries the sentence a title cannot.

Anything can go in the body. Escape closes it, focus stays inside it, and the page behind goes inert — none of which is written here.

ConfirmDialog

app/components/ConfirmDialog.tsx — for deleting an application and for replacing a document

Delete this application?

The application and both its documents go with it, and this cannot be undone.

StatusLine

app/components/StatusLine.tsx — one per page, text-xs like a field hint; it takes a message id, so these are the only translated lines on this page

Saved to your account

Saving…

All changes saved

Not saved. Check your connection and try again.

Writing it now. This takes a moment…

AiNotice

app/components/AiNotice.tsx — beside every button that sends data to Gemini

Writing documents sends your profile and this advertisement to Google Gemini, which processes them in the US.

Icons

Lucide, imported one file at a time through SVGR. They inherit the text color, so size them with size-* and color them with text-*.

In use

import FileUserIcon from 'lucide-static/icons/file-user.svg?react'
file-user
file-text
briefcase
graduation-cap
sparkles
pencil
trash-2
download
upload
languages

Sizes and colors

Application statuses

The one place the app spends color outside its two scales, on the grounds that here a color is the information rather than the styling. Read as a temperature: cold while it is nobody's move, warm while it is in flight, green once the news is good, grey once it is over by your own choice.

The ramp

statusVariants in app/utils/status.ts
ConsideringNobody’s move yet
DraftYours to finish
SubmittedWaiting on them
WithdrawnOver, by your choice
InterviewingA date in a calendar
Received offerGood news
Received rejectionThe one decision that was not yours
Declined offerOver, by your choice
Accepted offerThe job

StatusPicker

the status field — an ARIA listbox, not a select

The only control here that is not a native one, and the coloring is the whole reason: an option cannot be styled where it counts — Chrome and Safari on macOS draw the platform menu and drop it — so the options are the badges themselves. Focus stays on the trigger and aria-activedescendant tracks the highlight; arrows move, Enter picks, Escape closes, and a press anywhere else dismisses it.