mcpField
D
desktop-principles

desktop-principles

github.com/athevonデザイン・UI698981 回インストール3 週間前に更新
リポジトリ

desktop-principles について

Web、macOS、Windows、Linux のデスクトップ UI 向け UX スキルです。ポインターへの反応、キーボード操作、ウィンドウ、フォーカス、情報密度、控えめな動きを実装する指針を示します。

主な機能

  1. 01ホバーとポインター操作の指針
  2. 02キーボードショートカットとフォーカス
  3. 03マルチウィンドウのパターン
  4. 04デスクトップの情報密度
  5. 05控えめなアニメーションの原則

活用シーン

  1. 01デスクトップ操作部品に明確なホバー反応を追加
  2. 02ショートカットと視認できるキーボードフォーカスを設計
  3. 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 がないのは、ミニマリズムではなくバグです。

操作macOSWindows / Linux
新規作成⌘+NCtrl+N
ウィンドウを閉じる⌘+WCtrl+W
アプリを終了⌘+QAlt+F4
設定/環境設定⌘+,Ctrl+,
検索⌘+FCtrl+F
切り替え(コメント、サイドバーなど)⌘+/Ctrl+/
保存⌘+SCtrl+S
コマンドパレット⌘+K または ⌘+Shift+PCtrl+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 のターゲットである場合)

出典

コメント