Bullet component catalogue

Single source of truth for every UI element. Build screens only from what's here. Each block: a live example (all states) + an HTML comment with NAME · WHEN TO USE · SNIPPET.

Foundation

Typography & amounts

h1 page title · body 16px · .text-muted 14px secondary · .amount for € (tabular-nums).

Your VAT return is ready to file

Body text at 16px Inter. VAT due is €0.00.

Secondary text at 14px — metadata and subtext.

€1,300.00 / €4,021.70 / €894.01

A text link — tab to it for the focus ring.

Buttons & actions

Button — .btn

--primary main action (one per view) · plain secondary · --small toolbars/menus · --icon icon-only · --danger destructive · .is-applied green "✓ applied" confirmation JS sets for ~900ms after a money action commits, before the view moves on.

Form controls

Text input / field — .field + .input

Every input wrapped in .field with a .field__label. textarea.input for notes. Error = .field--error / aria-invalid.

Enter a valid VAT number.

Form controls

Dropdown (select) — .tx-dd · THE STYLE GUIDE

Placeholder state (nothing chosen) — is-placeholder on the button:

Dropdown "+ New …" creation row

Searchable client field (combobox) + Add Client drawer

Filtering / open-on-focus / pick-fills-input + the add-drawer JS live inline in invoice-create.html. The SAME pattern drives the line-item Item field (data-combo="item", one per card, clone-safe): picking an item fills its default rate + VAT; "+ New item" opens window.openAddLineItemDrawer({onSave}) — a GLOBAL right-docked Add Line Item drawer in ui.js (Item name / Default rate / Unit / VAT / Description), also wired to settings-invoice-line-items' "+ Add line item". -->

Form controls

Icon field — .icon-field

Form controls

Checkbox — .checkbox

Form controls

Radio — .radio

Form controls

Toggle switch — .switch

Instant on/off setting. Checkbox = include-in-set; switch = turn-setting-on.

Form controls

Chip group (segmented) — .chip-group

Segmented single-select (selected = solid dark). For switching views/panels use Tabs below.

How would you like to pay?

Form controls

Tabs — .chip-row--tabs

Switch between sections/panels of one screen/drawer. Active tab = white/raised.

Form controls

Upload box — .upload-box

Form controls

File drop (multi-file) — .file-drop

Several documents on ONE question. Single fixed slot (logo, banner) = .upload-box above.

  • medical-receipts-jan.pdf240 KB
  • scans.zipOver 10 MB
  • p60.pdfAdd this one again

Data display

Inbox — .inbox-row / .inbox-doc / .inbox-orig

Paperwork that has arrived and isn't filed yet. One engine — the business pile and the personal pile both mount it.

2 documents

Emailed in

Vodafone — Pay Monthly

Mountainview, Leopardstown, Dublin 18

Account087 555 0142 Red Unlimited€35.00 VAT (23%)€8.41 TOTAL€45.00

We read €45.00 on 20 Jun 2024 from Vodafone. Not tied to anything in your bank yet.

App shell

Profile chip + switcher — .pchip / .pswitch

On every screen. Which books am I in, and switch. Injected — never hand-written into a page.

Your companies


You

App shell

Offer card — .offer (personal-onboarding)

Bullet Personal's sales pitch only. A thing in the offer + its price + one sentence.

Your own bank Free forever

Connect your own account — not your employer's, and they can never see it.

Data display

Data table — .data-table

UserEmailTypeAccess
Suzannesuzanne@bullethq.comAdministratorFull access
Heidiheidi@brady.ieMemberFull access

Data display

Missing-info status card — .txd-status / .txd-field--missing

GLOBAL (31 Jul): every record detail panel wears this card under its title — returns, invoices, payslips — with linkable items that take you to the fix. New quiet variant .txd-status--wait for "filed, waiting on Revenue".

Not sorted yet3 missing
Not sorted yet1 missing
Sorted

Field states — normal vs missing-highlight:

Data display

Data grid (spreadsheet lens) — .data-grid / .view-toggle / .data-gridbar

Card ⇄ Grid lens for power/bulk work. Same dataset + filters as the cards. States shown: complete (quiet tick), gaps (loud), selected, filed/locked.

Bulk edit3 selected
Sorted 24 Feb Marriott Hotel Travel 23% -€280.00
No receipt 24 Feb Spar General Expenses 9% -€24.50
Flagged 23 Feb Amazon Web Services Software 23% +€1,200.00
Filed 18 Feb Riverside Joinery Ltd Subcontractors 13.5% -€960.00

Data display

Row card (list row) — .row-card

Data display

Stat cards — .stat-cards / .stat-card

Data display

Section title (pink chip heading) — .section-title

Heading of every content section / drawer sub-group. Always the pink chip — never plain bold text. --sm for group labels; button.section-title for collapsible.

Returns status

Invoice payment

Data display

Year calendar month card — .taxmo / .taxdl

12 month cards · current month marked · handled shown but quieter · red only for genuinely overdue · empty months get copy, never blank.

JulyThis month
VAT return19 Jul Needs you5 min
Payroll return14 Jul Handled
August

Nothing due. Go on your holidays.

Data display

Pill / status — .pill

--paid/--active/--on green · --dashed "needs info" prompt · plain = neutral tag.
STATUS states, for an at-a-glance read beside a record's title: --paid green = done/nothing to do · --wait amber = waiting on someone else · --danger red = needs you · --muted grey = not started.

Paid Active Connected General expenses + receipt VAT 9%?

Record status (return header pill):

Filed & paid Waiting for Revenue Payment not detected Not filed

Lists & nav rows

Todo / row button — .todo / .todo--card

Full-width tappable row (icon + heading + chevron/check). Use for hub links, drawer action lists (Mark as sent / Pay with X / Download), and checklist items. The Figma "Button on/off" card.

Download invoice

PDF for your records

Payroll welcome

Done

Sole trader payment

Locked until the step above is done

Overlays & panels

Filter panel / drawer — .filter-panel

Right-hand panel — filter form or a task/settings drawer. --drawer is fixed-right; the page <main> needs app__main--with-drawer. Fill the body with section-title chips + todo--card rows. Spacing (global, space + calm): every docked drawer uses the drawer tokens — --drawer-pad 32px, --drawer-pad-bottom 48px, --drawer-section-gap 24px, --drawer-row-pad 18px, --drawer-row-gap 14px. Never hand-pick drawer paddings (see CLAUDE.md "Drawer spacing"). Motion (global): slides in on open; the X slides it back out with a little wind-up bump (ui.js + filter-panel.css). Close button: data-close-href / data-close-back / nothing (hide).

Overlays & panels

Filter — "Unlinked" chip + summary — .fseg / .fsum

Unlinked surfaces floating records the accounts engine can't square — a deposit/transaction not linked to an invoice, or an invoice not linked to a payment. It's a normal stackable filter chip. Built as a segmented .fseg group (Invoices) / .tx-filtseg group (Transactions) with data-filter="unlinked" so the shared live-filter engine, orange section dot, removable chip and Clear all work with no extra wiring. Read the linked status per row — Invoices: data-unlinked on the row; Transactions: data-linked (absence = unlinked). TODO(backend): supply the real reconciled/linked status and stamp those attributes from it (both screens infer a demo value today — see their "Unlinked filter hook" comments). When active, show the plain-language .fsum__note line.

Unlinked (default)

Unlinked (active)

Showing unlinked items — not yet linked to an invoice or document.

+ €1,240 in · − €80 out · = €1,160 total5 transactions

Showing unlinked items — not yet linked to a payment or bank transaction.

€7,910 total7 invoices

Overlays & panels

Payroll dashboard rail + expense capture — .pr-dash / .exp-chip (candidate — promote to CSS)

Live on wages-payslips.html — subsistence rate chips (fixed Revenue rates fill the amount), the mileage drawer's band-split € calc, and the one-per-day allowance guard.

Owner-only tax-saved panel — .pay-saved (candidate — promote to CSS)

€8,141 saved in tax

Your €6,133 of mileage and expenses came back to you tax free. Take the same money as wages and you'd have handed Revenue another €8,141. Bullet works that out every time you run payroll.

Overlays & panels

Reset test data — src/js/payroll-reset.js (self-injecting)

Idle + armed states. Live on every payroll/wages screen, fixed beside the logout corner.

Overlays & panels

Invoice peek drawer — browse-in-place (#inv-peek-drawer)

Peek drawer — browse-in-place preview for filtered lists: clicking a row opens/swaps this drawer (totals + What happened, newest first) instead of navigating, so the filter and scroll position survive. Same chrome as every drawer (back arrow, breadcrumb title, X, --drawer-* rhythm, sticky footer). The browsed row gets the yellow inset selection ring. Reference build: invoice-home-settings.html (openPeek/closePeek).

INV 1033

Kavanagh & Sons Builders · Overdue

Total€2,400
Paid€1,100
Left€1,300
8 Jul 2026Deposit 001−€300
15 Jan 2026Payment 001−€800

Overlays & panels

Receipts drawer — window.openReceiptsDrawer()

Per-transaction receipts manager: pink Add-documents dropzone (with a "Get the app" QR), Forward-receipt takeover (receipts@bullethq.com), and a receipts list with All/Matched/Unmatched tabs (default Unmatched) — each row a checkbox (attach to this transaction), a thumbnail (click → preview modal) and a kebab (Download / Detach / Delete). Built + opened by openReceiptsDrawer(cfg) in ui.js — never hand-build it. Wired into bank-transactions (row paperclip + detail "Add receipt").

Overlays & panels

Guided tour + help chat (demo) — window.startGuideTour() / window.openDemoChat()

Demo customer-help assistant: the bottom-right help bubble opens a chat; typing “how do I attach a receipt?” makes it run a coach-mark walkthrough over the real bank-transactions UI — spotlight the paperclip → tick a receipt → done. Engine is generic (startGuideTour(steps)); intent matching + the receipt script live in ui.js. No backend.

Overlays & panels

Are you sure? — window.uiConfirmAction

Only for what cannot be undone. States the consequence with real figures; the button repeats the action.

Overlays & panels

Toast — uiToast()

Cards & content blocks

Video / welcome card — .welcome-card

Bullet

What is payroll?

Cards & content blocks

Plan card — .plan-card

StandardActive€89 / month

All-in, unlimited users. Renews 1 July 2026.

Cards & content blocks

Note card (callout) — .note-card

Your accountant gets a free guest account — it doesn't use a paid seat.

Cards & content blocks

Danger zone — .danger-zone

Cancel subscription & close account

Your account stays open until the end of the billing period. Export your data first.

Cards & content blocks

Avatar — .avatar

PB

Cards & content blocks

Walkthrough — the journey, the chooser and the demo item

The demo item — marked, and never saved

Practice run. You are looking at a made-up transaction. Nothing here is saved, and your own records are untouched.
Practice Supplier Demo General expenses · −€40.00

The chooser — one question, several meanings

Change an invoice

That could mean a few different things. Which one are you after?

A journey step — says what is next, then moves

STEP 1 OF 2
Go to the lines

The VAT lives on each line of the invoice, so that is where we are headed.

The flash the step lands on — uiHighlightField / .txd-field--flash

VAT rate
23%

Cards & content blocks

Answer figure cards — .answer__figs

You are owed €12,907 right now.

That is across 10 invoices. About €6,907 of it is late.

Owed to you€12,907across 10 invoices
Overdue€6,9075 invoices past their date
Due but not late€6,0005 invoices
Oldest34 daysJavelin Payments

Cards & content blocks

Ask box — .ask-box · THE GLOBAL SEARCH BOX

Cards & content blocks

Ask bubble — .ask-float · THE SAME BOX, FLOATING

How much am I owed?

You are owed €12,907 right now.

Across 10 invoices — €6,907 of it is overdue.

what about the vat

vat

Did you mean:

Cards & content blocks

Ask by voice — .voice · THE VOICE SKIN ON THE ASK ENGINE

idle

Morning, Peter. Ask me a question about your business.

Push to talk
listening

Listening…

thinking

One second…

“How much am I owed?”

speaking / answered

You are owed €12,907 right now.

You are owed €12,907 right now.

low confidence

Not sure I caught that — did you mean one of these?

Did you mean:

couldn't hear

I didn’t catch anything — tap the mic and try again

mic denied

Bullet can’t hear you — your browser has the microphone blocked. Allow the mic in your browser settings, or type instead.

offline / API failed

I can’t reach the listening service right now. You can type instead.

Reference

Feature map — preview/feature-map.html

Open the feature map →

Cards & content blocks

Status pill — .status-pill

Overdue Due in 6 days Done When you can Watching We are chasing it

Cards & content blocks

What needs you — .needs (+ the calm empty state)

Dashboard only. One card, one row per job, count leading. Rows tap through to the pre-filtered view — reuse the existing filter system, never a bespoke list.

EMPTY — the state that matters most:

Nothing needs you right now.

Your books are up to date and nothing is due. We'll put anything that comes up right here.

Cards & content blocks

Letter from Revenue — needs-row + notice drawer (.ntc-*)

ONE letter view — the TAX INBOX's document pane (tax-inbox.html: list → synopsis → the letter itself as a document page, .ntc-letter inside the shared .inbox-orig frame). Home's needs-you rows and the Ask box's "Read the letter" both deep-link tax-inbox.html?notice=<id> — the Home DRAWER was retired 13 Aug (Peter: bring them to the inbox; two renderings of one letter would drift). A Home row is earned by an action or a deadline; receipts/statements live in the inbox only. Feed: data/revenue-notices.js (sample) — on localhost a gitignored revenue-notices.local.js (the ros-agent's real letters) replaces it. No tax advice anywhere — what arrived, when it's due, talk to your accountant.

The drawer body (loud state — deadline inside 14 days, payment asked for):

Needs an answer by 22 Aug 2026 — 11 days away.

What you need to do

Check whether the payment left your bank. If it did, match it in Bullet so the books agree. If it did not, the amount below is what Revenue are waiting on.

What it’s about

The payment for your March–April VAT return has not reached Revenue.

If you need to pay

€2,140

Through ROS (Revenue Online Service) · by 22 Aug 2026

Received 8 Aug 2026 · Deadline 22 Aug 2026

Not sure what it means? Talk to your accountant.

Cards & content blocks

Tax block — .dash-aside / .dash-claim / .dash-next

Dashboard Tax section. Verdict line always does the arithmetic. No gamification on the claimed figure — ever.

Set aside in your tax account€37,006 Owed to Revenue€9,675

You're covered — €27,331 more than you owe.

Set aside in your tax account€5,475 Owed to Revenue€9,675

You're €4,200 short. Move it across before the next return is due.

Tax reduced this week €312.40 Expenses and VAT you've already claimed. It's yours — you don't need to do anything with it. 31 Oct·Form 11·~5 min 14 Jul·Payroll return · Jun 2026·overdue · ~2 min

Cards & content blocks

Cash over time — .cashchart

Dashboard only, and only cash. Always ship the __basis footnote — an unexplained chart is worse than no chart.

Lowest point in the next 90 days€119,524 · 27 Jul Balance If nothing you're owed comes in
Today Aug Sep

Built from your €129,199 bank balance, €9,675 of tax already filed or due, and €12,907 of unpaid invoices placed on their due dates — anything already overdue is assumed to land within a fortnight. It's cash only, straight off your bank, so the 23 transactions still to categorise don't change it. It doesn't include anything you haven't invoiced or scheduled yet.

Cards & content blocks

Setup path — .setup (pre-onboarded dashboard)

Pre-onboarded dashboard only. Three steps, progress visible, each opening the real flow. No €0 cards anywhere on this screen.

Cards & content blocks

Answer — .answer (Ask surface + weekly summary)

Ask surface + weekly summary only. One container for every answer. Max two numbers, plain voice, chart/link/follow-ups all omittable.

Full — chart, "show me" link + follow-ups

You are owed €12,907 right now.

That is across 10 invoices. About €6,907 of it is late, so a quick nudge would help.

See the 5 overdue invoices

Minimal — headline + body only

Yes — you have €37,006 put by for tax.

That covers what is coming up. Keep new sales money going in there and you will stay ahead of it.

Loading — calm skeleton, no spinner

Can't answer — no apology, a ready-made question to paste elsewhere

That is not something your books can tell you.

Bullet only knows what is in your account — your money, invoices, bills and returns. For anything wider, here is a question you can paste into your own AI or a search, with your figure filled in.

Copy this and paste it anywhere

For a small business in Ireland with about €12,907 owed by customers, what should I be thinking about?

Did you mean — near-miss chips instead of a dead end

I am not sure which you mean.

Did you mean one of these?

How-to — hands off to a walkthrough instead of answering with figures

Create an invoice.

Easier to show you than tell you. I will bring you to the right screen and point at what to tap — you can stop any time.

Cards & content blocks

Run status marker + steps — .pr-status / .pr-return / .pr-next (candidate — promote to CSS; lives inline on wages-payslips)

ONE icon family — the circle is constant, what's inside carries the state. Same r=10 geometry, stroke-1 (global icon rule), at 19px on card headers (.pr-status) and 15px in the pill row (.pr-return), so the two always agree.

ok — done wait — needs you na — nothing to do

The WAITING state keeps a filled dot inside the ring, not a bare outline: at 19px a stroke-1 ring is too quiet for the one thing that needs attention. Colour does the shouting. Rendered by statusMark(steps) / markHTML(state, tip) / returnsRow(steps) on wages-payslips.

Cards & content blocks

Timeline receipt — .tl-receipt + chevron .tl-chev (candidate — promote to CSS; lives inline on invoice-detail)

Payment receipt 001€450
DateToday
Received intoAIB — Current
AgainstInvoice 1005

Cards & content blocks

Invoice line-item card — .li-card (in invoice-line-item.css)

Item
All invoice discount
€0.00 VATSubtotal: €0.00 Total: €0.00

Layout / shell

App shell, sidebar, submenu, page header

Every in-app screen = .app > .sidebar (+ optional .submenu) + .app__main + .page-header + floating .app__corner. Copy the shell from an existing screen; never hand-roll nav. Canonical: src/screens/settings-company.html.

Sidebar + submenu + corners render full-screen — see any file in src/screens/.

Projects — card + CFO panel

Riverside rebrandActive Riverside Joinery Ltd €11,300In €8,842Out €2,458Net 49% of €18,000 · 76% of the timeline

What your accountant would tell you

You’ve invoiced €11,300 and spent €8,842 — you’re €2,458 ahead so far.

That’s 49% of the budget used, with 76% of the timeline gone.

Email connect — shared flow (#emailconnect)

Live demo: the button opens the mock OAuth moment (Connecting… → Google-ish chooser). Rows + kebab actions are on settings-connected-emails.html. Never build a second connect UI — call the shared flow.

Report drill-down drawer (#reportdrill)

Live on report-profit-loss.html + report-balance-sheet.html — click any figure. Never hand a report page its own drill UI — the layer is the one module, and the transactions are derived from the page's own data object so they can't disagree with the table.

← the "has a kept note" marker that sits beside a figure.

Proposals — slice 1, dark (#proposals)

Flag off by default. Flip with ?proposals=on, then: the board · create · the customer page.