Empty states
Two production empty / fallback patterns ship today, both inside the API demo response panel: an initial empty state (the panel before any request runs) and an error state (when a request fails or returns a malformed shape). Beyond these the site has gaps — the blog index, for instance, would render an empty <ul> with no fallback if no posts existed — documented at the bottom so they don’t get forgotten.
1 · Initial empty state
Renders inside both API-demo response panels on first load, before the user has clicked run or sample data. Two-line invitation centred in a tall placeholder zone — a heading-weight title and a sentence pointing at the two action buttons above. Replaced by the actual response (or by the error / loading states) once the user kicks off a request.
API demo — initial state
.api-demo__empty · .api-demo__empty-title · .api-demo__empty-body
Ready when you are.
Hit Try Neuwo Analysis to see live classification, or start with Sample data to preview the shape.
src/css/api-demo/api-demo.css:846. Flex column, centred, min-height: 22 rem so the panel reserves space for the eventual response — layout doesn’t reflow when the result lands. Title uses --font-size-lg, weight 500, heading colour; body uses --font-size-sm, muted, max-width: 24 rem so the line length stays readable. Pulls double duty as an empty state and a soft call-to-action, which is why the body explicitly names both buttons — users land on the page mid-scroll and need to be told what to click.
2 · Error state
Replaces the empty / response content when a request fails — network error, non-2xx status, or malformed JSON. Same panel position as the initial state, but with red-tinted chrome and a three-line hierarchy: title (label), message (what failed), hint (what to try next). The hint copy differs by mode: live runs suggest checking the URL or trying sample data; sample-data failures point at a likely build-pipeline issue.
API demo — error
.api-demo__error · .api-demo__error-title · .api-demo__error-message · .api-demo__error-hint
Request failed
503 — service temporarily unavailable.
Double-check the URL or try the sample data button to preview the response shape.
src/css/api-demo/api-demo.css:365. Padded glass surface with a red-tinted border, min-height: 16 rem. Title is uppercase, weight 700, red. Message is --font-size-sm in body colour. Hint is --font-size-xs in muted colour. The component carries no dismiss affordance — the next successful request replaces the error in place, which feels more like a temporary banner than a modal.
3 · Coverage gaps
Surfaces that could render an empty state today but don’t. Documented here so they get a real treatment when production shows up rather than a blank container.
- Blog index — src/blog.njk renders
{% for post in collections.blog | reverse %} with no {% else %} branch. With zero posts the page would render an empty .post-list and nothing else. Add a small “No posts yet” placeholder in the same shape as the API-demo empty state when the collection is empty.
- Search — the site has no search affordance today; if one ships, the zero-results state should follow the API-demo empty recipe rather than inventing a third one.
- Solution-page live demos — the bid-enrichment and contextual-audiences cards use skeleton placeholders while loading, then swap to populated content. They don’t have an explicit empty state because they’re hard-coded to populated data; if either becomes data-driven, reach for the API-demo empty recipe.
Onboarding
First-visit and persistent-dismissal surfaces. The site has exactly one today — the announcement bar — documented as a persistence pattern here. The visual recipe (markup, layout, dismiss button) lives under Components › Navigation; this section captures the storage contract and the lifecycle so future first-visit surfaces (product tours, cookie consent, feature callouts) can reuse the same shape.
1 · Persistent dismissal — announcement bar
The announcement bar is initially hidden at the markup level. JS in src/js/announcement.js reads localStorage.getItem("neuwo-announcement-dismissed") on load: if a key is present, the bar stays hidden for the rest of the session — if not, the hidden attribute is removed and the bar appears. Two click targets dismiss it and write the key: the close button (explicit dismissal) and the announcement link (implicit dismissal — once the user has acted on the offer, there’s no value in showing it again).
// src/js/announcement.js
const STORAGE_KEY = "neuwo-announcement-dismissed";
if (localStorage.getItem(STORAGE_KEY)) return; // never seen again
bar.hidden = false;
closeBtn.addEventListener("click", () => {
localStorage.setItem(STORAGE_KEY, "1");
bar.hidden = true;
});
linkBtn.addEventListener("click", () => {
localStorage.setItem(STORAGE_KEY, "1"); // implicit dismissal
});
Lifecycle
localStorage["neuwo-announcement-dismissed"]
- First visit — key absent. Bar reveals;
hidden attribute removed.
- User dismisses — click on close or on the announcement link. Key set to
"1"; bar re-hidden.
- Subsequent visits — key present. JS exits early, bar stays hidden.
- New campaign — bumping the storage key (e.g.
"neuwo-announcement-dismissed-2026q2") resets the audience: every visitor sees the next bar exactly once.
Why localStorage and not a cookie. The dismissal carries no server-side significance — the announcement is purely a marketing surface, and the persistence is a kindness to repeat visitors rather than a tracking signal. localStorage is per-origin, persists across sessions until the user clears site data, and never leaves the browser, which is the right contract for this kind of UI state. Pattern reuse. The same shape (single key, set on dismissal, checked on load) covers any future first-visit nudge — product tour, cookie banner, feature callout. Pick a stable key per surface, namespace it under neuwo-, and bump the suffix when the message changes.
Cross-refs. Visual recipe for the bar itself — markup, layout, breakpoint behaviour, dismiss button — lives under Components › Navigation › Announcement bar. The link recipe inside the bar is documented under Components › Links.
2 · Coverage gaps
No other first-visit / onboarding surfaces ship today — no cookie banner, no product tour, no feature callouts, no “what’s new” modal. If any of these land later, document them here against the same persistence contract above so the keys are namespaced consistently.