@k2b/ui
Colors and selection
Color style guidelines: when color carries meaning, the app color of a selected row, view, or filter, and what stays blue or neutral.
Two hosts set the selection hooks and the link accent from a teal and a yellow app color, as Cloud does. Selected rows, views, segments, and filters take that color with readable ink; buttons, toggles, focus, and status do not change. The theme follows this site.
Teal app
#14b8a6Selection takes the app color
Task | Status |
|---|---|
Review the project brief | Open |
Prepare the workshop | Done |
Controls stay platform blue
Status and content keep their own color
Link text stays body ink; its underline follows the app ink: read the project brief.
Yellow app
#eab308Selection takes the app color
Task | Status |
|---|---|
Review the project brief | Open |
Prepare the workshop | Done |
Controls stay platform blue
Status and content keep their own color
Link text stays body ink; its underline follows the app ink: read the project brief.
TSX
Copy/* CSS: the hooks for light and dark mode */.teal-app { --k2b-app-workspace-selected: #e8f8f6; --k2b-app-workspace-active: #0d7b6f; --k2b-row-selected: #effaf9;}.k2b-ui[data-theme="dark"] .teal-app,.k2b-ui.k2b-dark .teal-app,.dark .k2b-ui .teal-app { --k2b-app-workspace-selected: #123133; --k2b-app-workspace-active: #14b8a6; --k2b-row-selected: #112c2e;} /* Inside a .k2b-ui scope */<div class="teal-app"> <AppWorkspace.SidebarItem variant="object" icon="ti ti-briefcase" active>Customer work</AppWorkspace.SidebarItem> <ViewTabs label="Views" current="board" items={views} /> <SegmentedControl ariaLabel="Range" value={range} onValueChange={setRange} options={ranges} /> <FilterChip label="Tags" icon="ti ti-tag" showValue value={tags()} onValueChange={setTags} options={tagOptions} /> <DataTable rows={tasks} columns={columns} getRowId={(task) => task.id} selectedRowId="brief" /> <Button>New task</Button> <Checkbox label="Notify me" value={notify} onValueChange={setNotify} /> <Switch label="Sync" value={sync} onValueChange={setSync} /> <StatusBadge tone="ok" label="Healthy" /></div>These are the color style guidelines for @k2b/ui and for the apps that use
it. They say when color carries meaning, which states use the selection color
(in a Cloud app, the app color), what stays platform blue, and what stays
neutral.
The look stays clean and flat. Type, space, and order make the hierarchy. Color goes only where it means something.
Use color with meaning
- Color comes from content. Tags, status, places, and stock carry color. A Kanban card may take the color of its tag.
- Color is never the only signal. A name, an icon, a count, or an underline always goes with it.
- Surfaces stay neutral. Menus, popovers, dialogs, sheets, the chat composer, the model card, and cards without meaning get no tinted background.
- One selection rule. On a white surface a selection is tinted. On a colored surface it is white. Only the one main action of a view is fully filled, so two accent fills never sit side by side.
- No fake depth. No paper, stone, grooves, or decorative shadows. A surface is flat; depth comes from inset space and the shared surface roles.
Contrast
Text uses strong contrast: darker secondary text, stronger level steps, slightly larger secondary sizes, and tighter titles. The exact roles, sizes, and contrast floors are in Ink hierarchy.
Selection color
Selection marks where the person is or what they picked. Three optional host hooks carry it:
--k2b-app-workspace-selectedfills selected navigation and the selection controls below. It defaults tovar(--k2b-selected).--k2b-app-workspace-activeis the ink on that fill. It defaults tovar(--k2b-action).--k2b-row-selectedfills selected content rows. It defaults tovar(--k2b-selected).
Without a host value, selection keeps the platform selection color. A host sets the hooks on its own scope to give selection its color. Cloud maps them to the active app's color, so in a Cloud app the selected row, the selected navigation row, and the selected view carry the app color, not gray and not platform blue.
Selection changes the fill and the ink. Padding and border width stay the same, so nothing moves when a row or option is selected. Only a selected sidebar row also turns semibold; its label reserves that width in advance.
Navigation fill
| State | Primitive | Class |
|---|---|---|
| Selected sidebar row, object chip, or sidebar icon action | AppWorkspace.SidebarItem |
k2b-app-workspace |
| Selected navigation row | Navigation |
k2b-navigation |
| Selected settings category | SettingsModal |
k2b-settings |
Selected tabs, segments, and filters
A selected tab, view tab, or segment and an active filter chip are selection states, in the same family as the selected navigation row. They mark where the person is or what they picked, so they share the navigation fill and its ink. This is a decision of 19 October 2026; the shell designs approved on 17 October 2026 show exactly this.
| State | Primitive | Class |
|---|---|---|
| Selected tab | Tabs |
k2b-tabs |
| Current view | ViewTabs |
k2b-view-tabs |
| Selected segment | SegmentedControl |
k2b-segmented-control |
| Active filter | FilterChip, FilterToggle |
k2b-filter-chip |
Do not draw a chosen view, range, or filter as a filled primary button such
as variant={chosen ? "primary" : "secondary"}. That makes a choice look like
the main action.
Row fill
| State | Primitive | Class |
|---|---|---|
| Selected table row | DataTable |
k2b-data-table |
| Selected file tile or list row | FileGrid |
k2b-file-grid |
| Selected file tree row | FileTree |
k2b-content-file-tree |
Controls stay platform blue
Controls say what a person can do or what a field holds, not where they are.
They keep the platform colors --k2b-action, --k2b-action-solid, and
--k2b-selected in every app, and the selection hooks never change them:
buttons, inputs, checkboxes, radios, switches, and every focus ring
(--k2b-focus-ring), plus keyboard-active options, pressed toolbar buttons,
and checked cards.
| State | Primitive | Class |
|---|---|---|
| Primary button, the one main action | Button |
k2b-button |
| Checked checkbox | Checkbox |
k2b-check |
| Switch that is on | Switch |
k2b-switch |
| Checked card | CheckboxCard |
k2b-checkbox-card |
| Keyboard-active or selected option | Select, Combobox, MultiSelectInput |
k2b-choice-option |
| Checked choice chip in a field or picker filter | ChoiceChips, Select |
k2b-choice-groups |
| Keyboard-active suggestion | AutocompleteEditor |
k2b-autocomplete |
| Current page | Pagination |
k2b-pagination |
| Selected date, month, or duration | DatePicker |
k2b-date-grid, k2b-date-months, k2b-date-durations |
| Selected range | RangePicker |
k2b-range-picker |
| Selected tone or emoji group | EmojiPicker |
k2b-emoji-picker |
| Pressed toolbar button | Toolbar |
k2b-toolbar |
| Pressed formatting tool | MarkdownEditor |
k2b-markdown-editor |
Drop targets
Drop targets and other control states keep the platform fill.
| State | Primitive | Class |
|---|---|---|
| File over a dropzone | FileDropzone |
k2b-dropzone |
| Item over a drop target | DropTarget |
k2b-drop-target |
| Files over a page or region | FileDropTarget |
k2b-file-drop |
| Item over a folder | FileTree |
k2b-content-file-tree |
| Event over a day while it moves | Calendar |
k2b-calendar-month |
Links
Link text in content stays body ink. Its underline, and the arrow of a web
link, use --k2b-link-accent, which defaults to var(--k2b-action). It is a
separate host hook; the selection hooks do not change it. Cloud maps it to the
app ink, as described in Markdown.
Status stays green, yellow, and red
Status uses the shared intent tones info, success, warning, and
danger. Status color never marks an app, and an app color never marks a
status. A red app must not make normal navigation look destructive. See
Badges.
Icon tiles
App, file-type, and model icons sit in a soft chip of their color:
k2b-icon-tile takes its fill from the glyph color. Status icons and avatars
do not use it. Folder icons and the icons of object rows, overview headers, and
panel dialogs take the selection ink --k2b-app-workspace-active, so in a
Cloud app they carry the app color. Sizes, fill shares, and states are in
Icon tile.
App color in Cloud
A Cloud app declares one six-digit accent. Cloud derives the app ink and
both selection fills from it, for light and dark mode. The app ink keeps at
least 4.5:1 contrast (WCAG AA) on its own fills and on the shared surfaces. A
light accent, such as yellow #eab308, is darkened only as far as needed. Any
six-digit accent works. See
Define an application and
Styling and accessibility.
Import
@import "@k2b/ui/global.css";The hooks are CSS custom properties on a .k2b-ui scope. Nothing else needs
importing.
Accessibility
- Selected ink on the selection fill keeps at least 4.5:1 contrast, and so do descriptions and counts on a selected navigation row.
- Color is never the only signal. A filter chip names its value, a status badge has a label, and a link has an underline.
- Selection keeps its semantics:
aria-current,aria-selected,aria-checked, oraria-pressedsay what is selected, not the fill. - Keyboard focus keeps the platform focus ring on every control, also on a selected one.
- In forced-color mode, where fills disappear, the selected tab, view tab, and segment are underlined.
Runtime
The colors are plain CSS. They work in the server response, need no browser
code, and follow the light and dark theme of the .k2b-ui scope.
Example
Set the hooks for light and dark mode on an element inside the .k2b-ui
scope, so selected text stays readable in both themes:
.teal-app {
--k2b-app-workspace-selected: #e8f8f6;
--k2b-app-workspace-active: #0d7b6f;
--k2b-row-selected: #effaf9;
}
.k2b-ui[data-theme="dark"] .teal-app,
.k2b-ui.k2b-dark .teal-app,
.dark .k2b-ui .teal-app {
--k2b-app-workspace-selected: #123133;
--k2b-app-workspace-active: #14b8a6;
--k2b-row-selected: #112c2e;
}import { Button, FilterToggle, ViewTabs } from "@k2b/ui";
<div class="teal-app">
<ViewTabs
label="Views"
current="board"
items={[
{ id: "board", label: "Board", href: "?view=board" },
{ id: "list", label: "List", href: "?view=list" },
]}
/>
<FilterToggle label="Only mine" icon="ti ti-user" active href="?mine=0" />
<Button>Create task</Button>
</div>