关于 desktop-principles
面向 Web、macOS、Windows 和 Linux 桌面界面的 UX 技能,提供指针反馈、键盘操作、窗口、焦点、信息密度与克制动效的实现指引。
主要功能
- 01悬停与指针交互指引
- 02键盘快捷键与焦点
- 03多窗口模式
- 04桌面信息密度
- 05克制动效原则
适用场景
- 01为桌面控件添加清晰的悬停反馈
- 02规划快捷键和可见的键盘焦点
- 03判断使用新窗口还是临时面板
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ athevon/ genjutsu --skill desktop-principles安装后在 Claude Code 中输入/desktop-principles确认已加载。
详细介绍
版本敏感。 以下所有 API 名称、SDK 门槛和浏览器支持声明均已于 2026-09-08 根据一手资料核实。核实依据和时间见
_jutsu/VERSIONS.md。如果该日期已久远,请在使用版本号前重新核实。
桌面端原则
桌面端 UX 背景。检测到桌面环境(macOS、Windows、Linux 桌面端、Web 桌面端)时加载。 此处提供简明规则。深入内容见
references/。
悬停状态不可或缺
在桌面端,悬停是主要的交互提示,与移动端相反。指针悬停在目标上却没有即时视觉反馈,会让人觉得界面出了问题:用户依赖 :hover 在点击前确认元素是否可交互。每个可点击的界面区域都必须有明显的悬停样式,最好设置 100-200ms 的过渡效果,让变化清晰可感,又不会显得迟缓。
CSS - 交互元素的悬停样式:
.button {
background: var(--surface);
transition: background 120ms ease-out, transform 120ms ease-out;
}
.button:hover {
background: var(--surface-hover);
transform: translateY(-1px);
}
.button:active {
transform: translateY(0);
}
SwiftUI - .onHover 是通用的悬停信号;.hoverEffect 仅适用于 iOS/iPadOS,必须通过 #if 进行条件编译:
| 修饰符 | 支持情况 | 说明 |
|---|---|---|
.onHover { Bool } | iOS 13.4+、iPadOS 13.4+、Mac Catalyst 13.4+、macOS 10.15+、visionOS 1.0+ | 在 macOS 上可用的修饰符 |
.onContinuousHover { HoverPhase } | iOS 17.0+、macOS 14.0+、tvOS 17.0+、visionOS 1.0+ | 提供指针位置,不只是进入/移出状态 |
.hoverEffect(_:) | iOS 13.4+、iPadOS 13.4+、Mac Catalyst 13.4+、tvOS 16.0+、visionOS 1.0+ - macOS 不支持 | 在 macOS 目标中会导致编译错误 |
.pointerStyle(_:) | macOS 15.0+、visionOS 2.0+ | 更改视图上的 macOS 光标 |
struct ToolbarButton: View {
@State private var hovering = false
var body: some View {
Image(systemName: "square.and.arrow.up")
.padding(8)
.background(hovering ? Color.gray.opacity(0.15) : .clear)
.onHover { hovering = $0 }
.animation(.easeOut(duration: 0.12), value: hovering)
.pointerHighlight() // see the extension below - .hoverEffect is UNAVAILABLE on macOS
}
}
// `.hoverEffect` is @available(macOS, unavailable). Gate it with #if, never with #available.
extension View {
@ViewBuilder func pointerHighlight() -> some View {
#if os(macOS)
self
#else
self.hoverEffect(.highlight)
#endif
}
}
Compose Desktop - 使用 onPointerEvent 或 hoverable + interactionSource:
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun ToolbarButton(onClick: () -> Unit) {
val interactionSource = remember { MutableInteractionSource() }
val hovered by interactionSource.collectIsHoveredAsState()
Box(
modifier = Modifier
.hoverable(interactionSource)
.background(if (hovered) Color.LightGray.copy(alpha = 0.15f) else Color.Transparent)
.clickable(onClick = onClick)
.padding(8.dp),
) { Icon(Icons.Default.Share, contentDescription = "Share") }
}
指针精确度
鼠标和触控板指针远比拇指精准,因此桌面端目标可以小于移动端的 44pt 最小值。常见范围是图标按钮 24-32px、工具栏项 28-36px。WCAG 2.5.8 Target Size (Minimum)(目标尺寸(最小))AA 级将底线设为24x24 CSS 像素,适用于所有指针输入,并非仅限桌面端。小于 24px 的目标仍可通过间距例外满足要求:以每个尺寸不足的目标为圆心,绘制直径为 24px 的圆,且任意两个圆都不得相交。Apple 的 macOS HIG 在理念上更严格、数值上更宽松:默认控件尺寸为 28x28 pt,绝对最小尺寸为 20x20 pt。
**实际应用 Fitts 定律:**获取目标所需的时间会随着目标尺寸增大而缩短,随着距离增加而延长。屏幕边缘和角落是深度无限的目标——无论指针滑过多远,都会停在那里。将高频全局控件(关闭窗口、系统菜单、应用程序 Dock)放在角落和边缘。macOS 菜单栏和 Windows 任务栏就是典型应用:锚定在边缘,获取目标时不会越过目标。
键盘快捷键(一级功能)
桌面端用户期待快捷键符合原生惯例。在列表繁多的应用中缺少 ⌘+F 不是极简主义,而是缺陷。
| 操作 | macOS | Windows / Linux |
|---|---|---|
| 新建 | ⌘+N | Ctrl+N |
| 关闭窗口 | ⌘+W | Ctrl+W |
| 退出应用 | ⌘+Q | Alt+F4 |
| 设置 / 偏好设置 | ⌘+, | Ctrl+, |
| 查找 | ⌘+F | Ctrl+F |
| 切换(注释、侧边栏……) | ⌘+/ | Ctrl+/ |
| 保存 | ⌘+S | Ctrl+S |
| 命令面板 | ⌘+K 或 ⌘+Shift+P | Ctrl+K 或 Ctrl+Shift+P |
Web - 正确检测 Ctrl 与 Cmd:
// Prefer event.metaKey on macOS, event.ctrlKey elsewhere.
// navigator.platform is deprecated but still pragmatic; fall back to userAgent.
// navigator.platform is always a non-empty string where it exists, so `||` never falls through.
// Prefer UA-CH where available, then platform, then the UA string.
const isMac = /Mac|iPhone|iPad/.test(
navigator.userAgentData?.platform ?? navigator.platform ?? navigator.userAgent ?? ""
);
window.addEventListener("keydown", (e) => {
const cmdOrCtrl = isMac ? e.metaKey : e.ctrlKey;
if (cmdOrCtrl && e.key.toLowerCase() === "k") {
e.preventDefault();
openCommandPalette();
}
});
SwiftUI - .keyboardShortcut 绑定到菜单命令:
Button("New Document", action: newDoc)
.keyboardShortcut("n", modifiers: .command)
Button("Find", action: focusSearch)
.keyboardShortcut("f", modifiers: .command)
Compose Desktop - onKeyEvent + KeyShortcut:
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.commandShortcut(key: Key, onTrigger: () -> Unit) =
onKeyEvent { event ->
if (event.type == KeyEventType.KeyDown && event.isMetaPressed && event.key == key) {
onTrigger(); true
} else false
}
// In MenuBar:
MenuBar {
Menu("File") {
Item("New", shortcut = KeyShortcut(Key.N, meta = true), onClick = ::newDoc)
Item("Find", shortcut = KeyShortcut(Key.F, meta = true), onClick = ::focusSearch)
}
}
多窗口模式
桌面用户会将窗口并排放置。以下情况适合打开新窗口:
- 任务耗时较长,用户希望继续在主窗口中工作(渲染、导出、同步日志)。
- 用户需要比较两个并行的上下文(两份文档、两个聊天、两个问题)。
- 应用以文档为中心,且每份文档都是同级对象(Pages、Figma 文件、Xcode 项目)。
对于临时确认、简短的设置面板,或可以放在工作表或弹出框中的内容,打开新窗口并不合适。
SwiftUI - 用于文档样式窗口的 WindowGroup,用于单例的 Window:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup("Document") { DocumentView() } // peer windows, one per doc
Window("Inspector", id: "inspector") { InspectorView() }
.windowResizability(.contentSize) // tracks intrinsic content size
Settings { SettingsView() } // ⌘+, target on macOS
}
}
Compose Desktop - Window 可组合项、应用程序作用域:
fun main() = application {
val docs = remember { mutableStateListOf(Document()) }
docs.forEach { doc ->
Window(onCloseRequest = { docs.remove(doc) }, title = doc.title) {
DocumentView(doc)
}
}
if (showInspector) {
Window(onCloseRequest = { showInspector = false }, title = "Inspector") {
InspectorView()
}
}
}
**状态共享:**窗口是同一模型上的视图。将事实来源保存在单例或 DI 作用域对象中(通过环境注入的 SwiftUI @Observable、Compose koin 或等效的 viewModel)。切勿为每个窗口复制状态——协调彼此分歧的副本会引发大量 bug。
焦点管理
键盘导航是桌面端的一等输入方式。Tab 顺序必须合理,焦点环必须清晰可见,移除焦点环又不提供替代方案会导致无障碍体验退步。
SwiftUI - @FocusState 控制字段焦点:
struct LoginForm: View {
enum Field { case email, password }
@State private var email = ""
@State private var password = ""
@FocusState private var focused: Field?
var body: some View {
VStack {
TextField("Email", text: $email)
.focused($focused, equals: .email)
.onSubmit { focused = .password }
SecureField("Password", text: $password)
.focused($focused, equals: .password)
.onSubmit(submit)
}
.onAppear { focused = .email }
}
}
Compose - FocusRequester + LocalFocusManager:
val emailFocus = remember { FocusRequester() }
val passwordFocus = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
TextField(
value = email, onValueChange = { email = it },
modifier = Modifier.focusRequester(emailFocus),
// Without keyboardOptions.imeAction the IME never emits Next and onNext never fires.
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(onNext = { passwordFocus.requestFocus() }),
)
TextField(
value = password, onValueChange = { password = it },
modifier = Modifier.focusRequester(passwordFocus).focusable(),
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus(); submit() }),
)
LaunchedEffect(Unit) { emailFocus.requestFocus() }
Web - tabindex + :focus-visible:
.button {
/* Never `outline: none` without an alternative. */
}
.button:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
<div role="button" tabindex="0" class="button">Custom button</div>
信息密度
桌面用户面对 13-32 英寸的屏幕,使用精准的指针和完整键盘。他们每个视口能解析、也希望解析的信息比移动端更多。使用 8px 基础网格(移动端为 4-8px)、持久侧边栏而非底部标签页、面向高级用户的命令面板(⌘K),以及适合数据需求的高密度数据表。Linear、Things 3 和 Notion 是值得借鉴的范例:信息丰富却不显拥挤,每个像素都物尽其用。
微妙动画原则
桌面应用会被连续注视数小时。初次体验令人愉悦的动画,重复上百次后可能变得难以忍受。优先使用简短且纯粹功能性的动效:透明度变化和低于 200ms 的轻微位移;常规交互中不要使用弹跳效果,悬停时也不要俏皮地过度移动。富有表现力的动效只用于一次性场景(引导流程、成功状态),不要用于日常 UI。
/* BAD - every hover bounces for 600ms, exhausting after the third use */
.card {
transition: transform 600ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.card:hover { transform: scale(1.05); }
/* GOOD - 100ms opacity, almost subliminal, never tires */
.card {
opacity: 0.92;
transition: opacity 100ms ease-out;
}
.card:hover { opacity: 1; }
反模式(BAD / GOOD)
1. 在桌面端将导航隐藏在汉堡菜单后
<header>
<button class="hamburger" aria-label="Menu">☰</button>
</header>
<nav class="drawer hidden">...</nav>
<aside class="sidebar">
<nav>
<a href="/inbox">Inbox</a>
<a href="/projects">Projects</a>
<a href="/archive">Archive</a>
</nav>
<button class="collapse-toggle" aria-label="Collapse sidebar">⇤</button>
</aside>
2. 主要操作没有键盘快捷键
// BAD - "New" is buried in a menu, no shortcut, every creation is 3 clicks
<Toolbar>
<Menu>
<MenuItem onClick={newDoc}>New document</MenuItem>
</Menu>
</Toolbar>
// GOOD - ⌘N baseline, surfaced in the menu, mirrored on the toolbar tooltip
<Toolbar>
<button onClick={newDoc} title="New document (⌘N)">
<PlusIcon />
</button>
</Toolbar>
// Bound globally:
useShortcut("mod+n", newDoc);
useShortcut("mod+s", save);
useShortcut("mod+f", focusSearch);
3. 移除焦点环却不提供替代方案
/* BAD - keyboard users now have no idea where focus is */
button:focus { outline: none; }
/* GOOD - :focus-visible keeps mouse clicks ring-free, keyboard navigation visible */
button:focus { outline: none; }
button:focus-visible {
outline: 2px solid var(--brand-500);
outline-offset: 2px;
border-radius: 6px;
}
快速参考:加载子技能
| 需求 | 加载 |
|---|---|
| 深入了解键盘模式 | references/keyboard-patterns.md |
| 多窗口状态 | references/multi-window.md |
| SwiftUI 动画 | ../swiftui-motion/SKILL.md |
| Compose Desktop | ../compose-multiplatform/SKILL.md(当桌面端是 CMP 目标时) |
来源
- Apple Human Interface Guidelines (macOS):https://developer.apple.com/design/human-interface-guidelines/macos
- Microsoft Fluent Design 2:https://fluent2.microsoft.design/
- GNOME Human Interface Guidelines:https://developer.gnome.org/hig/