Taric Support — UX Analysis (live walkthrough)
2026-07-15. Method: drove the authenticated app (app.taricsupport.com) via Chrome DevTools Protocol in a dedicated automation profile (%LOCALAPPDATA%\chrome-cdp-profile, port 9222, puppeteer-core). Flows exercised: landing → nomenclature tree → BE commodity page → origin filter → full duty calculation (2204 21 09 00, CN origin, €5,000 / 900 kg / 750 L) → additional-code picker → CH Tares page (2204.2150) → GB page → global search. Companion to multi-country-tariff-gap-analysis.md.
1. Information architecture
Landing (jurisdiction cards + recent changes)
└─ per jurisdiction
├─ Sections → Chapters → Nomenclature tree (search/tree)
├─ Commodity page /good/{code} ← the workhorse (3 tabs)
│ ├─ Measures
│ ├─ Validate declaration
│ └─ Calculate duties
├─ CH only: /tares-measures/{code} ← reduced, measures-only page
└─ Search (multi-source result hub)
Persistent top bar (never scrolls away): jurisdiction flag row (EU, BE, NL, FR, GB, CH, SE, IT, US, DE-locked — a padlock marks unsubscribed tariffs), date picker (point-in-time for the whole app), Import/Export movement toggle, global search, language menu, logout. Every screen is therefore always scoped by (jurisdiction, movement, date) — the three context dimensions our /tariff/{Source}/{Code}/{Movement} route also encodes, except TaricHive keeps the date inside the page.
2. Nomenclature tree
- Two-panel rows: dark code rail on the left (code chips only), description column right — codes are scannable in a straight vertical line, descriptions can wrap freely.
- Contextual evidence chips at both chapter level and per-row:
N Notes,E Explanatory notes,A ATRs,B BTIs,⚖ Case law,🏛 Legislation. One-letter chips per row keep the row compact. - Non-declarable intermediate ranges collapse to a single line ("2204 21 06 — 2204 21 09") until expanded — the tree stays short.
- Cross-references in descriptions are red links ("other than that of heading 2009", "subheading 2204 10") that jump to that heading.
- Footnote chips (
TN 701) inline next to the heading text, click = popup. - Navigating to a non-declarable code redirects to the tree view positioned on it; only declarable leaves open the commodity page. (TaricHive lets you open any code — fine, but the redirect is a good disambiguation.)
3. Commodity page (the workhorse)
Layout: breadcrumb (Section 4 / Chapter 22 / Heading 2204 / 2204 21 09 00) → tab strip (Measures | Validate declaration | Calculate duties) → right-aligned evidence chips + Favorite star → two-part header:
- Ancestry mini-table: the full parent chain with indent dashes, one row per level, with a copy button. Users always see why the code means what it means.
- Filter rail (right): Country of origin (autocomplete + "Remember" checkbox), National additional code (BE) / EXCISE (GB) — the slot is renamed per jurisdiction, Duty measure type, Preference code, Currency, and Pre-UCC/UCC radio. Filters apply live to the measure list below.
Measure area = two columns: left Supplementary unit → Duties → Additional duties → National taxes; right Restrictions and controls → Other measures. Same grouping TaricHive already implements — confirmed correct.
Row anatomy (excellent, worth copying literally):
- Duty chip leftmost in a fixed-width grey cell: € 32 / Hectolitre, 21%, 26.00 GBP / hl, Fr. 25 per 100 kg gross — the amount is always the anchor of the row.
- Source flag per measure block (EU flag on TARIC measures, BE flag on VAT/excise, UK flag on UKGT) — national vs union layer is visible at a glance.
- Geo area as a link (1011 — ERGA OMNES opens the member list), quota order numbers as links (order: 091512).
- Exclusions in italics under the area: "excluding US — United States, JP — Japan, …" (expandable when long).
- Certificate chips right-aligned (C 014, Y 929), footnote chips small and teal (CD 519, TM 01026), preference-code chips in yellow (100, 150, 300, 320 — pre-UCC codes; the UCC radio switches the code system).
- Per-row legislation icon (🏛) opens the legal basis. Expand all / Collapse all per group.
GB variant adds a yellow stale-data banner: "Warning: UK Tariff data for this commodity code is not available for this date. Currently shown data was valid on 14 Jul 2026." — data currency is communicated, never silently wrong.
4. Duty calculation flow (2204 21 09 00, CN, BE)
The calculator is a left-side form next to the live measure list (right side becomes the result). Observed behaviour:
- Quantity inputs are generated from the applicable measures' units — for wine:
Hl total alcohol / 20° Celsius,Net weight (Kilogram),Volume (Litre). After selecting excise-exemption code 0600, the Hl alcohol input disappeared — inputs are reactive to the measure set. This is the/goods/{code}/unitsserver pattern; unit labels are human phrases, not TARIC codes. - Customs value: radio
Calculate value(opens a transaction-value calculator) vsEnter value(€ input). Required-field errors turn the label red. - Additional codes are validated hard: calculation refused with an "Error in additional codes" banner until the national excise code is chosen. The magnifier opens a picker modal listing only the codes applicable to this commodity (0600, S-codes…). Selecting one auto-filled Duty measure type = Third country duty and Preference code = 100 Erga Omnes.
- Result table (right, replaces top of measure list):
| Line | Value |
|---|---|
| Third country duty € 32/Hectolitre | € 240.00 (750 L → 7.5 hl × 32 — unit conversion L→hl done for the user) |
| Additional duties 0% | € 0.00 |
| Duties | € 240.00 |
| Excise / special excise / packaging levy (code 0600) | € 0.00 |
| VAT rate (PLDA) 21% | € 1,100.40 (21% × (5,000 + 240) — VAT base = customs value + duties) |
| National taxes | € 1,100.40 |
| Total | € 1,340.40 |
The calculation order is spelled out line by line — users can audit it. This exact trace (duty → excise → VAT-on-value-plus-duty) is the reference behaviour for DutyCalculatorService.
5. A customs-value plausibility check panel appears above results — but it leaked raw i18n keys (customsValueCheck.CustomsValueCheck, …NoDataIsAvailableRegardingTheAverageCustomsValue). The feature (compare entered value to average import values) is good; the execution is buggy.
5. Country divergences in the UI
| BE/EU | GB | CH | |
|---|---|---|---|
| Tabs | 3 (Measures/Validate/Calculate) | 3 | Measures only — no calculator, no validation |
| Filter rail | National additional code | EXCISE slot | Country of origin only |
| Duty strings | € 32 / Hectolitre |
26.00 GBP / hl |
Fr. 25 per 100 kg gross (weight+factor+currency verbalised) |
| Extras | Excise breakdown per additional code | Stale-data warning banner, Channel Islands (1080) row | Statistical keys panel (803/804/805…, indent hierarchy, "Choose a statistical key to see more information") |
| Group names | TARIC areas | UKGT areas | Country groups resolved to names ("European Union", "EFTA", "Least developed countries") |
The per-rate quota remark from Tares ("port wine, within the limits of the preferential tariff quota no. 115 (s. 'Remarks', 'Tariff Quotas')") is shown as italic text inside the duty row — confirming these texts must be imported (currently dropped by ChImporter).
6. Search
- One global search box; results page is a hub with 11 source tabs: Nomenclature headings, CN explanatory notes (EU), HS explanatory notes (WCO), WCO classification opinions, BTIs, Classification regulations, Case law, Alphabetical index, Advance Tariff Rulings, Customs rulings (CROSS), Chemicals search, CCC conclusions.
- Results grouped per heading with matched terms highlighted and a BTI count per heading ("11479 binding tariff information decision(s) found") — classification evidence is quantified right in the result list.
- Refinements: search-in-text + commodity-code fields, order by relevance/code, "also search in" checkboxes (BTIs, classification regulations, case law).
7. Weaknesses (where TaricHive can do better)
- Raw i18n keys leak in the customs-value check panel — QA gap.
- Untranslated national data noise: the BE additional-code picker shows rows of "The content of this item is available in Dutch and French" — placeholder text instead of falling back to the NL/FR description. TaricHive's language-fallback chain (
TariffHelpers.Desc) already does this better. - Dated AngularJS visual language: dense, small hit targets, no dark mode, flags-only jurisdiction switcher (learnability/accessibility — a padlock on a flag is the only subscription cue).
- Validation errors appear as a banner far from the field (additional-codes error at top of form, red label at Value) — no inline hints, no scroll-to-error.
- The calc form silently resets duty-measure-type/preference when codes change (state coupling is invisible to the user).
- Search autocomplete didn't surface suggestions inline in our run; full results require Enter → page swap.
- CH page loses all calculator functionality instead of offering a reduced (duty-only) calculation, even though CHF/100kg gross × gross weight is trivially computable.
8. Recommendations for TaricHive (mapped to existing code)
Adopt (high value, low effort):
1. Duty-chip-first row layout — amount in a fixed-width leading cell, geo link, right-aligned certificate chips, italic exclusions. TariffDetail.razor is already close; the missing piece is real units from the importer fixes (26.00 GBP/hl, Fr. 25/100 kg gross).
2. Ancestry mini-table with copy button above the measures (we have AncestorCodes() already — render it).
3. Stale-data banner per source: compare TaricSyncTracker last-import date vs requested date, show "data valid on {date}" instead of silently serving old data.
4. Remember-origin checkbox + persisted per-user origin (AppSetting or localStorage).
5. Quota order numbers as links to a quota panel (BE/SE quota data is already in SQL; GB/NO after roadmap #11).
6. Reactive calculation inputs from DutyTerm units (report §7): render one input per distinct (unit, qualifier) of the applicable measures, phrased in words ("Net weight — Kilogram", "Volume — Litre"), and drop inputs that stop being needed when codes/preferences change.
7. Line-by-line result trace with explicit VAT base = value + duties (matches the verified €1,340.40 example).
8. Additional-code picker modal listing only codes applicable to the commodity (query: distinct additional codes on the code's measures), with hard validation before calculating.
9. Statistical-keys side panel for CH once statistical keys are imported (TariffBaseMasterData_v2/Tares keys), same indent-hierarchy presentation.
Do better than Taric Support: 10. Language fallback for national descriptions (already implemented — keep it). 11. Inline field-level validation with scroll-to-error instead of top banners. 12. Keep the CH calculator alive: gross-weight-only calculation is fully computable after the CH importer fix. 13. Jurisdiction switcher with names + flags (not flags alone), dark-mode aware (paper-graphite design system already supports it). 14. Point-in-time date: TaricHive's in-page date picker is fine, but mirror it in the URL for shareable links (TS keeps date global but not in the URL — a shared link loses the date context in both apps; putting it in the query string wins).
Skip: the customs-value plausibility check (needs an import-statistics dataset we don't have); pre-UCC/UCC dual code display unless Belgian users ask for it.
9. How the two UIs are constructed
9.1 Taric Support — anatomy
- Stack: AngularJS 1.x + Bootstrap 3 + FontAwesome, browserify bundle, angular-toastr, intro.js for guided tours, Sentry. Templates precompiled (
templates.*.js). No SSR; hash routing (#!/good/…). - Layout system: full-width fluid grid; the commodity page is a Bootstrap row with two ~50% columns of stacked measure "cards" (header + table). The filter rail is part of the page header block, not a sidebar — it scrolls away with the header.
- Component vocabulary: everything is a chip or a table row. Chips carry all semantics by colour: grey = duty amount, teal = footnotes (
CD/TM), yellow = preference codes, white-outline = certificates, green = selected additional code. Rows are<tr ng-click>toggles with hidden expansion rows. - Semantics are weak: the three page tabs are literally
H1[ng-click]elements (not links, not buttons, norole="tab"), so keyboard/AT users can't reach them — one reason automation needed coordinate clicks. Contrast that with Tolltariffen, where every icon has anaria-label. - Server-composed display strings: duty rates, group names and remarks arrive as final text; the client only decorates (
formatDutyadds<sup>/<sub>and line breaks). All heavy lifting (grouping, unit phrasing, exclusion lists) is API-side — that's why 10 tariffs share one UI with almost no per-country templates (CH's tares page and the per-country filter-rail slots are the exceptions). - State model: one global
applicationStateService.filter(referenceCountry, movement, date, origin country) persisted via a preference service; every data call re-sends the whole filter. This is what makes the persistent top bar work: change any dimension anywhere → whole page refetches. - Palette/typography: dark navy header (~
#0d2f42), pale blue-grey panels on white, teal links; a geometric sans for everything,~13–14pxbase — dense, enterprise, no dark mode, mixed hit-target sizes.
9.2 Tolltariffen (toll.no) — anatomy
- Stack: React SPA (Material-UI-style components), path routing with real URLs,
?language=enas query param. - Layout system: a single centred column (~1280 px) of prose + one table. No sidebars, no dashboard grid. The heading page is one component: notes links → heading title → tree table → heading pager (84.20 … 84.26 + "Go directly to" input). White page, one accent colour (deep teal
rgb(18,64,79)), generous whitespace,16pxbase font. - The tree table is the whole product: columns are description | ordinary duty rate | units of quantity | 🌐 | commodity code | copy. Indent dashes in the description column encode depth;
OPEN ALL LEVELS / CLOSE ALL LEVELSappears both above and below the table; the selected row is outlined. Every code has a copy button (aria-label="Copy commodity code"); every declarable row has a globe (aria-label="Free trade agreements"). - The overlay is the master-stroke: the globe opens a full-screen dialog that is URL-routed (
…/84.23.2000/commodity-info/import/all) — shareable, back-button-safe, yet the tree stays underneath. Inside: the full ancestry chain drawn with└connectors, thenImport | Export | Generaltabs, then anALL RESULTS | CHOOSE COUNTRYsegmented switch (also routed:/import/search). - Progressive disclosure everywhere: VAT table (both 25% standard and 15% low rate) is open; Preferential duty rates — free trade agreements and Fees are collapsed accordions with a plain sentence as the trigger ("Click here to see the various free trade agreements"). CHOOSE COUNTRY empties the view down to a single input.
- Plain language instead of empty tables: "Duty free commodity code.", "Use the current ordinary duty rate unless there is a preferential duty rate stated below for the country of origin of the product.", "No restrictions registered for any country of origin." The system explains what to do, not just what the data is. Norway can afford this because the data model is simple (one MFN rate + FTA rates + VAT + restrictions — no additional codes, no Meursing).
- Rough edges: country search is "(in Norwegian only)" and requires two capital letters; HS explanatory notes only in Norwegian; body font stack falls back oddly (Times New Roman leaks into helper texts).
9.3 Synthesis — what TaricHive should take from each
The two sites sit at opposite ends of one axis: Taric Support = maximum data density for the professional who reads tariffs all day; Tolltariffen = maximum guidance for the citizen who reads one tariff a year. TaricHive's users (customs declarants) are the professional case, but several Tolltariffen patterns cost nothing and lower the load:
| Pattern | Take from | How in TaricHive |
|---|---|---|
| Duty-chip rows, two-column measure groups, evidence chips | Taric Support | Already close in TariffDetail.razor — keep |
| URL-routed quick-info overlay from the tree (globe → duties/VAT/restrictions without leaving the tree) | Tolltariffen | /tariff/{src}/{code}/{movement}?panel=info overlay route; tree stays mounted underneath |
| Plain-language verdict lines ("Duty free commodity code.", "No restrictions registered.") | Tolltariffen | Compute from the measure set; render instead of empty group sections rather than showing empty headers |
| Guidance sentence above rate tables ("use ordinary rate unless a preferential rate below applies") | Tolltariffen | Static one-liner per group, localised |
| Copy button on every code; open/close-all above and below long trees; heading pager (prev/next heading + go-to) | Tolltariffen | Small Blazor components, an afternoon of work |
| ALL RESULTS ↔ CHOOSE COUNTRY as two routed states, not a filter that silently mutates the list | Tolltariffen | Origin filter already exists — reflect it in the query string and give it an explicit all/one segmented control |
| Persistent (jurisdiction, movement, date) context bar | Taric Support | Worth adopting; TaricHive currently keeps date/movement per page |
| Server/shared-layer composed duty strings, one formatter | Taric Support | The DutyTerm formatter from the gap-analysis report §7 |
Real semantics (role="tab", aria-labels, links not H1s) |
Tolltariffen | Do better than both — Blazor makes this cheap |
Appendix — captured screens (scratchpad cdp/)
login-state.png (landing), good-2204.png (tree), good-declarable.png / good-cn-filtered.png (BE measures, unfiltered/CN), calc-form.png (calc inputs), addcode-modal.png (picker), calc-done.png (result €1,340.40), ch-tares2.png (CH statistical keys + Fr./100kg gross), gb-good.png (GBP/hl + stale banner), search-results.png (multi-source hub). Tolltariffen: no-8423-top.png / no-fta2.png (84.23 tree + commodity-info overlay), no-wine-fta.png (22.04.2101 overlay, dual VAT rows), no-choose-country.png (routed country view).
Reusable setup: launch Chrome with --remote-debugging-port=9222 --user-data-dir=%LOCALAPPDATA%\chrome-cdp-profile (login persists), drive with puppeteer-core via browserURL: http://127.0.0.1:9222. Note: Chrome 136+ refuses CDP on the default profile — the dedicated profile is required.