desktop-principles について
Web、macOS、Windows、Linux のデスクトップ UI 向け 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 デスクトップ、ウェブデスクトップ)が検出されると読み込まれます。 ここでは簡潔なルールを記載します。詳細は
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「ターゲットサイズ(最小)」レベル AA では、あらゆるポインター入力に対する最小値が 24x24 CSS ピクセルと定められています。これはデスクトップに固有の基準ではありません。24px 未満のターゲットでも、間隔に関する例外を満たせば適合します。サイズの小さい各ターゲットを中央に配置した直径 24px の円を描き、どの円も交差しないようにしてください。Apple の macOS HIG は、考え方としてはより厳格で、数値としてはより緩やかです。デフォルトのコントロールサイズは 28x28 pt、絶対最小サイズは 20x20 pt です。
実践におけるフィッツの法則: ターゲットのサイズが大きいほど到達時間は短くなり、距離が長いほど到達時間は長くなります。画面の端や隅は奥行きが無限のターゲットです。カーソルは行き過ぎてもそこで止まります。使用頻度の高いグローバルコントロール(ウィンドウを閉じる、システムメニュー、アプリの 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)
}
}
マルチウィンドウのパターン
デスクトップユーザーは、複数のウィンドウを並べて使います。次のような場合は、新しいウィンドウを開くのが適切です。
- タスクに時間がかかり、ユーザーがメインウィンドウで作業を続けたい場合(レンダリング、エクスポート、同期ログなど)。
- 2つのドキュメント、2つのチャット、2つの課題など、並行する2つのコンテキストを比較する場合。
- アプリがドキュメントベースで、各ドキュメントが同等のものとして扱われる場合(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 - ウィンドウコンポーザブル、アプリケーションスコープ:
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 相当)に信頼できる情報源を保持してください。ウィンドウごとに状態を複製してはいけません。食い違ったコピーの整合を取ることは、バグの温床になります。
フォーカス管理
キーボード操作は、デスクトップにおける主要な入力手段です。タブ順は自然で、フォーカスリングは見える状態にする必要があります。代替手段なしにフォーカスリングを取り除くと、アクセシビリティが低下します。
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インチの画面と精密なポインター、フルキーボードを使います。モバイルよりも、1画面に多くの情報を表示でき、ユーザーもそれを望んでいます。8px を基本グリッドとして使い(モバイルでは 4〜8px)、下部タブの代わりに常時表示のサイドバーを使い、パワーユーザー向けにコマンドパレット(⌘K)を用意し、データに見合う場合は密度の高いデータテーブルを使いましょう。参考になるのは Linear、Things 3、Notion です。窮屈さを感じさせずに情報を豊富に伝え、すべてのピクセルを有効活用しています。
控えめなアニメーションの原則
デスクトップアプリは何時間も見続けるものです。一度は心地よく感じるアニメーションも、100回目には耐えがたいものになります。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/