diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 0d0a139a8a..912a705d16 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -9,7 +9,9 @@ using osu.Framework.Bindables;
 using osu.Framework.Caching;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
 using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterface;
 using osu.Game.Rulesets.Edit;
 using osu.Game.Rulesets.Edit.Tools;
 using osu.Game.Rulesets.Mods;
@@ -17,6 +19,7 @@ using osu.Game.Rulesets.Objects;
 using osu.Game.Rulesets.Objects.Drawables;
 using osu.Game.Rulesets.Osu.Objects;
 using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Edit.Components.TernaryButtons;
 using osu.Game.Screens.Edit.Compose.Components;
 using osuTK;
 
@@ -39,11 +42,11 @@ namespace osu.Game.Rulesets.Osu.Edit
             new SpinnerCompositionTool()
         };
 
-        private readonly BindableBool distanceSnapToggle = new BindableBool(true) { Description = "Distance Snap" };
+        private readonly Bindable<TernaryState> distanceSnapToggle = new Bindable<TernaryState>();
 
-        protected override IEnumerable<Bindable<bool>> Toggles => base.Toggles.Concat(new[]
+        protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
         {
-            distanceSnapToggle
+            new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
         });
 
         private BindableList<HitObject> selectedHitObjects;
@@ -156,7 +159,7 @@ namespace osu.Game.Rulesets.Osu.Edit
             distanceSnapGridCache.Invalidate();
             distanceSnapGrid = null;
 
-            if (!distanceSnapToggle.Value)
+            if (distanceSnapToggle.Value != TernaryState.True)
                 return;
 
             switch (BlueprintContainer.CurrentTool)
diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs
index f6b0107bd2..8b1f5a366a 100644
--- a/osu.Game/Audio/HitSampleInfo.cs
+++ b/osu.Game/Audio/HitSampleInfo.cs
@@ -17,6 +17,11 @@ namespace osu.Game.Audio
         public const string HIT_NORMAL = @"hitnormal";
         public const string HIT_CLAP = @"hitclap";
 
+        /// <summary>
+        /// All valid sample addition constants.
+        /// </summary>
+        public static IEnumerable<string> AllAdditions => new[] { HIT_WHISTLE, HIT_CLAP, HIT_FINISH };
+
         /// <summary>
         /// The bank to load the sample from.
         /// </summary>
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index abd8374d98..b9b7c1ef54 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -6,7 +6,6 @@ using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.Linq;
 using osu.Framework.Allocation;
-using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
 using osu.Framework.Input;
@@ -14,7 +13,6 @@ using osu.Framework.Input.Events;
 using osu.Framework.Logging;
 using osu.Game.Beatmaps;
 using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Overlays.Settings;
 using osu.Game.Rulesets.Configuration;
 using osu.Game.Rulesets.Edit.Tools;
 using osu.Game.Rulesets.Mods;
@@ -24,6 +22,7 @@ using osu.Game.Rulesets.UI;
 using osu.Game.Rulesets.UI.Scrolling;
 using osu.Game.Screens.Edit;
 using osu.Game.Screens.Edit.Components.RadioButtons;
+using osu.Game.Screens.Edit.Components.TernaryButtons;
 using osu.Game.Screens.Edit.Compose;
 using osu.Game.Screens.Edit.Compose.Components;
 using osuTK;
@@ -63,7 +62,7 @@ namespace osu.Game.Rulesets.Edit
 
         private RadioButtonCollection toolboxCollection;
 
-        private ToolboxGroup togglesCollection;
+        private FillFlowContainer togglesCollection;
 
         protected HitObjectComposer(Ruleset ruleset)
         {
@@ -121,14 +120,19 @@ namespace osu.Game.Rulesets.Edit
                     Spacing = new Vector2(10),
                     Children = new Drawable[]
                     {
-                        new ToolboxGroup("toolbox") { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } },
-                        togglesCollection = new ToolboxGroup("toggles")
+                        new ToolboxGroup("toolbox (1-9)")
                         {
-                            ChildrenEnumerable = Toggles.Select(b => new SettingsCheckbox
+                            Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X }
+                        },
+                        new ToolboxGroup("toggles (Q~P)")
+                        {
+                            Child = togglesCollection = new FillFlowContainer
                             {
-                                Bindable = b,
-                                LabelText = b?.Description ?? "unknown"
-                            })
+                                RelativeSizeAxes = Axes.X,
+                                AutoSizeAxes = Axes.Y,
+                                Direction = FillDirection.Vertical,
+                                Spacing = new Vector2(0, 5),
+                            },
                         }
                     }
                 },
@@ -139,6 +143,9 @@ namespace osu.Game.Rulesets.Edit
                                       .Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon))
                                       .ToList();
 
+            TernaryStates = CreateTernaryButtons().ToArray();
+            togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b)));
+
             setSelectTool();
 
             EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged;
@@ -167,10 +174,14 @@ namespace osu.Game.Rulesets.Edit
         protected abstract IReadOnlyList<HitObjectCompositionTool> CompositionTools { get; }
 
         /// <summary>
-        /// A collection of toggles which will be displayed to the user.
-        /// The display name will be decided by <see cref="Bindable{T}.Description"/>.
+        /// A collection of states which will be displayed to the user in the toolbox.
         /// </summary>
-        protected virtual IEnumerable<Bindable<bool>> Toggles => BlueprintContainer.Toggles;
+        public TernaryButton[] TernaryStates { get; private set; }
+
+        /// <summary>
+        /// Create all ternary states required to be displayed to the user.
+        /// </summary>
+        protected virtual IEnumerable<TernaryButton> CreateTernaryButtons() => BlueprintContainer.TernaryStates;
 
         /// <summary>
         /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
@@ -215,9 +226,9 @@ namespace osu.Game.Rulesets.Edit
             {
                 var item = togglesCollection.ElementAtOrDefault(rightIndex);
 
-                if (item is SettingsCheckbox checkbox)
+                if (item is DrawableTernaryButton button)
                 {
-                    checkbox.Bindable.Value = !checkbox.Bindable.Value;
+                    button.Button.Toggle();
                     return true;
                 }
             }
diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs
new file mode 100644
index 0000000000..c72fff5c91
--- /dev/null
+++ b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs
@@ -0,0 +1,112 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Screens.Edit.Components.TernaryButtons
+{
+    internal class DrawableTernaryButton : TriangleButton
+    {
+        private Color4 defaultBackgroundColour;
+        private Color4 defaultBubbleColour;
+        private Color4 selectedBackgroundColour;
+        private Color4 selectedBubbleColour;
+
+        private Drawable icon;
+
+        public readonly TernaryButton Button;
+
+        public DrawableTernaryButton(TernaryButton button)
+        {
+            Button = button;
+
+            Text = button.Description;
+
+            RelativeSizeAxes = Axes.X;
+        }
+
+        [BackgroundDependencyLoader]
+        private void load(OsuColour colours)
+        {
+            defaultBackgroundColour = colours.Gray3;
+            defaultBubbleColour = defaultBackgroundColour.Darken(0.5f);
+            selectedBackgroundColour = colours.BlueDark;
+            selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f);
+
+            Triangles.Alpha = 0;
+
+            Content.EdgeEffect = new EdgeEffectParameters
+            {
+                Type = EdgeEffectType.Shadow,
+                Radius = 2,
+                Offset = new Vector2(0, 1),
+                Colour = Color4.Black.Opacity(0.5f)
+            };
+
+            Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
+            {
+                b.Blending = BlendingParameters.Additive;
+                b.Anchor = Anchor.CentreLeft;
+                b.Origin = Anchor.CentreLeft;
+                b.Size = new Vector2(20);
+                b.X = 10;
+            }));
+        }
+
+        protected override void LoadComplete()
+        {
+            base.LoadComplete();
+
+            Button.Bindable.BindValueChanged(selected => updateSelectionState(), true);
+
+            Action = onAction;
+        }
+
+        private void onAction()
+        {
+            Button.Toggle();
+        }
+
+        private void updateSelectionState()
+        {
+            if (!IsLoaded)
+                return;
+
+            switch (Button.Bindable.Value)
+            {
+                case TernaryState.Indeterminate:
+                    icon.Colour = selectedBubbleColour.Darken(0.5f);
+                    BackgroundColour = selectedBackgroundColour.Darken(0.5f);
+                    break;
+
+                case TernaryState.False:
+                    icon.Colour = defaultBubbleColour;
+                    BackgroundColour = defaultBackgroundColour;
+                    break;
+
+                case TernaryState.True:
+                    icon.Colour = selectedBubbleColour;
+                    BackgroundColour = selectedBackgroundColour;
+                    break;
+            }
+        }
+
+        protected override SpriteText CreateText() => new OsuSpriteText
+        {
+            Depth = -1,
+            Origin = Anchor.CentreLeft,
+            Anchor = Anchor.CentreLeft,
+            X = 40f
+        };
+    }
+}
diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/TernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/TernaryButton.cs
new file mode 100644
index 0000000000..7f64695bde
--- /dev/null
+++ b/osu.Game/Screens/Edit/Components/TernaryButtons/TernaryButton.cs
@@ -0,0 +1,44 @@
+// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Screens.Edit.Components.TernaryButtons
+{
+    public class TernaryButton
+    {
+        public readonly Bindable<TernaryState> Bindable;
+
+        public readonly string Description;
+
+        /// <summary>
+        /// A function which creates a drawable icon to represent this item. If null, a sane default should be used.
+        /// </summary>
+        public readonly Func<Drawable> CreateIcon;
+
+        public TernaryButton(Bindable<TernaryState> bindable, string description, Func<Drawable> createIcon = null)
+        {
+            Bindable = bindable;
+            Description = description;
+            CreateIcon = createIcon;
+        }
+
+        public void Toggle()
+        {
+            switch (Bindable.Value)
+            {
+                case TernaryState.False:
+                case TernaryState.Indeterminate:
+                    Bindable.Value = TernaryState.True;
+                    break;
+
+                case TernaryState.True:
+                    Bindable.Value = TernaryState.False;
+                    break;
+            }
+        }
+    }
+}
diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
index 6f66c1bd6f..88c3170c34 100644
--- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
@@ -7,12 +7,15 @@ using osu.Framework.Allocation;
 using osu.Framework.Bindables;
 using osu.Framework.Graphics;
 using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
 using osu.Framework.Input;
+using osu.Game.Graphics.UserInterface;
 using osu.Game.Rulesets.Edit;
 using osu.Game.Rulesets.Edit.Tools;
 using osu.Game.Rulesets.Objects;
 using osu.Game.Rulesets.Objects.Drawables;
 using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Screens.Edit.Components.TernaryButtons;
 using osuTK;
 
 namespace osu.Game.Screens.Edit.Compose.Components
@@ -48,6 +51,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
         [BackgroundDependencyLoader]
         private void load()
         {
+            TernaryStates = CreateTernaryButtons().ToArray();
+
             AddInternal(placementBlueprintContainer);
         }
 
@@ -57,36 +62,34 @@ namespace osu.Game.Screens.Edit.Compose.Components
 
             inputManager = GetContainingInputManager();
 
-            Beatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateTogglesFromSelection();
-
-            // the updated object may be in the selection
-            Beatmap.HitObjectUpdated += _ => updateTogglesFromSelection();
+            // updates to selected are handled for us by SelectionHandler.
+            NewCombo.BindTo(SelectionHandler.SelectionNewComboState);
 
+            // we are responsible for current placement blueprint updated based on state changes.
             NewCombo.ValueChanged += combo =>
             {
-                if (Beatmap.SelectedHitObjects.Count > 0)
-                {
-                    SelectionHandler.SetNewCombo(combo.NewValue);
-                }
-                else if (currentPlacement != null)
-                {
-                    // update placement object from toggle
-                    if (currentPlacement.HitObject is IHasComboInformation c)
-                        c.NewCombo = combo.NewValue;
-                }
+                if (currentPlacement == null) return;
+
+                if (currentPlacement.HitObject is IHasComboInformation c)
+                    c.NewCombo = combo.NewValue == TernaryState.True;
             };
         }
 
-        private void updateTogglesFromSelection() =>
-            NewCombo.Value = Beatmap.SelectedHitObjects.OfType<IHasComboInformation>().All(c => c.NewCombo);
+        public readonly Bindable<TernaryState> NewCombo = new Bindable<TernaryState> { Description = "New Combo" };
 
-        public readonly Bindable<bool> NewCombo = new Bindable<bool> { Description = "New Combo" };
+        /// <summary>
+        /// A collection of states which will be displayed to the user in the toolbox.
+        /// </summary>
+        public TernaryButton[] TernaryStates { get; private set; }
 
-        public virtual IEnumerable<Bindable<bool>> Toggles => new[]
+        /// <summary>
+        /// Create all ternary states required to be displayed to the user.
+        /// </summary>
+        protected virtual IEnumerable<TernaryButton> CreateTernaryButtons()
         {
             //TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects.
-            NewCombo
-        };
+            yield return new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = FontAwesome.Regular.DotCircle });
+        }
 
         #region Placement
 
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
index a316f34ad0..6ca85fe026 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
@@ -316,19 +316,22 @@ namespace osu.Game.Screens.Edit.Compose.Components
 
         #region Selection State
 
-        private readonly Bindable<TernaryState> selectionNewComboState = new Bindable<TernaryState>();
+        /// <summary>
+        /// The state of "new combo" for all selected hitobjects.
+        /// </summary>
+        public readonly Bindable<TernaryState> SelectionNewComboState = new Bindable<TernaryState>();
 
-        private readonly Dictionary<string, Bindable<TernaryState>> selectionSampleStates = new Dictionary<string, Bindable<TernaryState>>();
+        /// <summary>
+        /// The state of each sample type for all selected hitobjects. Keys match with <see cref="HitSampleInfo"/> constant specifications.
+        /// </summary>
+        public readonly Dictionary<string, Bindable<TernaryState>> SelectionSampleStates = new Dictionary<string, Bindable<TernaryState>>();
 
         /// <summary>
         /// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions)
         /// </summary>
         private void createStateBindables()
         {
-            // hit samples
-            var sampleTypes = new[] { HitSampleInfo.HIT_WHISTLE, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_FINISH };
-
-            foreach (var sampleName in sampleTypes)
+            foreach (var sampleName in HitSampleInfo.AllAdditions)
             {
                 var bindable = new Bindable<TernaryState>
                 {
@@ -349,11 +352,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
                     }
                 };
 
-                selectionSampleStates[sampleName] = bindable;
+                SelectionSampleStates[sampleName] = bindable;
             }
 
             // new combo
-            selectionNewComboState.ValueChanged += state =>
+            SelectionNewComboState.ValueChanged += state =>
             {
                 switch (state.NewValue)
                 {
@@ -377,9 +380,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
         /// </summary>
         protected virtual void UpdateTernaryStates()
         {
-            selectionNewComboState.Value = GetStateFromSelection(SelectedHitObjects.OfType<IHasComboInformation>(), h => h.NewCombo);
+            SelectionNewComboState.Value = GetStateFromSelection(SelectedHitObjects.OfType<IHasComboInformation>(), h => h.NewCombo);
 
-            foreach (var (sampleName, bindable) in selectionSampleStates)
+            foreach (var (sampleName, bindable) in SelectionSampleStates)
             {
                 bindable.Value = GetStateFromSelection(SelectedHitObjects, h => h.Samples.Any(s => s.Name == sampleName));
             }
@@ -413,7 +416,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
 
                 if (selectedBlueprints.All(b => b.HitObject is IHasComboInformation))
                 {
-                    items.Add(new TernaryStateMenuItem("New combo") { State = { BindTarget = selectionNewComboState } });
+                    items.Add(new TernaryStateMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } });
                 }
 
                 if (selectedBlueprints.Count == 1)
@@ -423,7 +426,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
                 {
                     new OsuMenuItem("Sound")
                     {
-                        Items = selectionSampleStates.Select(kvp =>
+                        Items = SelectionSampleStates.Select(kvp =>
                             new TernaryStateMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray()
                     },
                     new OsuMenuItem("Delete", MenuItemType.Destructive, deleteSelected),