Kimi Web · Design System

● Design System · v1.0

# Kimi Web Design System

This document defines the visual language and component specification for Kimi Web — design tokens, component primitives, the chat interface, theming, and style rules. All UI work is grounded in it: unified, restrained, token-driven, and themeable.

Scope apps/kimi-web Component primitives Theme 1 set · 4 customizable colors Light / dark mode
i
This spec is the single reference when changing the web UI. Before adding or modifying a component, style, layout, or theme, read this document first; color, font, radius, spacing, shadow, z-index, and motion always use the §02 tokens, components reuse the §03 primitives, and the §06 style rules are followed.
01

## Design Principles

Every UI decision traces back to the following principles. Kimi Web is a local Agent tool for developers: quick scanning, long stretches of staring, often in the dark — the design serves the task, and is restrained, clinical, and density-first.

- Consistency —— The same semantics use the same component. The primary button, dialog, input, and badge should each have exactly "one" correct way to be written across the entire site.
- Hierarchy —— Build a clear hierarchy through size, weight, color, and whitespace; emphasize through "restraint" rather than "bolder and bigger".
- Proximity —— Group related elements, leave whitespace between unrelated ones. A card's padding, line spacing, and group spacing all come from the same spacing scale.
- Feedback —— hover / active / focus / loading / success / error all have visible states, and the state language is unified.
- Breathing room —— Control density with the spacing scale rather than arbitrary pixels; prefer restrained whitespace over cramming controls together.
- Accessibility (A11y) —— Text contrast ≥ 4.5:1, visible focus rings, touch targets ≥ 32px, and states that don't rely on color alone.
- Reduction —— The number of colors, radii, shadow levels, and type sizes all converge to a finite set of tokens; delete stray values.

✓
Brand tone (the do-not list): calm, clinical, never exaggerated. Reject purple gradients, glassmorphism, glowing shadows, AI purple / blue glows, endlessly looping fussy micro-animations, "Boost your productivity"-style marketing copy, and using emoji as icons — the moon phases 🌑…🌘 are the sole exception, used only in the "waiting for the Agent to respond" chat state, as a brand signature. These are all common tells of AI-generated interfaces (an "AI tell"), deliberately avoided.
i
Declare design intent first (Design Read): before adding a component / page, write one sentence describing its scenario, audience, and tone (for example, "a lightweight tool card embedded in a conversation, for developers, calm and restrained"), then build. If the intent isn't clear, ask one question first rather than defaulting to the nearest existing style.
02

## Design Tokens

Collapse every visual decision into tokens. Color tokens keep the existing short names and fill out the semantics (lowering migration cost), while spacing, z-index, motion, and font-weight fill in the scales that are currently missing. Every token has: name, light value, dark value, and usage.

i
Naming convention: `--<category>-<role>-<state>`. For example `--color-text-muted`, `--radius-md`, `--space-4`. To reduce churn, the existing short names (`--bg` / `--ink` / `--line` / `--blue` …) are kept as compatibility aliases for one release cycle.

### Color

Semantic-first, in three layers: background / text / border + accent + status colors. All colors are defined in light / dark pairs, with contrast ≥ 4.5:1.

i
The table below shows the derived semantic tokens. The neutrals and the accent are derived from the 4 color seeds in §05 — for example `--color-accent` comes from `--accent-primary`, and `--color-bg` comes from the current light / dark surface. The semantic status colors (success / warning / danger / info) are independent palettes paired with the seeds, one set each for light / dark; they are not auto-derived from the seeds. Day-to-day reskinning usually only needs the 4 seeds, with the status colors fine-tuned as needed.
bg
#ffffff / #0d1117
surface
#fafbfc / #161b22
surface-sunken
#f3f5f8 / #0d1117
fg
#14171c / #e8eaed
fg-muted
#6b7280 / #9aa0a8
line
#e7eaee / #2d333b
accent (KMBlue)
#1783ff / #58a6ff
accent-soft
#e8f3ff / rgba(88,166,255,.14)

Token | Light | Dark | Usage
--- | --- | --- | ---
--color-bg | #ffffff | #0d1117 | Page background
--color-surface | #fafbfc | #161b22 | Panel / sidebar / card head
--color-surface-raised | #ffffff | #1c2128 | Raised card / dialog / input
--color-text | #14171c | #e8eaed | Body text / headings
--color-text-muted | #6b7280 | #9aa0a8 | Secondary text / placeholder
--color-line | #e7eaee | #2d333b | Divider / card border
--color-accent | #1783ff | #58a6ff | Primary action / link / focus
--color-success | #0e7a38 | #3fb950 | Success / pass
--color-warning | #a9610a | #d29922 | Warning / pending
--color-danger | #c0392b | #f85149 | Danger / error / abort

#### Surface usage

The four surface layers each have a role — choose by "raised layer / default flat layer / sunken layer / page background", and avoid treating `--p-surface-raised` as a universal background.

Token | Light | Dark | Usage
--- | --- | --- | ---
--p-surface-raised | #ffffff | #1c2128 | Raised card / dialog / input (raised layer)
--p-surface | #fafbfc | #161b22 | Panel / sidebar / card head (default flat layer)
--p-surface-sunken | #f3f5f8 | #0d1117 | Code block / inline input / recessed area (sunken layer)
--p-bg | #ffffff | #0d1117 | Page background

#### Focus ring

All focusable controls (button, input, link, menu item, switch, checkbox) use the focus-ring token uniformly; do not hand-write a `box-shadow` focus ring.

Token | Value | Usage
--- | --- | ---
--p-focus-ring | 0 0 0 3px var(--p-accent-soft) | Default focus ring (link, menu item, switch, checkbox)
--p-focus-ring-strong | 0 0 0 3px var(--p-accent-soft), 0 0 0 1px var(--p-accent) | Strong focus ring (button, primary action)

#### Text selection

The text-selection color uses `--p-selection` uniformly (light `rgba(23,131,255,.18)` / dark `rgba(88,166,255,.32)`), applied by the global `::selection` rule; do not set a separate highlight background.

#### Disabled state

All disabled controls use `opacity:.5` + `cursor:not-allowed` uniformly; do not separately grey out or recolor.

### Font families

Kimi Web uses two font families: --font-ui (UI and body, system fonts first) and --font-mono (code and monospace). Components always reference the variables; do not hard-code font names.

#### --font-ui · UI & body (system fonts first)

Body and UI use each platform's native UI font — close to the system feel, comfortable for long text and CJK. Fallback chain:

--font-ui

    --font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI",
      "PingFang SC", "Microsoft YaHei", "Noto Sans SC",
      "Helvetica Neue", Arial, sans-serif,
      "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji";

- System UI fonts first: SF Pro on macOS / iOS, Segoe UI on Windows.
- CJK next: PingFang SC (macOS) / Microsoft YaHei (Windows) / Noto Sans SC (Linux).
- Helvetica Neue / Arial / sans-serif as generic fallbacks; emoji fonts at the end.

#### --font-mono · Code & monospace

Code, tool names, line numbers, diffs, etc. use JetBrains Mono (a self-hosted variable font), falling back to the system monospace:

--font-mono

    --font-mono: "JetBrains Mono Variable", "JetBrains Mono",
      ui-monospace, "SF Mono", Menlo, Consolas, monospace;

#### Loading strategy

Font | Source | Bundled | Usage
--- | --- | --- | ---
JetBrains Mono | @fontsource-variable/jetbrains-mono | ✓ self-hosted | monospace / code (--font-mono)
Inter | @fontsource-variable/inter | ✓ self-hosted | optional: page titles / brand wordmark (--font-display)
System UI / CJK fonts | operating system | — | body / UI (--font-ui), not bundled

✓
Self-hosted Inter / JetBrains Mono: no external network requests, no FOUT, works offline; system fonts are not bundled, consistent with the local-first approach.

#### Usage rules

- Components always use `var(--font-ui)` / `var(--font-mono)`; do not hard-code font names like `'Inter'` / `'JetBrains Mono'`.
- Body / UI use `--font-ui` (system fonts first); code / monospace use `--font-mono` (JetBrains Mono).
- Inter is used only for headings / brand scenarios that need a unified look (optional `--font-display`); it is no longer the body default.

### Type scale & weight

The type scale keeps and formalizes the existing `--ui-font-size` (UI, default 14px) / `--content-font-size` (body, 16px) dual tracks: UI controls / buttons / forms use `--text-base` (14px); reading body — including chat Markdown, message bubbles, etc. uses `--text-lg` (16px, relaxed line-height) for readability; the sidebar session list uses `calc(var(--ui-font-size) - 1px)` (15px, one step below the sidebar's 16px base) for density. Drop stray `font-weight: 650 / 750`; converge on two weights, 400 / 500 (regular / emphasis).

Page Title
--text-2xl · 22 / 500
Section Title
--text-xl · 18 / 500
Chat body / card title
--text-lg · 16 / 400
UI control / button / form
--text-base · 14 / 500
Helper text / table
--text-sm · 13 / 400
Badge / timestamp / line number
--text-xs · 12 / 500

Token | Value | Usage
--- | --- | ---
--font-ui | -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC"… | UI & body (system fonts first)
--font-mono | JetBrains Mono… | code, tool names, line numbers, diffs
--leading-tight/normal/relaxed | 1.25 / 1.5 / 1.7 | headings / UI / long text
--weight-regular/medium | 400 / 500 | body / emphasis

#### Icon size

Icons use three size tokens uniformly. The global `.p-ic` default is 16px (`--p-ic-md`); components pick as needed, and random pixel sizes are forbidden.

Token | Value | Usage
--- | --- | ---
--p-ic-sm | 14px | small button, badge, menu item, inline link icon
--p-ic-md | 16px | default (button, icon button, toolbar)
--p-ic-lg | 20px | Toast status icon, empty-state illustration

#### Icon

Icons always come from the centralized registry `lib/icons.ts`: in templates use the `<Icon name size />` component (`components/ui/Icon.vue`); for `v-html` contexts (such as a tool glyph) use `iconSvg(name, size)`. Do not hand-write `<svg>` — the `scripts/check-style.mjs` `icon-from-registry` rule flags stray SVGs. Icons come from Remix Icon (Apache-2.0), uniformly in a fill style (`fill="currentColor"`, 24×24 source grid), with color following the text; size uses the three tokens below. The registry is generated on demand by `scripts/gen-icon-data.mjs` from `@iconify-json/ri`, offline and tree-shaken. The whole site uses only this one icon family; do not mix in other icon libraries, and never hand-write SVG paths. When an icon is missing, add it to the registry (edit the mapping in `gen-icon-data.mjs` and regenerate) rather than drawing it in a component.

#### Size scale

sm · 14
md · 16
lg · 20

#### Icon library

Currently registered icons, grouped by purpose. The icon data is determined by the `NAME_TO_REMIX` mapping in `scripts/gen-icon-data.mjs`; running `pnpm gen:icons` generates `lib/icon-data.ts`. This catalog is regenerated with `node --experimental-strip-types scripts/gen-icon-catalog.mjs` so the registry and the document never drift.

Actions
plus
chat-new
close
check
search
copy
link
external-link
download
undo
send
image
settings
sliders
log-in
Navigation & layout
chevron-down
chevron-right
arrow-up
arrow-down
arrow-right
minus
panel-collapse
panel-expand
expand
collapse
list
Files & tools
folder
folder-closed
folder-plus
folder-solid
file
file-text
file-plus
file-off
image-off
code
terminal
pencil
glob
globe
check-list
bolt
git-pull-request
Communication
message
mail
user
Status & media
info
help-circle
alert-triangle
clock
sparkles
play
stop
star
star-outline
dots-horizontal
Other
sort
grip

Do not use emoji as functional icons (the sole exception is the moon phases 🌑…🌘, used only in the "waiting for the Agent to respond" chat state). The Kimi brand mark (the 32×22 eye logo) is a brand asset and is not part of this icon system.

A few special graphics are not in the registry; each has a dedicated component maintained in one place, and must not be copied by hand: `<ContextRing :pct />` (the Composer context progress ring, data-driven), `<AuthStateIcon kind />` (the success / expired / error colored illustrations in the login flow), `<Spinner />` (loading state). Status dots (such as in the Provider list) always use CSS dots (`border-radius:50%`), not SVG. The `scripts/check-style.mjs` `icon-from-registry` rule exempts the above and the brand mark; all other hand-written `<svg>` is flagged.

### Spacing

A 4px base grid. All spacing, gaps, and padding inside and outside components come from this scale — no arbitrary pixels.

--space-1 · 4
icon gap, badge padding
--space-2 · 8
control gap, small padding
--space-3 · 12
button padding, form-item gap
--space-4 · 16
card padding, grid gap
--space-5 · 20
dialog padding
--space-6 · 24
section gap
--space-8 · 32
large section gap

#### Dense list (sidebar / file tree)

High-density navigation lists like the sidebar share one rhythm, all on the 4px grid: in-row vertical padding `--space-1` (4px), no margin between rows (the hover pill provides the separation); section gap (between logo / search / action buttons / group title / list) uniformly `--space-2` (8px); between groups `--space-2`; the brand header is slightly looser at the top (`--space-3`). When building similar lists, reuse this scale — do not hand-write 1/6/7/10px.

### Radius

Merge the existing 14 values into the nearest of 7 scale steps. Rule: the component type determines the radius, not the author's feel.

xs · 4
sm · 6
md · 8
lg · 12
xl · 16
2xl · 20
full · 999

Token | Value | Usage | Merged from
--- | --- | --- | ---
--radius-xs | 4px | small badge, inline tag | 2/3/4px →
--radius-sm | 6px | small button, icon button, menu item | 5/6px →
--radius-md | 8px | button, input, badge, card | 7/8/9px →
--radius-lg | 12px | dropdown panel | 10/12px →
--radius-xl | 16px | dialog, bottom Sheet, Composer | 14/16px →
--radius-2xl | 20px | accent container / large panel | 20px
--radius-full | 999px | pill badge, avatar, send button | 999px / 50%

### Elevation & z-index

Shadows express only "elevation", never decoration (no colored glow). z-index is unified into a scale, eradicating `9999`-style one-upping.

sm · dropdown menu / sticky
md · Toast
lg · overlay (reserved)
xl · dialog

Z-index Token | Value | Usage
--- | --- | ---
--z-base | 0 | normal flow
--z-sticky | 100 | sticky header / sidebar
--z-dropdown | 200 | dropdown menu / tooltip
--z-overlay | 300 | overlay / bottom Sheet
--z-modal | 400 | dialog
--z-toast | 600 | toast
--z-max | 9999 | reserved: only this tier for extreme fallback

### Motion

Token | Value | Usage
--- | --- | ---
--ease-out | cubic-bezier(0.16, 1, 0.3, 1) | enter, hover, expand
--ease-in-out | cubic-bezier(0.4, 0, 0.2, 1) | panel width, layout changes
--duration-fast | 120ms | press, focus
--duration-base | 160ms | hover, show/hide
--duration-slow | 260ms | dialog, Sheet, layout

#### Reduced motion

i
Under `@media (prefers-reduced-motion: reduce)`, all animation and transition durations drop to about `0.001ms` (effectively off), and the MoonSpinner moon phase pauses on the current frame. Components should not check this individually; it is handled uniformly in the global styles.

### Layout & breakpoints

Layout sizes and responsive breakpoints are tokenized too: sidebar width, content reading-column width, and two global breakpoints. Components should not hard-code pixels.

Token | Value | Usage
--- | --- | ---
--p-sidebar-w | 264px | left session sidebar width
--p-content-max | 760px | chat reading-column max width
--p-content-wide | 920px | wide content (settings / panel)
--p-bp-sm | 640px | mobile / desktop boundary
--p-bp-md | 980px | narrow / wide screen boundary

i
At ≤640px: dialogs become bottom Sheets, the sidebar collapses into an expandable drawer, and Composer toolbar controls are allowed to wrap.
03

## Primitives

Component primitives are the "smallest correct units" of the site UI. Each primitive exposes variants along only two dimensions — `variant` / `size` — with appearance driven by tokens, so it naturally supports light / dark mode and customizable theme colors.

i
For every interactive primitive, the keyboard behavior, focus, and ARIA contract are in §08 Accessibility. New primitives must ship with a keyboard model — mouse-only interaction is not enough.

### Component selection guide

Scenario | Use
--- | ---
Primary action (submit / confirm) | `Button variant=primary`
Secondary action / cancel | `Button secondary` / `ghost`
Destructive action (delete / abort) | `Button danger` / `danger-soft`
Status marker | `Badge`
Toolbar filter / model switch | `Pill`
2–4 mutually exclusive options | `SegmentedControl`
Top tabs | `Tabs`
Switch / multi-select | `Switch` / `Checkbox`
Floating content card / list action menu | `Card` / `Menu`
Inline notice / global toast | `Banner` / `Toast`
Dialog / confirmation · bottom panel (mobile) | `Dialog` / `Sheet`

### Button

4 semantic variants × 3 sizes. The primary action `primary` takes its color from the current theme color (§05 can switch between the blue and black families). Radius uses `--radius-md` uniformly (small size `--radius-sm`), weight 600, with a visible focus ring.

Variant matrix lightpreview
medium · default
small
With icon / state
Dark skin dark

#### API

Button.vue · usage

    <Button variant="primary" size="md" :loading="submitting">Save</Button>
    // variant: primary | secondary | ghost | danger | danger-soft
    // size:    sm | md | lg

States

### IconButton

Unified into three sizes — 26 / 32 / 44px — with a light-grey hover background and a visible focus ring. Replaces the ad-hoc icon + click areas scattered across components today.

IconButton
i
The desktop IconButton comes in `sm` 26 / `md` 32; on touch devices the tap target should be ≥ 44px, so use `lg` 44px, satisfying the §01 accessibility principle (the mobile three-piece set uses `lg`).

### Badge · Chip · Pill

Collapsed into two kinds: Badge (status badge, with an optional status dot) and Pill (the clickable pill in the composer toolbar). Radius, font size, and padding are all unified.

Badge · status badge
Semantic variants
pending running completed needs confirmation failed KIMI
With icon / small size
plan passed read-only
Pill · toolbar pill (composer)
kimi-k2· thinking
yolo
12k / 200k

### Card / Surface

All cards across the site share one shell: flat, `1px` border, `--radius-md` radius, no shadow. The structure is split into three parts — `head / body / foot`. Cards differ only in the head — in two tiers by visual weight, while the shell stays consistent:

- Operation card —— "process" content such as tool calls, Agent, Todo. The head is compact mono with no fill, low weight by default, not competing with the conversation.
- Attention card —— content that needs a user decision, such as Question / Approval. The head carries a semantic color band (accent / warning) to stand out from the message stream.

Operation card · compact mono head (no fill)
read_file session.ts
The head uses mono + a neutral background to emphasize its "code / process" nature; the body uses sans for readability. Flat, radius-md, same shape as the tool group and Agent group.
Attention card · semantic color-band head (accent / warning)
A decision needs your confirmationquestion
The head uses a semantic light background (`accent-soft` / `warning-soft`) to stand out from the message stream, signaling that the user must step in. The shell is exactly the same as the operation card.
Group · the container owns the border, rows are separated by hairlines
3 tool calls· completed
read_filesession.ts
grep"jwt" · 4 hits

- Unified shell: all cards are flat + 1px border + radius-md, casting no shadow.
- Differences are intentional: only the head distinguishes the type (compact mono vs semantic color band); the shell stays consistent.
- Grouping: the outer container owns the border and radius; inner rows are separated by `border-top` hairlines, rather than each row being its own card.
- Status dots: running (pulsing blue) / done (green) / failed (red), sharing one color vocabulary (see §04 tool calls).

### Input / Select / Textarea

Unified 38px height (32px small), `--radius-md` radius, `--color-surface-raised` background, and a unified blue focus ring (`0 0 0 3px accent-soft`).

Form primitives
Workspace name Only letters, numbers, and hyphens are allowed.
Model provider
System prompt
States
Workspace name Please enter a valid workspace name
Display name Normal state · validation passed

### Code / Diff

Inline code, code blocks, and diffs all use the monospace font (`--p-font-mono`). Code blocks have a filename title bar and a copy button. Diffs use `+` / `-` row colors to express additions and deletions — additions use a success light background, deletions use a danger light background, with no gradients.

Code / Diff
inline code
The server uses `jwt.verify(token)` to verify the signature, returning 401 on failure.
code block
session.ts

    import { verify } from './jwt';

    export function auth(token: string) {
      return verify(token, process.env.JWT_SECRET!);
    }

diff
session.ts · +3 -1
import { verify } from './jwt';
-const secret = 'dev-secret';
+const secret = process.env.JWT_SECRET!;
return verify(token, secret);

### Dialog

One dialog primitive replaces 6 hand-written implementations: unified `--radius-xl` radius, `--shadow-xl` shadow, 20px head padding, right-aligned footer actions, and an IconButton close button.

Dialog primitive
New chat
Create an independent Agent chat in the current workspace.
Chat title (optional)
i
Size & height: Dialog offers three widths — `md` 440 / `lg` 640 / `xl` 760 (`--p-content-max`) — chosen by content weight. Height comes in two kinds: `auto` (default, grows with content up to `max-height`) and `fixed` (constant height `min(680px, 100vh - 64px)`, with overflow scrolled inside the body). Content / multi-tab dialogs (settings, model picker, provider manager, folder browser) always use `fixed` so the frame size stays constant and doesn't jump when switching tabs or content length; short confirmation dialogs keep `auto`.

### Toast

Unified information architecture: status icon + title + description. The status color appears only on the icon, avoiding large colored areas that create visual noise.

Toast
Connected to server
The local daemon is responding normally; you can start a new chat.
Context usage 82%
Consider running /compact to free up space.

### Spinner

Loaders fall into two categories by scenario — do not mix them:

- Spinner (plain · SVG ring) —— the default loader. Used for button loading, app startup (GlobalLoading), and general inline waits — "everything else".
- MoonSpinner (moon phase · brand signature) —— used only for the chat waiting state of "message sent, waiting for the Agent's first response" (the sending placeholder in ChatPane, SideChatPanel, ActivityNotice).

#### Spinner · plain loader (default)

Spinner · common scenarios
Loading…

#### MoonSpinner · moon phase (only "waiting for the Agent")

MoonSpinner · chat waiting state only signature
Frame loop (8 frames)
🌑🌒🌓🌔🌕🌖🌗🌘
Usage · only while the chat waits for a response
🌔Thinking… 🌕Waiting for response…
i
The moon phase is the sole exception to the emoji-as-icon rule, and is limited to the "waiting for the Agent's first response" scenario. It is currently implemented twice — in `MoonSpinner.vue` and `ActivityNotice.vue` — and should be merged into a single `MoonSpinner` component, sized via tokens and supporting `prefers-reduced-motion`. All other loading states use the plain Spinner.

### Link

Inline text link: the default is the accent color with no underline; on hover it shows an underline and darkens. The `.muted` variant uses the secondary text color. Used for in-text jumps, external links, "view all", and other lightweight actions.

Link · inline link
Read the full design token docs before building. View on GitHub
View history

### Menu / Dropdown

Dropdown menu panel: raised surface + border + light shadow (`--shadow-sm`, flat-leaning). Menu items support icons, the current (active) state, the danger state, and the disabled state, with separators grouping items. On touch / mobile, use `lg` (≥44px row height) for menu items.

Menu · dropdown menu

### SegmentedControl

Mutually exclusive short option groups, commonly used for 2–4 option switches such as "light / dark / follow system". The current item is highlighted with a raised surface + subtle shadow.

SegmentedControl
Light Dark Follow system

### Tabs

Tabs with a bottom hairline, used for grouping and switching sibling content. The current tab is marked with accent text + an accent underline.

Tabs
General Agent Advanced

### Switch

A two-state switch for settings that take effect immediately. 36×20 track with full radius, 16px knob; when on, the track turns accent and the knob slides right, with the transition driven by tokens.

Switch

### Checkbox

A 17×17 checkbox. When checked it fills with the accent color and shows a white tick (inline SVG). Often paired with a text label.

Checkbox
Enable auto-save

### Avatar

A 32px default avatar with md radius; `.sm` is 24px. Can hold an initial or an icon; falls back to this placeholder when there is no image.

Avatar
K
K

### EmptyState

A centered placeholder for empty lists / panels: a 48px faint icon + title + hint, avoiding blank pages.

EmptyState
No chats yet
Click "New chat" to start a conversation with Kimi

### Divider

A 1px horizontal divider (`--p-line`); `.p-divider-v` is the vertical divider, used between inline elements.

Divider
Content above

Content below
kimi-k2 thinking

### Tooltip

A CSS-only hover hint, wrapped in `.p-tip`. Inverted background (`--p-text` / `--p-bg`), single line, no wrapping — carries only short notes.

Tooltip (hover the button)
New chat

### Banner

An inline notice bar placed at the top of a content area. Three states — `.info` / `.warning` / `.danger` — each with a matching 18px icon.

Banner
Connected to server
Currently in yolo mode; tool calls will run automatically

### Sheet / BottomSheet

A mobile bottom slide-up panel: xl top radius + drag handle, xl shadow. At ≤640px, dialogs become bottom-anchored Sheets.

BottomSheet
Choose a model

### Skeleton

A placeholder for loading content, using a breathing opacity animation (no gradients), following the `no-gradient-text` rule. Composed into titles / text lines / avatars.

Skeleton

### Command Bar

An inline combination of "primary action + command text + copy", sitting between a button and a code block — used for install / onboarding / one-click execution. The primary action reuses `Button primary`; the command area uses a mono light-grey background.

Command Bar
curl -fsSL https://code.kimi.com/install.sh | bash

### TopBar

The application top bar. Solid by default; the `.frost` variant is translucent + background blur, used only for sticky navigation bars, and is the sole exception to the `no-glassmorphism` rule (see §06).

TopBar · solid / frosted glass
Solid TopBar
Frosted-glass TopBar · .frost

### SectionLabel

A small group title for sidebar lists, used to section the content below (such as `Workspaces` in the sidebar). Spec: 13px / 700 / uppercase / letter-spacing `.08em`, color `--color-fg-faint`; left-aligned to the row's starting padding (`--sb-pad-x`), keeping the same indent as the group rows below. For scripts without case (such as Chinese), `text-transform:uppercase` simply has no effect — no special handling needed.

Sidebar · group title
Workspaces
kimi-code-web
playground
04

## Chat Interface Overhaul

The message stream is the core of Kimi Web. The goal of the overhaul: have the 6 card types (Agent / Tool / Question / Approval / Swarm / Todo) share one card skeleton, distinguished only by the head icon and semantic color; and collapse the Composer into a single rounded container.

### Unified message stream

Conversation · 760px reading column
Please change the login endpoint to JWT and add the corresponding unit tests.
🌔Analyzing the auth module…
3 tool calls · completed · 0.8s
read_file src/auth/session.ts 0.2s
12 export function verify(token: string) {
13 return jwt.verify(token, getSecret());
14 }
read_file src/auth/middleware.ts 0.2s
grep "jwt.verify" · 4 matches 0.1s

I looked at the structure of `src/auth`; it is currently based on a session cookie. The scope of the change is below — once you confirm, I'll start.

A decision needs your confirmation
How long should the JWT expiry be? Default 7 days, refresh token 30 days.
Write permission required write_file
About to modify `src/auth/middleware.ts`, 42 lines changed. Allow?
✓Replace session with JWT signing
●Refactor the auth middleware
○Add unit tests

### Tool calls: compact by default, grouped, expand on demand

High-frequency calls like `read_file` / `bash` / `grep` are "operational noise" — if each one took a full card, parallel triggers would quickly drown out the conversation. The new strategy splits tool calls into three tiers by visual weight, pushing them as light as possible:

Three visual-weight tiers
① Tool row · lightest (default)
read_file src/auth/session.ts 0.2s
② Tool group · medium (consecutive / parallel auto-merged; collapsed to one line)
3 tool calls · completed · 0.8s
③ Decision card · heavy (only question / approval, needs user input)
Write permission requiredwrite_file
About to modify `src/auth/middleware.ts`, 42 lines changed.

- Tool calls render as compact rows by default (30px single-line mono + status dot + key argument); no head / body / shadow.
- Consecutive or parallel calls auto-merge into one tool group; when collapsed, the whole group takes one line (`N tool calls · status`).
- Clicking a row expands it in place to show details (code / output); click again to collapse — details don't grab attention by default.
- Status is expressed with a colored dot: running (pulsing blue) / done (green) / failed (red), taking no extra space.
- Only two types keep a full card: `Question` (needs an answer) and `Approval` (needs authorization) — they genuinely need the user's attention.

Tool Call · compact row (expand on demand)
3 tool calls· completed
read_filesession.ts
12 export function verify(…
read_filemiddleware.ts
grep"jwt" · 4 hits

### Composer

Unified into a single rounded container: `--radius-xl`, with the whole border turning blue + a soft focus ring on focus. Toolbar controls all use the Pill / IconButton primitives, and the send button is a 32px circle.

Composer
Message Kimi, / to run a command, @ to reference a file…
yolo
plan
kimi-k2· thinking
i
Site-wide consistency: the composer has only one radius (`--radius-xl` · 16px) and one height; toolbar controls all use the Pill / IconButton primitives, and the send button is a 32px circle — it no longer drifts with the theme.

### Responsive

See §02 `--p-bp-sm` for the breakpoint. This section only gives mobile-adaptation pointers for the chat interface; a full mobile mockup is out of scope for this spec.

i
At ≤640px: dialogs anchor to the bottom as Sheets (xl top radius, top drag handle), the sidebar collapses into an expandable drawer, the Composer toolbar is allowed to wrap, and the chat reading column drops its max-width to fill the screen.
05

## Theming

Kimi Web uses one unified theme: the same components, fonts, radii, shadows, and surfaces — "reskinning" only changes colors. Colors are collapsed into 4 seed tokens — two theme colors + one light surface + one dark surface; the neutrals and accent are derived from them, and the semantic status colors (success / warning / danger) ship as independent palettes paired with the seeds, one set each for light / dark.

### Color seeds

Day-to-day customization only needs these 4 seeds; the whole site's neutrals and accent change with them:

Theme color · primary
--accent-primary
Theme color · secondary
--accent-secondary
Light surface
--surface-light
Dark surface
--surface-dark

### Accent families

Within one theme, the theme color (accent) can switch among several color families. Two parallel families are provided today: blue (default, brand blue, carrying semantic emphasis) and black (neutral black, carrying the most restrained strong action). Both share the same components, fonts, radii, and surfaces — switching families only swaps the accent token set, with zero structural change; more families (green / purple, etc.) can be added later. The two cards below show the same `primary` button under the two families.

Family switch · same primary, different theme color
Blue family · default
accent
--accent #1783ff · soft #e8f3ff
Black family · neutral
accent
--accent #14171c · soft #f1f2f4

### Theme console · change 4 colors, light & dark change together

Theme Console
Primary #1783ff Secondary #6b7280 Light surface #ffffff Dark surface #0d1117
Light surface preview
White background + accent button + neutral text
Dark surface preview
Dark background + same accent + derived text

### Light / dark mode

Driven by the two surfaces `--surface-light` / `--surface-dark`: whichever surface is current derives the corresponding foreground, border, shadow, and status colors. Switching light / dark simply swaps between these two sets of derived tokens, with zero structural change.

✓
Benefits of one theme: components, fonts, radii, and surfaces are consistent site-wide; reskinning only changes 4 color seeds; light / dark mode works out of the box; semantic status colors are independently tunable.
06

## Style Rules

Anti-pattern rules that all UI code must follow. These rules are also the basis of the check-style detection script, one-to-one with a warning.

Rule ID | What it detects | Action
--- | --- | ---
no-gradient-text | gradient text / gradient background | Forbidden
no-glassmorphism | `backdrop-filter: blur` (TopBar sticky nav bar is the sole exception) | TopBar exempt
no-color-glow | colored / large-radius box-shadow glow | Forbidden
no-emoji-icon | using emoji as a functional icon (the moon phases 🌑…🌘 are the sole exception, and only in the "waiting for the Agent to respond" chat state; all other loading states use the plain Spinner) | Moon phase exempt
no-hardcoded-hex | unregistered hex color inside a component `<style>` | Warning
no-hardcoded-font | hard-coded `font-family` in a component (e.g. `'Inter'`) instead of `var(--font-ui)` | Warning
radius-from-scale | radius value not in `{4,6,8,12,16,20,999}` | Warning
z-from-scale | z-index using an unregistered large number | Warning
weight-from-scale | font-weight not in `{400,500}` | Warning

### State matrix

Every interactive primitive should define the following states where applicable; missing ones are flagged by the style rules. `focus-visible` always uses `--p-focus-ring` (appears only on keyboard focus, see §08); `disabled` is uniformly `opacity:.5`.

State | Button | Input | Card | Menu item | Switch
--- | --- | --- | --- | --- | ---
default | ✓ | ✓ | ✓ | ✓ | ✓
hover | ✓ | ✓ | ✓ | ✓ | —
active / pressed | ✓ | — | — | — | —
focus-visible | ✓ | ✓ | — | — | ✓
disabled | ✓ | ✓ | — | ✓ | —
loading | ✓ | — | — | — | —
selected / active | — | — | — | ✓ | ✓
error | — | ✓ | — | — | —
readonly | — | ✓ | — | — | —

### Moon phase exemption

✓
The "🌑…🌘" moon-phase emoji are a brand signature of Kimi Web, used only in the chat state of "message sent, waiting for the Agent's first response", and are rendered uniformly by the `MoonSpinner` component; waiting states such as `ActivityNotice` reuse it rather than implementing their own moon phase. It is the sole exception to the `no-emoji-icon` rule; all other loading states use the plain `Spinner`.

### Glassmorphism exemption

✓
`backdrop-filter: blur` is banned site-wide, with the sole exception of the `.frost` variant of `TopBar` — and only in the one place of the "sticky navigation bar", used to stay readable over scrolling content. No other component (card, dialog, Toast, panel) may use glassmorphism; violations are flagged under `no-glassmorphism`.
07

## App Shell & Sidebar

The structural spec for the app shell (three-column grid + right preview panel) and the left session sidebar. These are business-agnostic "skeletons" — components, fonts, radii, and surfaces are reused from §02 / §03, but layout and alignment have their own conventions.

### Layout grid

On desktop it is a single-row 5-track grid: the sidebar and the right panel each occupy a permanent track, with the conversation column in the middle; two 0-width tracks are for the ResizeHandles.

App.vue · .app

    grid-template-columns: var(--side-w) 0 minmax(0, 1fr) 0 auto;
    /*           sidebar ↑      ↑handle  ↑conversation  ↑handle ↑right panel (auto) */

Token | Value | Usage
--- | --- | ---
--side-w | 248px (adjustable) | left conversation column width, changed by dragging the ResizeHandle; should approach §02's `--p-sidebar-w` (264px)
--preview-w | 460px | width of the right preview panel when open
--panel-head-h | 48px | unified height for all right panel heads + the conversation column head, so the hairline runs as one line
--p-bp-sm | 640px | ≤640 switches to a mobile single column (top bar + conversation), no sidebar / handle / right panel

- The right panel track exists permanently, with its width transitioning between `0 ↔ var(--preview-w)` (when open it squeezes the conversation column, rather than switching templates).
- When the sidebar is collapsed, track 1 becomes a thin "rail" holding only an expand IconButton, avoiding crushing the conversation column head.
- All grid children must have `min-height:0; min-width:0`, so only the inner scroll containers scroll and the page itself does not scroll.

### Sidebar alignment system (`--sb-*`)

All sidebar rows (group head, session row, New chat button) share 3 custom properties, so the "session title" aligns precisely under the "workspace name".

Token | Value | Usage
--- | --- | ---
--sb-pad-x | 16px | row horizontal padding
--sb-gutter | 20px | leading icon slot width (14px icon + 6px whitespace)
--sb-gap | 6px | gap between the icon slot and the text

i
The session title's starting x = `--sb-pad-x + --sb-gutter + --sb-gap`. The group head has a folder icon and the session row has a status slot; both icons are the same width and position, so the titles align naturally.

### Sidebar structure

The sidebar from top to bottom: brand header → search → New chat → grouped list (workspace head + session rows). Controls reuse the §03 primitives as much as possible.

Block | Use | Note
--- | --- | ---
Brand header | logo + name + IconButton | collapse / settings use IconButton sm; the logo is animated (a blinking eye)
Search | bare search row (custom) | no border, hover/focus shows a sunken background; icon + input + clear IconButton. Do not use Input (the 38px bordered version is too heavy)
New chat | full-width left-aligned button (custom) | same rhythm as the session rows in the list (left-aligned, hover sunken). Do not use Button (centered, breaks the rhythm)
Section label | `.p-section-label` | uppercase muted small titles like "Workspaces"
Workspace head / session row | see next two sections | share `--sb-*` alignment

!
Why New chat / search / inline rename don't use Button / Input: they are "list-style" controls (full-width, left-aligned, compact, borderless), while Button is centered and Input is a 38px bordered control — forcing them in would break the sidebar's visual density and alignment. This is an intentional custom exception, not an oversight.

### Session row

A session row is an inset rounded pill, structured as: `status slot → title → attention Badge → trailing action slot`.

Part | Rule
--- | ---
Container | `margin: 1px 6px; padding: 7px 10px; radius-md`; hover = `surface-sunken`; active = `accent-soft` + `inset 0 0 0 1px accent-bd`
Status slot (lead) | fixed `--sb-gutter` width; running = `Spinner` sm, otherwise unread = 7px accent dot
Title | flex:1 with truncation; double-click enters inline rename (compact input, not Input)
Attention Badge | `Badge` sm: info (needs answer) / warning (needs approval) / danger (aborted); always shown, not toggled on hover
Trailing action slot (trail) | fixed slot shared by the time ↔ kebab (see below)
Archive confirmation | replaces the title area, `Button` sm (danger confirm / secondary cancel)

#### Trailing action slot

The session row's rightmost is a fixed-width action slot: at rest it shows the relative time (mono xs, `fg-faint`); on hover it swaps in place to the kebab (`IconButton` sm).

- The time and kebab are stacked with `display:inline-grid` + `grid-area:1/1` and swapped on hover via `visibility` — never `display:none`.
- The slot width is always `max(time width, IconButton sm 26px)`, so the right region's total width is unchanged across hover — badges don't shift, the title's visible character count doesn't change, no jitter.
- A session awaiting an answer (info Badge) likewise shows no time on hover: the slot holds only the kebab.

SessionRow.vue

    .act { display: inline-grid; flex: none; align-items: center; justify-items: center; }
    .act .ts, .act .kebab { grid-area: 1 / 1; }
    .act .kebab { visibility: hidden; }
    .se:hover .act .kebab, .act:has(.kebab.open) .kebab { visibility: visible; }
    .se:hover .act .ts,   .act:has(.kebab.open) .ts   { visibility: hidden; }

i
Why the time and kebab share a slot and use `visibility` rather than `display:none`: the time is variable-width (`2h` / `5m` / `just now` …), the kebab a fixed 26px. If hover used `display:none` to drop the time and append the kebab, the right region would reflow — badges shift and the title's truncation changes, causing visible jitter. Sharing one slot and swapping via `visibility` keeps the slot width constant; this is a hard rule for sidebar list-row stability.

### Workspace group

The group head and session rows share `--sb-*`: folder icon (open/closed) → name → path subtitle, with the kebab and "+" revealed on hover.

- The folder icon sits in the `--sb-gutter` slot, switching icons between open and closed states.
- A small `fg-muted` path line sits below the name.
- The kebab (menu) and "+" (new chat in this workspace) both use `IconButton` sm, shown on hover or keyboard focus (when not hovered they stay in the tab order via `opacity:0`, keeping them keyboard-reachable).
- The group is collapsible; when collapsed its session list is hidden.

### Show more & collapse

The "load more / show less" control at the bottom of each workspace group is a session-row-shaped compact list control (same family as search, New chat, inline rename — not a Button). It doubles as the pagination trigger and the in-group expand / collapse toggle.

Part | Rule
--- | ---
Container | session-row pill: `display:flex; gap:--sb-gap; min-height:26px`, same padding as a session row, `radius-md`; hover = `surface-sunken` (no text recolor); `:focus-visible` uses `--p-focus-ring`
Lead slot | empty, `--sb-gutter` wide, so the label's start x aligns with the session titles (`--sb-pad-x + --sb-gutter + --sb-gap`)
Label | `font-ui`, `text-xs`, `--color-text`; flex:1, truncated
Behavior | "Load more" fetches the next page and auto-expands; once more than the first page is loaded, "Show less" appears and collapses back to the first page (view-layer trim — data is kept, no refetch); "Show all" re-expands

### ResizeHandle

A 4px vertical drag bar, layered over the 1px column border (`margin: 0 -2px` makes the whole 4px grabbable), turning accent on hover / drag.

Rule | Value
--- | ---
Width / cursor | 4px / `col-resize`
Normal / active | transparent / `accent` fill
Layer | `--z-sticky`, over the column border
Behavior | panel width follows the pointer 1:1 while dragging (the parent disables transitions to avoid lag); on release it is persisted to localStorage

### Right panel

The right panels (file preview / Diff / thinking / sub-agent / side chat) share one track and one head primitive.

- The panel head uses the `PanelHeader` primitive (48px = `--panel-head-h`), the same height as the conversation column head, so the hairline runs as one line.
- Panel head: bold mono title + optional muted subtitle + middle slot (Badge / control / path) + close IconButton on the right.
- When opened, the panel width goes from `0 → var(--preview-w)`, smoothly squeezing the conversation column.
- At ≤640px the panel becomes a full-screen overlay (`position:fixed; inset:0`).

i
One-sentence principle: the sidebar / shell is a "list + grid" skeleton that reuses the §02 tokens and §03 primitives (Button / IconButton / Badge / Menu / Spinner / PanelHeader); compact list controls that don't fit a primitive (search, New chat, inline rename, show-more) keep their custom form, governed by this section.
08

## Accessibility (pragmatic edition)

Kimi Web is a local developer tool; it does not target a specific WCAG conformance level, nor maintain a full screen-reader QA matrix. This section collects only the rules that are "low-cost, don't hurt the look, and directly benefit keyboard-heavy users", as the baseline contract for each primitive; the more expensive, lower-ROI parts (such as real-time announcement orchestration for streaming output) are not mandatory for now.

i
On the "ugly" focus ring: the focus visibility required below always uses `:focus-visible` (not `:focus`). It appears only on keyboard focus; mouse clicks don't trigger it, so it doesn't pollute the mouse-driven visual; the ring's strength is tuned uniformly with `--p-focus-ring`, not overridden per place.

#### 1. Contrast & color

- Body text vs. background contrast ≥ 4.5:1; control borders, icons, and key graphics ≥ 3:1. When changing theme colors / dark mode, verify against §05 together.
- Button text vs. button background, and form controls (input, placeholder, helper / error text) vs. their section background must all have contrast ≥ 4.5:1 (large text ≥ 3:1). White-on-white text, a transparent borderless button floating over the page background, and a light placeholder on a near-white background are all flagged by the style rules.
- State is not conveyed by color alone. Error, selected, and disabled states also carry text, an icon, or a shape change (for example an error state is not just red, but also carries text or an icon).

#### 2. Keyboard operable

Anything doable with a mouse must also be doable with a keyboard; Tab order follows the DOM, with no invented skipping. Composite controls define their keyboard model per the table below; a missing model is treated as incomplete:

Control | Keyboard behavior
--- | ---
Dialog | `Tab` cycles within the dialog (focus trap); `Esc` closes; focus returns to the trigger element after closing.
Menu | `↑` / `↓` move the highlight, `Enter` selects, `Esc` closes.
Tabs | `←` / `→` switch tabs (roving tabindex); only the current tab is in the Tab sequence.
Switch / Segmented | `←` / `→` or `Space` / `Enter` to toggle.

#### 3. Focus visibility

- Every interactive element must have a visible focus indicator on keyboard focus, uniformly via `:focus-visible` + `--p-focus-ring` (primary actions may use `--p-focus-ring-strong`).
- Bare `outline: none` is forbidden. To remove the default outline, you must provide an equivalent replacement style.

#### 4. Labels & semantics

- Semantic HTML first (button / a / input / dialog…); ARIA is added only when native semantics fall short.
- Icon-only buttons must have an `aria-label` — `IconButton` already enforces this with a required `label` prop.
- Dialog: `role="dialog"` + `aria-modal="true"`, with the title as the dialog's accessible name.
- Purely decorative SVG / icons get `aria-hidden="true"` to avoid being read out by screen readers.

#### 5. Target size

Desktop click targets ≥ 32px; touch devices ≥ 44px (consistent with the §01 principle and the IconButton `lg` tier).

#### 6. Reduced motion

Handled uniformly in the global styles per §02's `@media (prefers-reduced-motion: reduce)`; components do not check this individually. The MoonSpinner moon phase pauses on the current frame.

#### 7. Live announcements (non-mandatory)

Screen-reader announcements are not a mandatory contract in this product. Short hints like Toast can use `role="status"` / `aria-live`; chat streaming output is currently not announced word-by-word, which is an acceptable trade-off, to be added later if a real need arises.

✓
Explicitly not mandatory for now: a WCAG conformance-level claim, a complete ARIA pattern table, a per-screen-reader QA matrix, and real-time announcement orchestration for streaming output — these are not written into the primitive contract, to avoid becoming slogans no one maintains.