Toolbar
Displays frequently used actions or tools in a compact, easily accessible bar.
<script lang="ts">
import { Separator, Toolbar } from "bits-ui";
import Sparkle from "phosphor-svelte/lib/Sparkle";
import TextAlignCenter from "phosphor-svelte/lib/TextAlignCenter";
import TextAlignLeft from "phosphor-svelte/lib/TextAlignLeft";
import TextAlignRight from "phosphor-svelte/lib/TextAlignRight";
import TextB from "phosphor-svelte/lib/TextB";
import TextItalic from "phosphor-svelte/lib/TextItalic";
import TextStrikethrough from "phosphor-svelte/lib/TextStrikethrough";
let text = $state(["bold"]);
let align = $state("");
</script>
<Toolbar.Root
class="rounded-10px border-border bg-background-alt shadow-mini flex h-12 min-w-max items-center justify-center border px-[4px] py-1"
>
<Toolbar.Group
bind:value={text}
type="multiple"
class="flex items-center gap-x-0.5"
>
<Toolbar.GroupItem
aria-label="toggle bold"
value="bold"
class="rounded-9px bg-background-alt text-foreground/60 hover:bg-muted active:scale-98 active:bg-dark-10 data-[state=on]:bg-muted data-[state=on]:text-foreground/80 active:data-[state=on]:bg-dark-10 inline-flex size-10 items-center justify-center transition-all"
>
<TextB class="size-6" />
</Toolbar.GroupItem>
<Toolbar.GroupItem
aria-label="toggle italic"
value="italic"
class="rounded-9px bg-background-alt text-foreground/60 hover:bg-muted active:scale-98 active:bg-dark-10 data-[state=on]:bg-muted data-[state=on]:text-foreground/80 active:data-[state=on]:bg-dark-10 inline-flex size-10 items-center justify-center transition-all"
>
<TextItalic class="size-6" />
</Toolbar.GroupItem>
<Toolbar.GroupItem
aria-label="toggle strikethrough"
value="strikethrough"
class="rounded-9px bg-background-alt text-foreground/60 hover:bg-muted active:scale-98 active:bg-dark-10 data-[state=on]:bg-muted data-[state=on]:text-foreground/80 active:data-[state=on]:bg-dark-10 inline-flex size-10 items-center justify-center transition-all"
>
<TextStrikethrough class="size-6" />
</Toolbar.GroupItem>
</Toolbar.Group>
<Separator.Root class="bg-dark-10 -my-1 mx-1 w-[1px] self-stretch" />
<Toolbar.Group
bind:value={align}
type="single"
class="flex items-center gap-x-0.5"
>
<Toolbar.GroupItem
aria-label="align left"
value="left"
class="rounded-9px bg-background-alt text-foreground/60 hover:bg-muted active:scale-98 active:bg-dark-10 data-[state=on]:bg-muted data-[state=on]:text-foreground/80 active:data-[state=on]:bg-dark-10 inline-flex size-10 items-center justify-center transition-all"
>
<TextAlignLeft class="size-6" />
</Toolbar.GroupItem>
<Toolbar.GroupItem
aria-label="align center"
value="center"
class="rounded-9px bg-background-alt text-foreground/60 hover:bg-muted active:scale-98 active:bg-dark-10 data-[state=on]:bg-muted data-[state=on]:text-foreground/80 active:data-[state=on]:bg-dark-10 inline-flex size-10 items-center justify-center transition-all"
>
<TextAlignCenter class="size-6" />
</Toolbar.GroupItem>
<Toolbar.GroupItem
aria-label="align right"
value="right"
class="rounded-9px bg-background-alt text-foreground/60 hover:bg-muted active:scale-98 active:bg-dark-10 data-[state=on]:bg-muted data-[state=on]:text-foreground/80 active:data-[state=on]:bg-dark-10 inline-flex size-10 items-center justify-center transition-all"
>
<TextAlignRight class="size-6" />
</Toolbar.GroupItem>
</Toolbar.Group>
<Separator.Root class="bg-dark-10 -my-1 mx-1 w-[1px] self-stretch" />
<div class="flex items-center">
<Toolbar.Button
class="rounded-9px text-foreground/80 hover:bg-muted active:scale-98 active:bg-dark-10 inline-flex items-center justify-center px-3 py-2 text-sm font-medium transition-all"
>
<Sparkle class="mr-2 size-6" />
<span> Ask AI </span>
</Toolbar.Button>
</div>
</Toolbar.Root>
import typography from "@tailwindcss/typography";
import animate from "tailwindcss-animate";
import { fontFamily } from "tailwindcss/defaultTheme";
import plugin from "tailwindcss/plugin";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: "class",
content: ["./src/**/*.{html,js,svelte,ts}"],
theme: {
container: {
center: true,
screens: {
"2xl": "1440px",
},
},
extend: {
colors: {
border: {
DEFAULT: "hsl(var(--border-card))",
input: "hsl(var(--border-input))",
"input-hover": "hsl(var(--border-input-hover))",
},
background: {
DEFAULT: "hsl(var(--background) / <alpha-value>)",
alt: "hsl(var(--background-alt) / <alpha-value>)",
},
foreground: {
DEFAULT: "hsl(var(--foreground) / <alpha-value>)",
alt: "hsl(var(--foreground-alt) / <alpha-value>)",
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground))",
},
dark: {
DEFAULT: "hsl(var(--dark) / <alpha-value>)",
4: "hsl(var(--dark-04))",
10: "hsl(var(--dark-10))",
40: "hsl(var(--dark-40))",
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
},
tertiary: {
DEFAULT: "hsl(var(--tertiary) / <alpha-value>)",
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
},
contrast: {
DEFAULT: "hsl(var(--contrast) / <alpha-value>)",
},
},
fontFamily: {
sans: ["Inter", ...fontFamily.sans],
mono: ["Source Code Pro", ...fontFamily.mono],
alt: ["Courier", ...fontFamily.sans],
},
fontSize: {
xxs: "10px",
},
borderWidth: {
6: "6px",
},
borderRadius: {
card: "16px",
"card-lg": "20px",
"card-sm": "10px",
input: "9px",
button: "5px",
"5px": "5px",
"9px": "9px",
"10px": "10px",
"15px": "15px",
},
height: {
input: "3rem",
"input-sm": "2.5rem",
},
boxShadow: {
mini: "var(--shadow-mini)",
"mini-inset": "var(--shadow-mini-inset)",
popover: "var(--shadow-popover)",
kbd: "var(--shadow-kbd)",
btn: "var(--shadow-btn)",
card: "var(--shadow-card)",
"date-field-focus": "var(--shadow-date-field-focus)",
},
opacity: {
8: "0.08",
},
scale: {
80: ".80",
98: ".98",
99: ".99",
},
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--bits-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--bits-accordion-content-height)" },
to: { height: "0" },
},
"caret-blink": {
"0%,70%,100%": { opacity: "1" },
"20%,50%": { opacity: "0" },
},
enterFromRight: {
from: { opacity: "0", transform: "translateX(200px)" },
to: { opacity: "1", transform: "translateX(0)" },
},
enterFromLeft: {
from: { opacity: "0", transform: "translateX(-200px)" },
to: { opacity: "1", transform: "translateX(0)" },
},
exitToRight: {
from: { opacity: "1", transform: "translateX(0)" },
to: { opacity: "0", transform: "translateX(200px)" },
},
exitToLeft: {
from: { opacity: "1", transform: "translateX(0)" },
to: { opacity: "0", transform: "translateX(-200px)" },
},
scaleIn: {
from: { opacity: "0", transform: "rotateX(-10deg) scale(0.9)" },
to: { opacity: "1", transform: "rotateX(0deg) scale(1)" },
},
scaleOut: {
from: { opacity: "1", transform: "rotateX(0deg) scale(1)" },
to: { opacity: "0", transform: "rotateX(-10deg) scale(0.95)" },
},
fadeIn: {
from: { opacity: "0" },
to: { opacity: "1" },
},
fadeOut: {
from: { opacity: "1" },
to: { opacity: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"caret-blink": "caret-blink 1.25s ease-out infinite",
scaleIn: "scaleIn 200ms ease",
scaleOut: "scaleOut 150ms ease",
fadeIn: "fadeIn 200ms ease",
fadeOut: "fadeOut 150ms ease",
enterFromLeft: "enterFromLeft 200ms ease",
enterFromRight: "enterFromRight 200ms ease",
exitToLeft: "exitToLeft 200ms ease",
exitToRight: "exitToRight 200ms ease",
},
},
plugins: [
typography,
animate,
plugin(({ matchUtilities }) => {
matchUtilities({
perspective: (value) => ({
perspective: value,
}),
});
}),
],
};
@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Colors */
--background: 0 0% 100%;
--background-alt: 0 0% 100%;
--foreground: 0 0% 9%;
--foreground-alt: 0 0% 32%;
--muted: 240 5% 96%;
--muted-foreground: 0 0% 9% / 0.4;
--border: 240 6% 10%;
--border-input: 240 6% 10% / 0.17;
--border-input-hover: 240 6% 10% / 0.4;
--border-card: 240 6% 10% / 0.1;
--dark: 240 6% 10%;
--dark-10: 240 6% 10% / 0.1;
--dark-40: 240 6% 10% / 0.4;
--dark-04: 240 6% 10% / 0.04;
--accent: 204 94% 94%;
--accent-foreground: 204 80% 16%;
--destructive: 347 77% 50%;
/* black */
--constrast: 0 0% 0%;
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.04) inset;
--shadow-popover: 0px 7px 12px 3px hsla(var(--dark-10));
--shadow-kbd: 0px 2px 0px 0px rgba(0, 0, 0, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.03);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(24, 24, 27, 0.17);
}
.dark {
/* Colors */
--background: 0 0% 5%;
--background-alt: 0 0% 8%;
--foreground: 0 0% 95%;
--foreground-alt: 0 0% 70%;
--muted: 240 4% 16%;
--muted-foreground: 0 0% 100% / 0.4;
--border: 0 0% 96%;
--border-input: 0 0% 96% / 0.17;
--border-input-hover: 0 0% 96% / 0.4;
--border-card: 0 0% 96% / 0.1;
--dark: 0 0% 96%;
--dark-40: 0 0% 96% / 0.4;
--dark-10: 0 0% 96% / 0.1;
--dark-04: 0 0% 96% / 0.04;
--accent: 204 90 90%;
--accent-foreground: 204 94% 94%;
--destructive: 350 89% 60%;
/* white */
--constrast: 0 0% 100%;
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.3);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.5) inset;
--shadow-popover: 0px 7px 12px 3px hsla(0deg 0% 0% / 30%);
--shadow-kbd: 0px 2px 0px 0px rgba(255, 255, 255, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.2);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.4);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(244, 244, 245, 0.1);
}
}
@layer base {
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
/* Mobile tap highlight */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-tap-highlight-color */
html {
-webkit-tap-highlight-color: rgba(128, 128, 128, 0.5);
}
::selection {
background: #fdffa4;
color: black;
}
/* === Scrollbars === */
::-webkit-scrollbar {
@apply w-2;
@apply h-2;
}
::-webkit-scrollbar-track {
@apply !bg-transparent;
}
::-webkit-scrollbar-thumb {
@apply rounded-card-lg !bg-dark-10;
}
::-webkit-scrollbar-corner {
background: rgba(0, 0, 0, 0);
}
/* Firefox */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
html {
scrollbar-color: var(--bg-muted);
}
.antialised {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer utilities {
.step {
counter-increment: step;
}
.step:before {
@apply absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 border-background bg-muted text-center -indent-px font-mono text-base font-medium;
@apply ml-[-50px] mt-[-4px];
content: counter(step);
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
}
.link {
@apply inline-flex items-center gap-1 rounded-sm font-medium underline underline-offset-4 hover:text-foreground/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}
}
Structure
<script lang="ts">
import { Toolbar } from "bits-ui";
</script>
<Toolbar.Root>
<Toolbar.Group>
<Toolbar.GroupItem />
</Toolbar.Group>
<Toolbar.Link />
<Toolbar.Button />
</Toolbar.Root>
Managing Value State
Bits UI offers several approaches to manage and synchronize the component's value state, catering to different levels of control and integration needs.
1. Two-Way Binding
For seamless state synchronization, use Svelte's bind:value
directive. This method automatically keeps your local state in sync with the component's internal state.
<script lang="ts">
import { Toolbar } from "bits-ui";
let myValue = $state("");
</script>
<button onclick={() => (myValue = "item-1")}> Press item 1 </button>
<Toolbar.Root>
<Toolbar.Group type="single" bind:value={myValue}>
<!-- ... -->
</Toolbar.Group>
</Toolbar.Root>
Key Benefits
- Simplifies state management
- Automatically updates
myValue
when the internal state changes (e.g., via clicking on an item) - Allows external control (e.g., toggling an item via a separate button)
2. Change Handler
For more granular control or to perform additional logic on state changes, use the onValueChange
prop. This approach is useful when you need to execute custom logic alongside state updates.
<script lang="ts">
import { Toolbar } from "bits-ui";
let myValue = $state("");
</script>
<Toolbar.Root>
<Toolbar.Group
type="single"
value={myValue}
onValueChange={(v) => {
myValue = v;
// additional logic here.
}}
>
<!-- ... -->
</Toolbar.Group>
</Toolbar.Root>
Use Cases
- Implementing custom behaviors on value change
- Integrating with external state management solutions
- Triggering side effects (e.g., logging, data fetching)
3. Fully Controlled
For complete control over the component's state, use a Function Binding to manage the value state externally.
<script lang="ts">
import { Toolbar } from "bits-ui";
let myValue = $state("");
</script>
<Toolbar.Root>
<Toolbar.Group type="single" bind:value={() => myValue, (newValue) => (myValue = newValue)}>
<!-- ... -->
</Toolbar.Group>
</Toolbar.Root>
When to Use
- Implementing complex logic
- Coordinating multiple UI elements
- Debugging state-related issues
Note
While powerful, fully controlled state should be used judiciously as it increases complexity and can cause unexpected behaviors if not handled carefully.
For more in-depth information on controlled components and advanced state management techniques, refer to our Controlled State documentation.
API Reference
The root component which contains the toolbar.
Property | Type | Description |
---|---|---|
loop | boolean | Whether or not the toolbar should loop when navigating. Default: true |
orientation | enum | The orientation of the toolbar. Default: horizontal |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-orientation | enum | The orientation of the component. |
data-toolbar-root | '' | Present on the root element. |
A button in the toolbar.
Property | Type | Description |
---|---|---|
disabled | boolean | Whether or not the button is disabled. Default: false |
ref $bindable | HTMLButtonElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-toolbar-button | '' | Present on the button element. |
A link in the toolbar.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLAnchorElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-toolbar-link | '' | Present on the link element. |
A group of toggle items in the toolbar.
Property | Type | Description |
---|---|---|
type required | enum | The type of toggle group. Default: 'single' |
value $bindable | union | The value of the toggle group. If the type is multiple, this will be an array of strings, otherwise it will be a string. Default: undefined |
onValueChange | function | A callback function called when the value changes. Default: undefined |
disabled | boolean | Whether or not the switch is disabled. Default: false |
ref $bindable | HTMLDivElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-toolbar-group | '' | Present on the group element. |
A toggle item in the toolbar toggle group.
Property | Type | Description |
---|---|---|
value required | string | The value of the toolbar toggle group item. When the toolbar toggle group item is selected, toolbar the toggle group's value will be set to this value if in single mode, or this value will be pushed to the toggle group's array value if in multiple mode. Default: undefined |
disabled | boolean | Whether or not the item is disabled. Default: false |
ref $bindable | HTMLButtonElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-state | enum | Whether the toolbar toggle item is in the on or off state. |
data-value | '' | The value of the toolbar toggle item. |
data-disabled | '' | Present when the toolbar toggle item is disabled. |
data-toolbar-item | '' | Present on the toolbar toggle item. |