{children}
+ {action && ( + + )} +{children}
+ +
setFailedId(id)}
+ />
+ )
+}
diff --git a/web/src/ui/Segmented.module.css b/web/src/ui/Segmented.module.css
new file mode 100644
index 0000000..3d94baa
--- /dev/null
+++ b/web/src/ui/Segmented.module.css
@@ -0,0 +1,35 @@
+.track {
+ display: flex;
+ gap: 4px;
+ padding: 4px;
+ border-radius: var(--radius-pill);
+ background: var(--color-surface);
+}
+
+.option {
+ flex: 1 1 0;
+ min-width: 44px;
+ min-height: 44px;
+ padding: 0 var(--space-4);
+ border: 0;
+ border-radius: var(--radius-pill);
+ background: transparent;
+ color: var(--color-neutral-700);
+ font-family: var(--font-body);
+ font-size: 14px;
+ font-weight: 700;
+ cursor: pointer;
+ transition: background-color 150ms var(--ease-out-soft), box-shadow 150ms var(--ease-out-soft);
+}
+
+.selected {
+ background: var(--color-bg);
+ color: var(--color-text);
+ box-shadow: var(--shadow-sm);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .option {
+ transition: none;
+ }
+}
diff --git a/web/src/ui/Segmented.test.tsx b/web/src/ui/Segmented.test.tsx
new file mode 100644
index 0000000..c107a1e
--- /dev/null
+++ b/web/src/ui/Segmented.test.tsx
@@ -0,0 +1,91 @@
+import { fireEvent, render, screen } from '@testing-library/react'
+import { useState } from 'react'
+import { describe, expect, it } from 'vitest'
+import { expectMinHitArea } from '../test/hitArea.ts'
+import { Segmented } from './Segmented.tsx'
+import styles from './Segmented.module.css'
+
+const options = [
+ { value: 'week', label: 'Week' },
+ { value: 'month', label: 'Month' },
+ { value: 'year', label: 'Year' },
+] as const
+
+type Period = (typeof options)[number]['value']
+
+function Harness({ initial = 'week' }: { initial?: Period }) {
+ const [value, setValue] = useStateGrouped by dinner
+ {withControls && ( + <> + + + > + )} +Nothing yet
+Grouped by dinner
+