git_ui: Design Polish (#26361)

Nate Butler , Cole Miller , Cole Miller , and Max Brunsfeld created

Polish PR

- [ ] Horizontal scrollbar for git panel
- [ ] Allow shift clicking a checkbox in any section to stage the whole
section
- [ ] Clean up design of no changes/pending push state in panel
- [x] Ensure checkbox placeholder dot is centered in the checkbox
- [x] Improve spacing between elements in git panel entries
- [x] Update git branch icon to match branch selector text when disabled
- [x] Truncate last commit message less aggressively in panel
- [x] Clean up new panel header design
- [x] Remove `_background` version control keys (backgrounds are derived
from the foreground colors)

### Previous message truncation:

Before:

![CleanShot 2025-03-10 at 11 54
32@2x](https://github.com/user-attachments/assets/46b18f66-bb5c-435e-a0da-6cc931bd8a15)

After:

![CleanShot 2025-03-10 at 11 55
24@2x](https://github.com/user-attachments/assets/fcf688c7-b949-41a2-a7b8-1a198eb7fa4a)

### Make branch icon match when menu is disabled

Before:

![CleanShot 2025-03-10 at 12 02
14@2x](https://github.com/user-attachments/assets/1990f4b3-c2f0-4e02-89ad-211aaebb3821)

After:

![CleanShot 2025-03-10 at 12 02
53@2x](https://github.com/user-attachments/assets/9b1caf65-c48f-44c9-924b-484892fb543f)

Release Notes:

- N/A *or* Added/Fixed/Improved ...

---------

Co-authored-by: Cole Miller <cole@zed.dev>
Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

crates/git_ui/src/git_panel.rs            |  96 ++++++++++++++-------
crates/git_ui/src/git_ui.rs               | 106 ++++++++++++++++++------
crates/panel/src/panel.rs                 |   2 
crates/theme/src/default_colors.rs        |   8 -
crates/theme/src/fallback_themes.rs       |   4 
crates/theme/src/schema.rs                |  32 -------
crates/theme/src/styles/colors.rs         |  22 -----
crates/ui/src/components/button/button.rs |   5 
crates/ui/src/components/toggle.rs        |  61 ++++++++-----
9 files changed, 180 insertions(+), 156 deletions(-)

Detailed changes

crates/git_ui/src/git_panel.rs 🔗

@@ -41,7 +41,8 @@ use language_model::{
 use menu::{Confirm, SecondaryConfirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
 use multi_buffer::ExcerptInfo;
 use panel::{
-    panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button, PanelHeader,
+    panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
+    panel_icon_button, PanelHeader,
 };
 use project::{
     git::{GitEvent, Repository},
@@ -103,13 +104,13 @@ enum TrashCancel {
 }
 
 fn git_panel_context_menu(
-    focus_handle: Option<FocusHandle>,
+    focus_handle: FocusHandle,
     window: &mut Window,
     cx: &mut App,
 ) -> Entity<ContextMenu> {
     ContextMenu::build(window, cx, |context_menu, _, _| {
         context_menu
-            .when_some(focus_handle, |el, focus_handle| el.context(focus_handle))
+            .context(focus_handle)
             .action("Stage All", StageAll.boxed_clone())
             .action("Unstage All", UnstageAll.boxed_clone())
             .separator()
@@ -2309,6 +2310,18 @@ impl GitPanel {
         self.has_staged_changes()
     }
 
+    fn render_overflow_menu(&self, id: impl Into<ElementId>) -> impl IntoElement {
+        let focus_handle = self.focus_handle.clone();
+        PopoverMenu::new(id.into())
+            .trigger(
+                IconButton::new("overflow-menu-trigger", IconName::EllipsisVertical)
+                    .icon_size(IconSize::Small)
+                    .icon_color(Color::Muted),
+            )
+            .menu(move |window, cx| Some(git_panel_context_menu(focus_handle.clone(), window, cx)))
+            .anchor(Corner::TopRight)
+    }
+
     pub(crate) fn render_generate_commit_message_button(
         &self,
         cx: &Context<Self>,
@@ -2458,9 +2471,17 @@ impl GitPanel {
             tooltip = "git add --all ."
         }
 
+        let change_string = match self.entry_count {
+            0 => "No Changes".to_string(),
+            1 => "1 Change".to_string(),
+            _ => format!("{} Changes", self.entry_count),
+        };
+
         self.panel_header_container(window, cx)
+            .px_2()
             .child(
-                Button::new("diff", "Open Diff")
+                panel_button(change_string)
+                    .color(Color::Muted)
                     .tooltip(Tooltip::for_action_title_in(
                         "Open diff",
                         &Diff,
@@ -2473,8 +2494,9 @@ impl GitPanel {
                     }),
             )
             .child(div().flex_grow()) // spacer
+            .child(self.render_overflow_menu("overflow_menu"))
             .child(
-                Button::new("stage-unstage-all", text)
+                panel_filled_button(text)
                     .tooltip(Tooltip::for_action_title_in(
                         tooltip,
                         action.as_ref(),
@@ -2631,15 +2653,14 @@ impl GitPanel {
                 .items_center()
                 .py_2()
                 .px(px(8.))
-                // .bg(cx.theme().colors().background)
-                // .border_t_1()
                 .border_color(cx.theme().colors().border)
                 .gap_1p5()
                 .child(
                     div()
                         .flex_grow()
                         .overflow_hidden()
-                        .max_w(relative(0.6))
+                        .items_center()
+                        .max_w(relative(0.85))
                         .h_full()
                         .child(
                             Label::new(commit.subject.clone())
@@ -2946,7 +2967,7 @@ impl GitPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let context_menu = git_panel_context_menu(Some(self.focus_handle.clone()), window, cx);
+        let context_menu = git_panel_context_menu(self.focus_handle.clone(), window, cx);
         self.set_context_menu(context_menu, position, window, cx);
     }
 
@@ -2993,6 +3014,9 @@ impl GitPanel {
         let marked = self.marked_entries.contains(&ix);
         let status_style = GitPanelSettings::get_global(cx).status_style;
         let status = entry.status;
+        let modifiers = self.current_modifiers;
+        let shift_held = modifiers.shift;
+
         let has_conflict = status.is_conflicted();
         let is_modified = status.is_modified();
         let is_deleted = status.is_deleted();
@@ -3078,7 +3102,7 @@ impl GitPanel {
             .px(rems(0.75)) // ~12px
             .overflow_hidden()
             .flex_none()
-            .gap(DynamicSpacing::Base04.rems(cx))
+            .gap_1p5()
             .bg(base_bg)
             .hover(|this| this.bg(hover_bg))
             .active(|this| this.bg(active_bg))
@@ -3123,6 +3147,7 @@ impl GitPanel {
                     .flex_none()
                     .occlude()
                     .cursor_pointer()
+                    .ml_neg_0p5()
                     .child(
                         Checkbox::new(checkbox_id, is_staged)
                             .disabled(!has_write_access)
@@ -3144,17 +3169,35 @@ impl GitPanel {
                                 })
                             })
                             .tooltip(move |window, cx| {
-                                let tooltip_name = if entry_staging.is_fully_staged() {
-                                    "Unstage"
+                                let is_staged = entry_staging.is_fully_staged();
+
+                                let action = if is_staged { "Unstage" } else { "Stage" };
+                                let tooltip_name = if shift_held {
+                                    format!("{} section", action)
                                 } else {
-                                    "Stage"
+                                    action.to_string()
                                 };
 
-                                Tooltip::for_action(tooltip_name, &ToggleStaged, window, cx)
+                                let meta = if shift_held {
+                                    format!(
+                                        "Release shift to {} single entry",
+                                        action.to_lowercase()
+                                    )
+                                } else {
+                                    format!("Shift click to {} section", action.to_lowercase())
+                                };
+
+                                Tooltip::with_meta(
+                                    tooltip_name,
+                                    Some(&ToggleStaged),
+                                    meta,
+                                    window,
+                                    cx,
+                                )
                             }),
                     ),
             )
-            .child(git_status_icon(status, cx))
+            .child(git_status_icon(status))
             .child(
                 h_flex()
                     .items_center()
@@ -3456,27 +3499,11 @@ impl PanelRepoFooter {
             git_panel: None,
         }
     }
-
-    fn render_overflow_menu(&self, id: impl Into<ElementId>, cx: &App) -> impl IntoElement {
-        let focus_handle = self
-            .git_panel
-            .as_ref()
-            .map(|git_panel| git_panel.focus_handle(cx));
-        PopoverMenu::new(id.into())
-            .trigger(
-                IconButton::new("overflow-menu-trigger", IconName::EllipsisVertical)
-                    .icon_size(IconSize::Small)
-                    .icon_color(Color::Muted),
-            )
-            .menu(move |window, cx| Some(git_panel_context_menu(focus_handle.clone(), window, cx)))
-            .anchor(Corner::TopRight)
-    }
 }
 
 impl RenderOnce for PanelRepoFooter {
     fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
         let active_repo = self.active_repository.clone();
-        let overflow_menu_id: SharedString = format!("overflow-menu-{}", active_repo).into();
         let repo_selector_trigger = Button::new("repo-selector", active_repo)
             .style(ButtonStyle::Transparent)
             .size(ButtonSize::None)
@@ -3565,7 +3592,11 @@ impl RenderOnce for PanelRepoFooter {
                         div().child(
                             Icon::new(IconName::GitBranchSmall)
                                 .size(IconSize::Small)
-                                .color(Color::Muted),
+                                .color(if single_repo {
+                                    Color::Disabled
+                                } else {
+                                    Color::Muted
+                                }),
                         ),
                     )
                     .child(repo_selector)
@@ -3584,7 +3615,6 @@ impl RenderOnce for PanelRepoFooter {
                     .gap_1()
                     .flex_shrink_0()
                     .children(spinner)
-                    .child(self.render_overflow_menu(overflow_menu_id, cx))
                     .when_some(branch, |this, branch| {
                         let mut focus_handle = None;
                         if let Some(git_panel) = self.git_panel.as_ref() {

crates/git_ui/src/git_ui.rs 🔗

@@ -1,13 +1,13 @@
 use ::settings::Settings;
 use git::{
     repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
-    status::FileStatus,
+    status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
 };
 use git_panel_settings::GitPanelSettings;
 use gpui::{App, Entity, FocusHandle};
 use project::Project;
 use project_diff::ProjectDiff;
-use ui::{ActiveTheme, Color, Icon, IconName, IntoElement, SharedString};
+use ui::prelude::*;
 use workspace::Workspace;
 
 mod askpass_modal;
@@ -86,30 +86,8 @@ pub fn init(cx: &mut App) {
     .detach();
 }
 
-// TODO: Add updated status colors to theme
-pub fn git_status_icon(status: FileStatus, cx: &App) -> impl IntoElement {
-    let (icon_name, color) = if status.is_conflicted() {
-        (
-            IconName::Warning,
-            cx.theme().colors().version_control_conflict,
-        )
-    } else if status.is_deleted() {
-        (
-            IconName::SquareMinus,
-            cx.theme().colors().version_control_deleted,
-        )
-    } else if status.is_modified() {
-        (
-            IconName::SquareDot,
-            cx.theme().colors().version_control_modified,
-        )
-    } else {
-        (
-            IconName::SquarePlus,
-            cx.theme().colors().version_control_added,
-        )
-    };
-    Icon::new(icon_name).color(Color::Custom(color))
+pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
+    GitStatusIcon::new(status)
 }
 
 fn can_push_and_pull(project: &Entity<Project>, cx: &App) -> bool {
@@ -465,3 +443,79 @@ mod remote_button {
         }
     }
 }
+
+#[derive(IntoElement, IntoComponent)]
+#[component(scope = "Version Control")]
+pub struct GitStatusIcon {
+    status: FileStatus,
+}
+
+impl GitStatusIcon {
+    pub fn new(status: FileStatus) -> Self {
+        Self { status }
+    }
+}
+
+impl RenderOnce for GitStatusIcon {
+    fn render(self, _window: &mut ui::Window, cx: &mut App) -> impl IntoElement {
+        let status = self.status;
+
+        let (icon_name, color) = if status.is_conflicted() {
+            (
+                IconName::Warning,
+                cx.theme().colors().version_control_conflict,
+            )
+        } else if status.is_deleted() {
+            (
+                IconName::SquareMinus,
+                cx.theme().colors().version_control_deleted,
+            )
+        } else if status.is_modified() {
+            (
+                IconName::SquareDot,
+                cx.theme().colors().version_control_modified,
+            )
+        } else {
+            (
+                IconName::SquarePlus,
+                cx.theme().colors().version_control_added,
+            )
+        };
+
+        Icon::new(icon_name).color(Color::Custom(color))
+    }
+}
+
+// View this component preview using `workspace: open component-preview`
+impl ComponentPreview for GitStatusIcon {
+    fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
+        fn tracked_file_status(code: StatusCode) -> FileStatus {
+            FileStatus::Tracked(git::status::TrackedStatus {
+                index_status: code,
+                worktree_status: code,
+            })
+        }
+
+        let modified = tracked_file_status(StatusCode::Modified);
+        let added = tracked_file_status(StatusCode::Added);
+        let deleted = tracked_file_status(StatusCode::Deleted);
+        let conflict = UnmergedStatus {
+            first_head: UnmergedStatusCode::Updated,
+            second_head: UnmergedStatusCode::Updated,
+        }
+        .into();
+
+        v_flex()
+            .gap_6()
+            .children(vec![example_group(vec![
+                single_example("Modified", GitStatusIcon::new(modified).into_any_element()),
+                single_example("Added", GitStatusIcon::new(added).into_any_element()),
+                single_example("Deleted", GitStatusIcon::new(deleted).into_any_element()),
+                single_example(
+                    "Conflicted",
+                    GitStatusIcon::new(conflict).into_any_element(),
+                ),
+            ])])
+            .into_any_element()
+    }
+}

crates/panel/src/panel.rs 🔗

@@ -18,8 +18,6 @@ pub trait PanelHeader: workspace::Panel {
             .w_full()
             .px_1()
             .flex_none()
-            .border_b_1()
-            .border_color(cx.theme().colors().border)
     }
 }
 

crates/theme/src/default_colors.rs 🔗

@@ -136,14 +136,10 @@ impl ThemeColors {
             terminal_ansi_dim_white: neutral().light().step_11(),
             link_text_hover: orange().light().step_10(),
             version_control_added: ADDED_COLOR,
-            version_control_added_background: ADDED_COLOR.opacity(0.1),
             version_control_deleted: REMOVED_COLOR,
-            version_control_deleted_background: REMOVED_COLOR.opacity(0.1),
             version_control_modified: MODIFIED_COLOR,
-            version_control_modified_background: MODIFIED_COLOR.opacity(0.1),
             version_control_renamed: MODIFIED_COLOR,
             version_control_conflict: orange().light().step_12(),
-            version_control_conflict_background: orange().light().step_12().opacity(0.1),
             version_control_ignored: gray().light().step_12(),
         }
     }
@@ -253,14 +249,10 @@ impl ThemeColors {
             terminal_ansi_dim_white: neutral().dark().step_10(),
             link_text_hover: orange().dark().step_10(),
             version_control_added: ADDED_COLOR,
-            version_control_added_background: ADDED_COLOR.opacity(0.1),
             version_control_deleted: REMOVED_COLOR,
-            version_control_deleted_background: REMOVED_COLOR.opacity(0.1),
             version_control_modified: MODIFIED_COLOR,
-            version_control_modified_background: MODIFIED_COLOR.opacity(0.1),
             version_control_renamed: MODIFIED_COLOR,
             version_control_conflict: orange().dark().step_12(),
-            version_control_conflict_background: orange().dark().step_12().opacity(0.1),
             version_control_ignored: gray().dark().step_12(),
         }
     }

crates/theme/src/fallback_themes.rs 🔗

@@ -190,14 +190,10 @@ pub(crate) fn zed_default_dark() -> Theme {
                 editor_foreground: hsla(218. / 360., 14. / 100., 71. / 100., 1.),
                 link_text_hover: blue,
                 version_control_added: ADDED_COLOR,
-                version_control_added_background: ADDED_COLOR.opacity(0.1),
                 version_control_deleted: REMOVED_COLOR,
-                version_control_deleted_background: REMOVED_COLOR.opacity(0.1),
                 version_control_modified: MODIFIED_COLOR,
-                version_control_modified_background: MODIFIED_COLOR.opacity(0.1),
                 version_control_renamed: MODIFIED_COLOR,
                 version_control_conflict: crate::orange().light().step_12(),
-                version_control_conflict_background: crate::orange().light().step_12().opacity(0.1),
                 version_control_ignored: crate::gray().light().step_12(),
             },
             status: StatusColors {

crates/theme/src/schema.rs 🔗

@@ -557,26 +557,14 @@ pub struct ThemeColorsContent {
     #[serde(rename = "version_control.added")]
     pub version_control_added: Option<String>,
 
-    /// Added version control background color.
-    #[serde(rename = "version_control.added_background")]
-    pub version_control_added_background: Option<String>,
-
     /// Deleted version control color.
     #[serde(rename = "version_control.deleted")]
     pub version_control_deleted: Option<String>,
 
-    /// Deleted version control background color.
-    #[serde(rename = "version_control.deleted_background")]
-    pub version_control_deleted_background: Option<String>,
-
     /// Modified version control color.
     #[serde(rename = "version_control.modified")]
     pub version_control_modified: Option<String>,
 
-    /// Modified version control background color.
-    #[serde(rename = "version_control.modified_background")]
-    pub version_control_modified_background: Option<String>,
-
     /// Renamed version control color.
     #[serde(rename = "version_control.renamed")]
     pub version_control_renamed: Option<String>,
@@ -585,10 +573,6 @@ pub struct ThemeColorsContent {
     #[serde(rename = "version_control.conflict")]
     pub version_control_conflict: Option<String>,
 
-    /// Conflict version control background color.
-    #[serde(rename = "version_control.conflict_background")]
-    pub version_control_conflict_background: Option<String>,
-
     /// Ignored version control color.
     #[serde(rename = "version_control.ignored")]
     pub version_control_ignored: Option<String>,
@@ -1000,26 +984,14 @@ impl ThemeColorsContent {
                 .version_control_added
                 .as_ref()
                 .and_then(|color| try_parse_color(color).ok()),
-            version_control_added_background: self
-                .version_control_added_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
             version_control_deleted: self
                 .version_control_deleted
                 .as_ref()
                 .and_then(|color| try_parse_color(color).ok()),
-            version_control_deleted_background: self
-                .version_control_deleted_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
             version_control_modified: self
                 .version_control_modified
                 .as_ref()
                 .and_then(|color| try_parse_color(color).ok()),
-            version_control_modified_background: self
-                .version_control_modified_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
             version_control_renamed: self
                 .version_control_renamed
                 .as_ref()
@@ -1028,10 +1000,6 @@ impl ThemeColorsContent {
                 .version_control_conflict
                 .as_ref()
                 .and_then(|color| try_parse_color(color).ok()),
-            version_control_conflict_background: self
-                .version_control_conflict_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
             version_control_ignored: self
                 .version_control_ignored
                 .as_ref()

crates/theme/src/styles/colors.rs 🔗

@@ -246,22 +246,14 @@ pub struct ThemeColors {
 
     /// Represents an added entry or hunk in vcs, like git.
     pub version_control_added: Hsla,
-    /// Represents the line background of an added entry or hunk in vcs, like git.
-    pub version_control_added_background: Hsla,
     /// Represents a deleted entry in version control systems.
     pub version_control_deleted: Hsla,
-    /// Represents the background color for deleted entries in version control systems.
-    pub version_control_deleted_background: Hsla,
     /// Represents a modified entry in version control systems.
     pub version_control_modified: Hsla,
-    /// Represents the background color for modified entries in version control systems.
-    pub version_control_modified_background: Hsla,
     /// Represents a renamed entry in version control systems.
     pub version_control_renamed: Hsla,
     /// Represents a conflicting entry in version control systems.
     pub version_control_conflict: Hsla,
-    /// Represents the background color for conflicting entries in version control systems.
-    pub version_control_conflict_background: Hsla,
     /// Represents an ignored entry in version control systems.
     pub version_control_ignored: Hsla,
 }
@@ -366,14 +358,10 @@ pub enum ThemeColorField {
     TerminalAnsiDimWhite,
     LinkTextHover,
     VersionControlAdded,
-    VersionControlAddedBackground,
     VersionControlDeleted,
-    VersionControlDeletedBackground,
     VersionControlModified,
-    VersionControlModifiedBackground,
     VersionControlRenamed,
     VersionControlConflict,
-    VersionControlConflictBackground,
     VersionControlIgnored,
 }
 
@@ -485,20 +473,10 @@ impl ThemeColors {
             ThemeColorField::TerminalAnsiDimWhite => self.terminal_ansi_dim_white,
             ThemeColorField::LinkTextHover => self.link_text_hover,
             ThemeColorField::VersionControlAdded => self.version_control_added,
-            ThemeColorField::VersionControlAddedBackground => self.version_control_added_background,
             ThemeColorField::VersionControlDeleted => self.version_control_deleted,
-            ThemeColorField::VersionControlDeletedBackground => {
-                self.version_control_deleted_background
-            }
             ThemeColorField::VersionControlModified => self.version_control_modified,
-            ThemeColorField::VersionControlModifiedBackground => {
-                self.version_control_modified_background
-            }
             ThemeColorField::VersionControlRenamed => self.version_control_renamed,
             ThemeColorField::VersionControlConflict => self.version_control_conflict,
-            ThemeColorField::VersionControlConflictBackground => {
-                self.version_control_conflict_background
-            }
             ThemeColorField::VersionControlIgnored => self.version_control_ignored,
         }
     }

crates/ui/src/components/button/button.rs 🔗

@@ -6,9 +6,7 @@ use crate::{
     prelude::*, Color, DynamicSpacing, ElevationIndex, IconPosition, KeyBinding,
     KeybindingPosition, TintColor,
 };
-use crate::{
-    ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize, Label, LineHeightStyle,
-};
+use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize, Label};
 
 use super::button_icon::ButtonIcon;
 
@@ -448,7 +446,6 @@ impl RenderOnce for Button {
                                 .color(label_color)
                                 .size(self.label_size.unwrap_or_default())
                                 .when_some(self.alpha, |this, alpha| this.alpha(alpha))
-                                .line_height_style(LineHeightStyle::UiLabel)
                                 .when(self.truncate, |this| this.truncate()),
                         )
                         .children(self.key_binding),

crates/ui/src/components/toggle.rs 🔗

@@ -1,6 +1,5 @@
 use gpui::{
-    div, hsla, prelude::*, AnyElement, AnyView, CursorStyle, ElementId, Hsla, IntoElement, Styled,
-    Window,
+    div, hsla, prelude::*, AnyElement, AnyView, ElementId, Hsla, IntoElement, Styled, Window,
 };
 use std::sync::Arc;
 
@@ -141,14 +140,14 @@ impl Checkbox {
 
         match self.style.clone() {
             ToggleStyle::Ghost => cx.theme().colors().border,
-            ToggleStyle::ElevationBased(elevation) => elevation.on_elevation_bg(cx),
+            ToggleStyle::ElevationBased(_) => cx.theme().colors().border,
             ToggleStyle::Custom(color) => color.opacity(0.3),
         }
     }
 
     /// container size
-    pub fn container_size(cx: &App) -> Rems {
-        DynamicSpacing::Base20.rems(cx)
+    pub fn container_size() -> Pixels {
+        px(20.0)
     }
 }
 
@@ -157,21 +156,21 @@ impl RenderOnce for Checkbox {
         let group_id = format!("checkbox_group_{:?}", self.id);
         let color = if self.disabled {
             Color::Disabled
-        } else if self.placeholder {
-            Color::Placeholder
         } else {
             Color::Selected
         };
         let icon = match self.toggle_state {
-            ToggleState::Selected => Some(if self.placeholder {
-                Icon::new(IconName::Circle)
-                    .size(IconSize::XSmall)
-                    .color(color)
-            } else {
-                Icon::new(IconName::Check)
-                    .size(IconSize::Small)
-                    .color(color)
-            }),
+            ToggleState::Selected => {
+                if self.placeholder {
+                    None
+                } else {
+                    Some(
+                        Icon::new(IconName::Check)
+                            .size(IconSize::Small)
+                            .color(color),
+                    )
+                }
+            }
             ToggleState::Indeterminate => {
                 Some(Icon::new(IconName::Dash).size(IconSize::Small).color(color))
             }
@@ -180,8 +179,9 @@ impl RenderOnce for Checkbox {
 
         let bg_color = self.bg_color(cx);
         let border_color = self.border_color(cx);
+        let hover_border_color = border_color.alpha(0.7);
 
-        let size = Self::container_size(cx);
+        let size = Self::container_size();
 
         let checkbox = h_flex()
             .id(self.id.clone())
@@ -195,22 +195,27 @@ impl RenderOnce for Checkbox {
                     .flex_none()
                     .justify_center()
                     .items_center()
-                    .m(DynamicSpacing::Base04.px(cx))
-                    .size(DynamicSpacing::Base16.rems(cx))
+                    .m_1()
+                    .size_4()
                     .rounded_xs()
                     .bg(bg_color)
                     .border_1()
                     .border_color(border_color)
-                    .when(self.disabled, |this| {
-                        this.cursor(CursorStyle::OperationNotAllowed)
-                    })
+                    .when(self.disabled, |this| this.cursor_not_allowed())
                     .when(self.disabled, |this| {
                         this.bg(cx.theme().colors().element_disabled.opacity(0.6))
                     })
                     .when(!self.disabled, |this| {
-                        this.group_hover(group_id.clone(), |el| {
-                            el.bg(cx.theme().colors().element_hover)
-                        })
+                        this.group_hover(group_id.clone(), |el| el.border_color(hover_border_color))
+                    })
+                    .when(self.placeholder, |this| {
+                        this.child(
+                            div()
+                                .flex_none()
+                                .rounded_full()
+                                .bg(color.color(cx).alpha(0.5))
+                                .size(px(4.)),
+                        )
                     })
                     .children(icon),
             );
@@ -522,6 +527,12 @@ impl ComponentPreview for Checkbox {
                             Checkbox::new("checkbox_unselected", ToggleState::Unselected)
                                 .into_any_element(),
                         ),
+                        single_example(
+                            "Placeholder",
+                            Checkbox::new("checkbox_indeterminate", ToggleState::Selected)
+                                .placeholder(true)
+                                .into_any_element(),
+                        ),
                         single_example(
                             "Indeterminate",
                             Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate)