Cloud|

Frontend conventions

Look up visual UI, interaction, and accessibility rules with their canonical owners and checks.

18 min read Updated 2026-10-15 #conventions#ui#interaction#accessibility

Use these rows with the Conventions for Cloud applications index and audit procedure. Follow each canonical link for the complete contract.

Run the click paths in the running application at desktop width and at phone width below 768 px, in light mode. Use WebKit for the phone width. Run the search patterns with rg -n in the application's src directory.

Visual UI (UI)

ID Rule Why How to check Canonical
UI-01 Use the public @k2b/ui component that owns a control, feedback, surface, or layout before writing local markup or CSS. Shared components carry accessibility, responsive behavior, theming, and states. Manual: search for recreated Cloud classes with "btn-primary|sidebar-item|focus-ui" and read each local control, menu, or dialog for a shared component that owns it. Shared components first
UI-02 Keep screens clean and flat: group with spacing, alignment, typography, and shared surfaces, put the most important value first, and place no box inside a box. The content carries the hierarchy, so a person finds the value without reading the frame. Manual: open each changed screen at both widths and check that the main value comes first and no framed group sits inside another; then check contrast and keyboard use again. Hierarchy before decoration
UI-03 Group ordinary content with whitespace, alignment, a shared surface, or a section label, never with lines between rows, cards, settings, sections, menu items, form sections, or pagination. A line of any weight competes with the content and splits one group into boxes. Manual: search app markup for "border-[tb]\b|divide-|<hr" and app CSS for pseudo-element rules and inset-shadow hairlines; look at the screen in light and dark mode. No decorative lines
UI-04 Draw no line between the header, content, and footer of an app-owned surface or between workspace regions, including the edge to the app rail. AppWorkspace, Panes, and SettingsModal own region edges, so one workbench stays one surface. Manual: search app markup for "border-[tblrxy]\b|border-[tblr]-|divide-|<hr" around dialogs, panels, cards, and workspace regions; inspect each header and footer edge at both widths. No decorative lines
UI-05 Take the depth of an in-flow surface from the shared surface, never from an outer shadow, a nested border, or a box inside a box; floating layers such as dialogs, popovers, menus, and toasts keep their soft outer shadow. In-flow content stays flat, and floating layers read as separate from the page. Manual: search app markup for "shadow-|drop-shadow|box-shadow" on in-flow elements and for a border or Paper inside another Paper; confirm that floating layers keep the shadow of their shared component. Surfaces
UI-06 Keep one visible dialog frame: with PanelDialog, the header and footer stay fixed and PanelDialog.Body scrolls. A second frame inside a dialog is a box inside a box. Manual: open each dialog with long content at both widths, scroll it, and check that only the body scrolls and no inner frame appears. Surfaces
UI-07 Let no state change move or resize the surrounding content: reserve space for validation messages, loading results, save bars, and badges, and draw focus without changing a border width or padding. Content that jumps loses the reader's place and turns a click into a click on the wrong target. Browser test: compare the neighbors' getBoundingClientRect() before and after each state change at desktop and phone width, as packages/ui/src/chat/chat-composer.browser.test.ts does. Manual: toggle focus, hover, loading, async results, validation, and save bars at both widths and compare the bounding boxes in the developer tools. Keep the layout still
UI-08 Let hover and focus change only paint: no translate, scale, rotate, or growing outer shadow. Movement under the pointer makes a target harder to hit. bun scripts/check.ts hover-motion; Manual: hover and focus each element and compare its bounding box with the resting state; search CSS for transform, translate, scale, and rotate in :hover and :focus rules and for "hover:shadow|hover:translate|hover:scale". Hover without movement
UI-09 Take every duration and curve from the shared motion tokens, animate only transform, opacity, and color, and animate nothing on first paint or mount. A result never waits for an animation, and animation never moves the layout. bun scripts/check.ts motion covers @k2b/ui CSS only; Manual: search app CSS for "transition|animation|@keyframes" and check tokens and animated properties; reload the page and watch for animation on first paint. Motion tokens
UI-10 Give a height-constrained region with dynamic content ScrollArea or the scrolling part of its owning component, with one scroll owner per full-height region. The stable scrollbar gutter keeps text and controls from moving sideways when a scrollbar appears. Manual: change filters, expand sections, or load more until the scrollbar appears and compare horizontal positions; read each ScrollArea for a wrapped DetailPanel.Body, PanelDialog.Body, DataTable, ChatTimeline, VirtualFeed, or Timeline. Stable scroll regions
UI-11 Use VirtualFeed for a long feed that loads at either end, MessageRow for conversation messages, and Timeline for dated items across days. These components keep the visible item in place while pages load and rows change size. Manual: read hand-made lists that load more items; scroll a long feed at both widths while pages load and check that the visible item stays in place. Stable scroll regions
UI-12 Keep app identity, action, status, and data colors independent. One color for unrelated meanings makes navigation look destructive or status look like an app. Manual: in light and dark mode, check that the app accent marks only identity, primary buttons use the action color, and status colors do not identify the app. Color roles
UI-13 Style app-owned CSS with the host's semantic theme variables, without fixed light backgrounds, black text, hardcoded app colors, arbitrary borders, or overrides of shared components. Fixed colors break dark mode, focus treatment, and application theming. Manual: search app styles and markup for "#[0-9a-fA-F]{3,8}\b|rgb\(|bg-white|text-black" and for selectors that target shared component classes. Semantic styling
UI-14 Follow the person's theme through the <html> class, read the theme preference on the server when markup depends on it, and never read prefers-color-scheme in application CSS. Light, Dark, and Automatic stay consistent with the person's choice. Manual: search for prefers-color-scheme; switch the appearance between Automatic, Light, and Dark with the page open and check that it follows without a reload. Theme
UI-15 Review every changed surface in light and dark mode in its resting, hover, focus, active, selected, disabled, loading, empty, and error states. Both themes keep the same hierarchy and readable states. Manual: walk each listed state of the changed surface at both widths in light mode, then in dark mode. Both themes
UI-16 Design and verify every screen at phone width with touch, in WebKit and Chromium, with no content that depends on hover and with dialogs whose primary actions stay reachable. A phone is a full Cloud client, and Safari and most iOS browsers run WebKit. bun run test --browser --filter packages/<app> with TEST_BROWSER=chromium and TEST_BROWSER=webkit; Manual: complete the primary flow at phone width with touch in WebKit and check for page-level overflow. Phones
UI-17 Size full-height surfaces with the dynamic viewport (h-dvh, min-h-dvh), not vh. vh follows the larger viewport, so a page that fits scrolls by the toolbar height. bun scripts/check.ts viewport-height; Manual: search for "h-screen|100vh"; open the surface in WebKit at phone width and check that a page whose content fits does not scroll. Application layout
UI-18 Give every NoticeCard its one appearance: no icon, own text size, faded text, or palette color in its content, placed directly in the host's content flow. A tint alone separates the notice, and every tone looks the same in every app. bun scripts/check.ts notice-cards; Manual: read each <NoticeCard for icons, text-size or color classes, a wrapping Paper, or a nested notice. NoticeCard composition
UI-19 Give each page its title array and add no back button of your own where that array already leads up. The shell shows the way up by one rule, the same in every app and at every width. Manual: search for "ti-arrow-left|ti-chevron-left" and read each control that leads to a parent; open a page two levels deep at both widths and check that only the shell's way up appears. Way up
UI-20 Give the single card that is a standalone page the standalone-card class, with the page padding on main. The card shows flat on phones from the server render on, so nothing moves afterwards. Manual: search standalone pages for standalone-card; open the page at phone width in WebKit and check that the card has no border, radius, shadow, surface, or padding of its own. One card on phones
UI-21 In a standalone (third-party) application, give each color-mix() that references a CSS variable or currentColor its own rule in src/styles/app.css. The @k2b/cloud build keeps only one such value per rule and shows a fallback color for the others. Manual: search src/styles/app.css for rules with more than one variable color-mix() and for @apply with several opacity modifiers on variable colors. Variable color mixes

Interaction and feedback (IX)

ID Rule Why How to check Canonical
IX-01 Make every action answer at once, before its request returns, with an optimistic update or a visible pending state on the acted element. A person who sees nothing happen clicks again or leaves. Manual: throttle the network in the developer tools and run each action; the acted element changes or shows a pending state before the response arrives. Mutation state
IX-02 Offer Undo only where the server can really reverse the action, never for a request that is merely held in the browser. A row that only hides while a permanent delete waits in the browser loses data when Undo is missed or the page closes. Manual: search for Undo and trace each one to a server reversal or a cancel of a command the server still holds; remove an item, close the tab before the notice ends, reload, and check that the item still exists. Mutation state
IX-03 Ask with prompts.confirm() before a hard delete or another irreversible destructive action, then show a pending state on the row until the server answers, with no Undo. Destroyed data cannot come back, so the person decides before it starts. Manual: throttle the network and hard-delete an item: a confirmation appears, the row stays pending, and no Undo is offered; block the request and check that the row returns to normal with the reason. Mutation state
IX-04 Keep a pending state on the acted element until the server answers for an action that grants a permission or exposes new data, even when it can be reversed. The view never shows access or data the server has not granted. Manual: throttle the network and share an item or toggle a permission; the element stays pending and shows no granted state before the response. Mutation state
IX-05 When the server refuses an optimistic change, return the item in its place with the reason; offer Retry only when repeating the request is safe, and ask the person to check the item when the outcome is unclear. Repeating an unsafe request could apply the change twice. Manual: block the request in the developer tools and check that the item returns with a reason; read each Retry handler for a reused idempotency key or a request that only sets a value. Mutation state
IX-06 Never drop an action that starts while an earlier one is pending, and run actions on the same item in the order the person chose them. An earlier failure never rolls back a later choice. Manual: throttle the network and run two actions on the same item in quick succession; both apply in order, and only conflicting controls are disabled. Mutation state
IX-07 Wait for the server's confirmation before a message claims an irreversible outcome, grants access, or exposes data. "Sent" or "Shared with Alice" must be true when the person reads it. Manual: throttle the network, send or share an item, and check that the confirmation appears only after the response. Mutation state
IX-08 When a write succeeds but the refreshing read fails, keep the result on screen, say that the change was saved, and offer to retry the refresh. A retry must not repeat a completed non-idempotent command. Manual: block only the refresh request after a save and check the message and the retry action. Mutation state
IX-09 Give each outcome one channel: no message when the result shows where the person acted, a toast only for an effect that is not on screen or a single failed action, inline errors for blocking input, and a dialog only for a decision. A toast for every visible change teaches people to ignore the rail, including its errors. Manual: search for toast.success and check that each effect is off screen; run each action and compare its feedback with the outcome table. Toast outcomes
IX-10 Choose feedback by scope: field validation props, InlineGuidance, Placeholder, NoticeCard, StatusBadge, or a toast, and keep content and the retry action visible when a refresh fails. A person can tell an empty result from a failed request and recover where it failed. Manual: compare each feedback in the changed screen with the scope table; block a refresh and check that existing content stays. Feedback by scope
IX-11 Pass the mutation's abort signal into network requests and route a dialog close through the same cancellation. Closing a dialog stops work that is still running. Manual: search mutation code for requests without signal; close a dialog during a throttled request and check that the request is cancelled in the network panel. Cancellation
IX-12 Keep request schema and domain validation on the server and map server field errors back to their inputs. Client validation improves feedback but does not protect the service. Manual: send an invalid value with curl past the form and check the rejection; submit a value only the server rejects and check that its error appears at the input. Server validation
IX-13 Put reloadable view state in the URL: filters, sorting, pagination, selected resources, active tabs, and date ranges. The view survives reload, sharing, and Back or Forward. Manual: change each filter and selection, then reload, paste the URL into a new tab, and go Back and Forward; the server renders the same view. URL state
IX-14 Start navigation with anchors and keep tables, pagination, range controls, and filter chips working without JavaScript. A link works before islands mount and opens in a new tab. Manual: disable JavaScript and use navigation, pagination, and filters; open a link in a new tab. Links first
IX-15 Change history only after the island has applied the new state, and listen to popstate whenever an island changes history. The URL and the visible state never diverge. Manual: search for push(, replaceWith(, and listenPopState; click through enhanced navigation quickly, use Back and Forward, and fail a target load with request blocking. Links first
IX-16 Keep hover, focus, open menus, unsaved field input, and temporary panel animation out of the URL. A reload or shared link shows the view, not a transient state. Manual: open menus and type into fields, then check that the URL stays unchanged. Transient state
IX-17 Render the initial answer on the server and start the island from its serialized state, passing the snapshot and its source when the island reloads it. The first frame matches the server render, and no second request loads the same data. Manual: load the page with the network panel open and check for a duplicate request after hydration; compare element positions before and after hydration and after a reload. Preserve the server result
IX-18 Never import an island or client module from another island or client module; share a plain .tsx module instead. Each island keeps its own mount, prop serialization, and error boundary. bun run --cwd packages/<app> build fails on a nested island; Manual: search *.island.tsx and *.client.tsx files for imports matching "\.(island|client)(\.tsx)?[\"']" and for import() with a computed path. No nested islands
IX-19 Leave island failures to the boundary Cloud gives each island, without an ErrorBoundary around an island's root, and handle errors in event handlers and async callbacks where they happen. A failing island shows a notice with Try again while the rest of the page keeps working. Manual: search for ErrorBoundary at island roots; read event handlers and async callbacks for a handled error, such as a mutation's error() or a toast. When an island fails
IX-20 Hide progressive row actions with k2b-progressive-row and k2b-progressive-action, or under @media (any-hover: hover) in custom CSS, never with (hover: hover), (hover: none), (pointer: fine), or Tailwind's hover: and group-hover: variants. A tablet's primary pointer stays the finger when a mouse is attached. packages/ui/src/styles/css-hygiene-contract.test.ts covers @k2b/ui; Manual: search for "hover: hover|hover: none|pointer: fine|group-hover:opacity"; check that row actions always show on a touch-only phone and appear on hover and keyboard focus with a mouse. Controls and feedback

Accessibility (A11Y)

ID Rule Why How to check Canonical
A11Y-01 Render icon-only actions with IconButton or IconButtonLink and a label, and give any other icon-only control an accessible name. The label becomes the control's accessible name and tooltip. bun scripts/check.ts icon-controls; Manual: read each <button>, <a>, Button, and ButtonLink whose only child is an icon; inspect accessible names in the developer tools' accessibility tree. Button accessibility
A11Y-02 Keep focus rings whole: add k2b-focus-inset to your own element that clips focusable content, and never add padding, margins, or borders so that a ring fits. A clipped ring hides where the keyboard is, and added space moves the content. Manual: search for "overflow-(hidden|auto|x-auto|y-auto)" on elements that contain controls and check for k2b-focus-inset; Tab through every control at both widths and check that each ring is whole. Clipping containers
A11Y-03 Draw a custom focus ring with the focus tokens, as an outline or an inset box-shadow sized by --k2b-focus-width, and never change border-width, padding, margin, or size on focus. The ring follows the shared placement inside and outside clipping containers. packages/ui/src/styles/focus-contract.test.ts covers @k2b/ui; Manual: read each :focus-visible rule for --k2b-focus-* tokens and for layout properties. Custom focus rings
A11Y-04 Use the host's var(--ui-focus) only as a box-shadow value. The browser drops a declaration that uses it as a color, so no ring appears. bun scripts/check.ts css; Manual: search for "var\(--ui-focus\)" and check that each use is a box-shadow value. Semantic styling
A11Y-05 Use native buttons for actions and anchors for navigation. Keyboard, screen reader, and new-tab behavior come with the element. Manual: search for "<(div|span|li)[^>]*onClick"; Tab through the screen and check that each action and link is reachable and announced by its role. Keyboard and screen readers
A11Y-06 Associate labels, descriptions, and errors with their inputs. A screen reader reads the field's purpose and its problem together. Manual: inspect each input in the accessibility tree for its name, description, and error, including an error from the server. Keyboard and screen readers
A11Y-07 Do not use color as the only status signal; name the state in words. People who cannot tell the colors apart still read the state. Manual: emulate a color vision deficiency or forced colors in the developer tools and check that each status, notice, and data value stays clear. Keyboard and screen readers
A11Y-08 Keep heading order and landmarks meaningful. Screen reader users navigate a page by its headings and landmarks. Manual: list the page's headings and landmarks in the accessibility tree and check that they follow the visible structure. Keyboard and screen readers
A11Y-09 Return focus to the control that opened an overlay when it closes, showing the ring only after keyboard use. A keyboard user continues where they were. Manual: open each dialog and menu once with the keyboard and once with a click, close it with Escape and with a chosen item, and check where focus lands and whether the ring shows. Focus after an overlay
A11Y-10 Call announce() for an outcome the screen shows but the focused control does not say, and announce each outcome only once. Screen reader users hear results of shortcuts or replaced controls without hearing them twice. Manual: search for announce( next to toasts for the same outcome; run shortcut actions with a screen reader or watch the live regions in the developer tools. Announce without a toast
A11Y-11 Add no second live region around InlineGuidance loading, and use role="alert" only for a newly appearing action error that needs immediate attention. Messages are announced once, and historical errors do not interrupt. Manual: search for "aria-live|role=\"alert\"|role=\"status\"" and read each one against the messages it wraps. Keyboard and screen readers
A11Y-12 Check prefersReducedMotion() for motion your code drives, and keep the wait of a delay that only debounces a loading indicator. Reduced motion makes movement instant without making a fast request flash a loader. packages/ui/src/styles/motion-contract.test.ts covers @k2b/ui; Manual: search for "scrollIntoView|\.animate\(|behavior: \"smooth\"" and check each for prefersReducedMotion(); set data-k2b-motion="reduced" on <html> and repeat the flow. Reduced motion
A11Y-13 Give every drag interaction an equivalent keyboard path, and do not add aria-grabbed in its place. People who cannot drag can still move items. Manual: search for "draggable|onDragStart|aria-grabbed"; complete each move with the keyboard only. Keyboard and screen readers
A11Y-14 Keep at least 0.625rem (0.75rem next to xs buttons) between lines of compact buttons that wrap or stack, and do not set position: static on a button. Each button keeps a 44 px tap area that does not reach a neighbor. packages/ui/src/styles/touch-target-contract.test.ts covers @k2b/ui; Manual: at phone width with touch, tap each compact button in wrapped rows and check that the intended one responds. Touch targets
A11Y-15 Run a visual and accessibility pass: narrow and wide layouts in both themes, element positions across state changes, and the primary flow with the keyboard. Automated checks cover only part of focus order, names, announcements, and contrast. Manual: complete the primary flow with the keyboard at both widths and check focus order, accessible names, dialog focus return, status announcements, and contrast. Visual and accessibility pass

Connect a coding agent

The Cloud skill provides compact working instructions. MCP supplies exact current documentation when details matter.

CLI command
bunx skills add https://docs.example.com

This command installs the working instructions published by this website in the selected coding agent.

For exact, current details, also connect the MCP server through one of the agent tabs.

Installation uses the open-source Vercel Skills CLI.

The skill and MCP complement each other: the skill describes workflows, while MCP supplies current documentation.