@cujuju/solidjs-seg-buttons
v0.3.0
Published
Segmented button control for SolidJS — controlled or uncontrolled, size presets, full a11y, RTL-ready.
Maintainers
Readme
@cujuju/solidjs-seg-buttons
Segmented button control for SolidJS. Controlled or uncontrolled. Full a11y. RTL-ready via CSS logical properties.
Install
pnpm add @cujuju/solidjs-seg-buttonsThe package ships its own stylesheet, auto-imported via the entrypoint. If your bundler doesn't pick up the side-effect import, add it manually:
import '@cujuju/solidjs-seg-buttons/styles.css';Usage
Uncontrolled (caller manages state per button)
import { SegGroup, SegButton } from '@cujuju/solidjs-seg-buttons';
const [tf, setTf] = createSignal('day');
<SegGroup ariaLabel="Timeframe">
<SegButton label="Day" active={tf() === 'day'} onClick={() => setTf('day')} />
<SegButton label="Week" active={tf() === 'week'} onClick={() => setTf('week')} />
<SegButton label="Month" active={tf() === 'month'} onClick={() => setTf('month')} />
</SegGroup>Controlled (SegGroup owns the selected value)
<SegGroup value={tf()} onChange={setTf} ariaLabel="Timeframe">
<SegButton value="day" label="Day" />
<SegButton value="week" label="Week" />
<SegButton value="month" label="Month" />
</SegGroup>When SegGroup.value is set, SegButtons read state from context and call onChange with their own value prop on click. Falls back to active/onClick props when SegGroup.value is undefined or the button has no value.
Radiogroup semantics (recommended for mutually-exclusive selections)
<SegGroup role="radiogroup" value={tf()} onChange={setTf} ariaLabel="Timeframe">
<SegButton value="day" label="Day" />
<SegButton value="week" label="Week" />
</SegGroup>role="radiogroup"on the wrapper; each SegButton emitsrole="radio"witharia-checked- Roving tabindex: exactly one button is Tab-focusable — the active button if it is enabled, else the first enabled button (none when every option is disabled)
- ArrowLeft / ArrowRight move focus (and selection) between siblings, skipping disabled ones; mirrored under
dir="rtl"
The default is role="group" with aria-pressed on each button — simpler and works fine for non-exclusive toggle patterns.
API
<SegGroup>
| Prop | Default | Description |
|---|---|---|
| value | — | Controlled-mode selected value. If set, enables context-driven selection. |
| onChange(value) | — | Called when a child SegButton with a value is clicked. |
| role | 'group' | 'group' (aria-pressed mode) or 'radiogroup' (ARIA radio mode). |
| ariaLabel | — | Describes the group for screen readers. |
| class | — | Additional class on the wrapper span. |
<SegButton>
| Prop | Default | Description |
|---|---|---|
| label | (required) | Button label. Also used for bold-width reservation. |
| active | false | Selected state (uncontrolled mode). |
| onClick | — | Click handler (uncontrolled mode). |
| value | — | This button's value in controlled mode. |
| size | 'md' | Preset — 'xs' / 'sm' / 'md'. |
| height | (preset) | Raw override. Numbers → px. |
| paddingX | (preset) | Raw override. |
| fontSize | (preset) | Raw override. |
| minWidth | — | Optional px minimum width. |
| reserveBoldWidth | true | Reserves bold-weight width so active state doesn't shift layout. |
| children | — | Alternate content (icon + text, etc.). Falls back to label. |
| disabled | false | Non-interactive, dimmed. |
| ariaLabel | — | Screen-reader label override. |
| title | — | Hover hint — rendered through a registered tooltip host, else a native title. |
| tooltipDelayMs | (shared default, 600) | Overrides the host's show delay for this button, ms. Host path only. |
| class | — | Additional class. |
Tooltip host
title renders as a native title unless a tooltip component is registered. Register one once at app boot to upgrade every SegButton hint (the native title is then suppressed, so hints never double up):
import { KvTooltip } from '@cujuju/solidjs-kv-tooltip';
import { setSegTooltipHost, setSegTooltipDefaults } from '@cujuju/solidjs-seg-buttons';
setSegTooltipHost(KvTooltip); // `null` clears it
setSegTooltipDefaults({ delayMs: 400, maxWidth: 240 }); // merges over { delayMs: 600, maxWidth: 300 }| Export | Description |
|---|---|
| setSegTooltipHost(host \| null) | Register or clear the hint renderer. Already-mounted buttons switch in place. |
| setSegTooltipDefaults(partial) | Shared delayMs / maxWidth for every hint. |
| segTooltipDefaults() | Current defaults (reactive). |
| SegTooltipHost / SegTooltipHostProps | Structural contract a host component must satisfy (KvTooltip does). |
| SegTooltipDefaults | { delayMs, maxWidth }. |
Theming
Override any of these CSS custom properties at any ancestor level:
:root {
--seg-active-bg: #10b981;
--seg-active-border: #059669;
--seg-active-text: #ffffff;
--seg-active-weight: 600;
--seg-inactive-bg: #1e293b;
--seg-inactive-border: #334155;
--seg-inactive-text: #94a3b8;
--seg-disabled-opacity: 0.4;
/* Recommended override point for a brighter ring: */
--seg-focus-ring-color: color-mix(in srgb, var(--seg-inactive-border) 45%, #fff);
--seg-focus-ring: 1px solid var(--seg-focus-ring-color);
--seg-focus-ring-offset: -1px;
--seg-radius: 4px;
}Intent variants (extension pattern)
The library ships with one accent. For red/green/etc. variants, define a scoped override:
.seg-danger {
--seg-active-bg: #fee2e2;
--seg-active-border: #ef4444;
--seg-active-text: #dc2626;
}Then apply the class to the SegGroup or individual SegButton:
<SegButton class="seg-danger" active={mode() === 'delete'} onClick={...} label="Delete" />RTL
All corner-rounding and button-overlap uses CSS logical properties (border-start-start-radius, margin-inline-start, etc.), so RTL layouts flip correctly when dir="rtl" is set on any ancestor.
Touch targets
Preset sizes xs (20px) and sm (24px) fall below WCAG 2.1 AA minimum touch target size of 44×44 CSS px. For touch-primary UIs, use size="md" (28px) with a height override:
<SegButton height={44} paddingX="1rem" label="Mobile" active={...} onClick={...} />License
MIT
