diff --git a/docs-ui/src/components/ColorFamily/ColorFamily.tsx b/docs-ui/src/components/ColorFamily/ColorFamily.tsx
index 03cf8be848..93e44c43ed 100644
--- a/docs-ui/src/components/ColorFamily/ColorFamily.tsx
+++ b/docs-ui/src/components/ColorFamily/ColorFamily.tsx
@@ -75,6 +75,10 @@ export const ColorFamily = () => {
the previous one, automatically incrementing the background depth.
This creates clear visual hierarchy without manually picking colors.
+
+ Neutral 0 is the application background and should only be used once,
+ at the root of your app. All other surfaces build on top of it.
+
Each level can be interactive or{' '}
non-interactive. A Card, for example, can be flat
diff --git a/docs-ui/src/components/CommandPalette/CommandPalette.module.css b/docs-ui/src/components/CommandPalette/CommandPalette.module.css
new file mode 100644
index 0000000000..dbbdb72ceb
--- /dev/null
+++ b/docs-ui/src/components/CommandPalette/CommandPalette.module.css
@@ -0,0 +1,154 @@
+.overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 200;
+ background-color: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: flex-start;
+ justify-content: center;
+ padding-top: 15vh;
+
+ &[data-entering] {
+ animation: overlayFadeIn 200ms ease-out;
+ }
+
+ &[data-exiting] {
+ animation: overlayFadeOut 150ms ease-in;
+ }
+}
+
+@keyframes overlayFadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes overlayFadeOut {
+ from {
+ opacity: 1;
+ }
+ to {
+ opacity: 0;
+ }
+}
+
+.modal {
+ width: min(90vw, 500px);
+ max-height: min(70vh, 400px);
+ background-color: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);
+ display: flex;
+ flex-direction: column;
+ outline: none;
+ overflow: hidden;
+
+ &[data-entering] {
+ animation: modalScaleIn 200ms ease-out;
+ }
+
+ &[data-exiting] {
+ animation: modalScaleOut 150ms ease-in;
+ }
+}
+
+@keyframes modalScaleIn {
+ from {
+ opacity: 0;
+ transform: scale(0.95) translateY(-8px);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
+@keyframes modalScaleOut {
+ from {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+ to {
+ opacity: 0;
+ transform: scale(0.95) translateY(-8px);
+ }
+}
+
+.dialog {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ max-height: min(70vh, 400px);
+ outline: none;
+ overflow: hidden;
+}
+
+.searchField {
+ display: flex;
+ align-items: center;
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+}
+
+.srOnly {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border-width: 0;
+}
+
+.input {
+ width: 100%;
+ background: transparent;
+ border: none;
+ outline: none;
+ font-size: 1rem;
+ color: var(--primary);
+ font-family: inherit;
+
+ &::placeholder {
+ color: var(--secondary);
+ }
+}
+
+.menu {
+ flex: 1;
+ overflow-y: auto;
+ padding: 8px;
+ outline: none;
+ border: none;
+}
+
+.menuItem {
+ padding: 8px 12px;
+ border-radius: 8px;
+ color: var(--primary);
+ cursor: pointer;
+ font-size: 0.875rem;
+ outline: none;
+
+ &[data-focused] {
+ background-color: var(--action);
+ }
+
+ &[data-pressed] {
+ background-color: var(--action);
+ }
+}
+
+.empty {
+ padding: 24px 12px;
+ text-align: center;
+ color: var(--secondary);
+ font-size: 0.875rem;
+}
diff --git a/docs-ui/src/components/CommandPalette/CommandPalette.tsx b/docs-ui/src/components/CommandPalette/CommandPalette.tsx
new file mode 100644
index 0000000000..bcc534372a
--- /dev/null
+++ b/docs-ui/src/components/CommandPalette/CommandPalette.tsx
@@ -0,0 +1,76 @@
+'use client';
+
+import {
+ Autocomplete,
+ Dialog,
+ Input,
+ Label,
+ Menu,
+ MenuItem,
+ Modal,
+ ModalOverlay,
+ SearchField,
+ useFilter,
+} from 'react-aria-components';
+import { useRouter } from 'next/navigation';
+import { components } from '@/utils/data';
+import styles from './CommandPalette.module.css';
+
+interface CommandPaletteProps {
+ isOpen: boolean;
+ onOpenChange: (isOpen: boolean) => void;
+}
+
+const items = components.map(c => ({ id: c.slug, ...c }));
+
+export const CommandPalette = ({
+ isOpen,
+ onOpenChange,
+}: CommandPaletteProps) => {
+ const router = useRouter();
+ const { contains } = useFilter({ sensitivity: 'base' });
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/docs-ui/src/components/CommandPalette/index.ts b/docs-ui/src/components/CommandPalette/index.ts
new file mode 100644
index 0000000000..95c1305982
--- /dev/null
+++ b/docs-ui/src/components/CommandPalette/index.ts
@@ -0,0 +1 @@
+export { CommandPalette } from './CommandPalette';
diff --git a/docs-ui/src/components/Toolbar/Toolbar.module.css b/docs-ui/src/components/Toolbar/Toolbar.module.css
index 581fc599b3..56b047447c 100644
--- a/docs-ui/src/components/Toolbar/Toolbar.module.css
+++ b/docs-ui/src/components/Toolbar/Toolbar.module.css
@@ -10,12 +10,10 @@
}
}
-.breadcrumb {
+.left {
display: flex;
align-items: center;
gap: 0.5rem;
- font-size: 0.875rem;
- font-weight: 500;
}
.logoMobile {
@@ -26,39 +24,6 @@
}
}
-.breadcrumbDesktop {
- display: none;
-
- @media (min-width: 768px) {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- }
-}
-
-.breadcrumbLink {
- color: var(--secondary);
- text-decoration: none;
- cursor: pointer;
-
- &:hover {
- color: var(--primary);
- text-decoration: underline;
- transition: color 0.2s ease-in-out;
- text-decoration-thickness: 1px;
- text-underline-offset: 4px;
- }
-}
-
-.breadcrumbSeparator {
- color: var(--secondary);
- flex-shrink: 0;
-}
-
-.breadcrumbCurrent {
- color: var(--primary);
-}
-
.actions {
display: none;
@@ -119,6 +84,53 @@
}
}
+.searchButton {
+ display: none;
+ align-items: center;
+ gap: 6px;
+ background-color: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 32px;
+ padding-inline: 16px 12px;
+ min-width: 180px;
+ color: var(--secondary);
+ font-size: 0.8125rem;
+ font-weight: 500;
+ height: 32px;
+ cursor: pointer;
+ font-family: inherit;
+ white-space: nowrap;
+
+ &:hover {
+ background-color: var(--action);
+ color: var(--primary);
+ transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
+ }
+
+ @media (min-width: 768px) {
+ display: flex;
+ }
+}
+
+.searchLabel {
+ display: none;
+ flex: 1;
+ text-align: left;
+ display: inline;
+}
+
+.searchKbd {
+ display: none;
+ font-family: inherit;
+ font-size: 0.6875rem;
+ font-weight: 500;
+ background-color: var(--action);
+ border-radius: 4px;
+ padding: 2px 5px;
+ color: var(--secondary);
+ display: inline;
+}
+
.buttonGroup {
display: flex;
align-items: center;
diff --git a/docs-ui/src/components/Toolbar/Toolbar.tsx b/docs-ui/src/components/Toolbar/Toolbar.tsx
index 05757c4459..4a071bf45e 100644
--- a/docs-ui/src/components/Toolbar/Toolbar.tsx
+++ b/docs-ui/src/components/Toolbar/Toolbar.tsx
@@ -1,10 +1,11 @@
'use client';
+import { useState, useEffect } from 'react';
import {
RiArrowDownSLine,
- RiArrowRightSLine,
RiGithubLine,
RiMoonLine,
+ RiSearchLine,
RiSunLine,
} from '@remixicon/react';
import {
@@ -19,10 +20,8 @@ import {
} from 'react-aria-components';
import styles from './Toolbar.module.css';
import { usePlayground } from '@/utils/playground-context';
-import { usePathname } from 'next/navigation';
-import Link from 'next/link';
-import { components } from '@/utils/data';
import { Logo } from '@/components/Sidebar/Logo';
+import { CommandPalette } from '@/components/CommandPalette';
interface ToolbarProps {
version: string;
@@ -42,70 +41,36 @@ export const Toolbar = ({ version }: ToolbarProps) => {
setSelectedThemeName,
} = usePlayground();
- const pathname = usePathname();
+ const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false);
- // Determine breadcrumb content based on current path
- const getBreadcrumb = () => {
- const allComponents = components;
+ useEffect(() => {
+ const isMac = /mac(os|intosh)/i.test(navigator.userAgent);
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'k' && (isMac ? e.metaKey : e.ctrlKey)) {
+ e.preventDefault();
+ setIsCommandPaletteOpen(prev => !prev);
+ }
+ };
- // Components index page
- if (pathname === '/components') {
- return { section: null, title: 'Components' };
- }
-
- // Component detail pages
- if (pathname?.startsWith('/components/')) {
- const slug = pathname.split('/components/')[1];
- const component = allComponents.find(c => c.slug === slug);
- return {
- section: 'Components',
- sectionLink: '/components',
- title: component?.title || slug,
- };
- }
-
- // Tokens page
- if (pathname === '/tokens') {
- return { section: null, title: 'Tokens' };
- }
-
- // Changelog page
- if (pathname === '/changelog') {
- return { section: null, title: 'Changelog' };
- }
-
- return { section: null, title: '' };
- };
-
- const breadcrumb = getBreadcrumb();
+ document.addEventListener('keydown', handleKeyDown);
+ return () => document.removeEventListener('keydown', handleKeyDown);
+ }, []);
return (
-
+
-
- {breadcrumb.section && breadcrumb.sectionLink ? (
- <>
-
- {breadcrumb.section}
-
-
-
- {breadcrumb.title}
-
- >
- ) : (
- {breadcrumb.title}
- )}
-
+
+
);
};