@fpkit/acss 3.7.0 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Checkbox Wrapper Component Styles
3
+ *
4
+ * Modern CSS architecture using :has() selector with ARIA attributes.
5
+ * No JavaScript class management required - ARIA attributes drive both
6
+ * accessibility AND styling.
7
+ *
8
+ * CSS Custom Properties:
9
+ * - --checkbox-gap: Space between checkbox and label (default: 0.5rem)
10
+ * - --checkbox-disabled-opacity: Opacity for disabled state (default: 0.6)
11
+ * - --checkbox-disabled-color: Label color when disabled (default: #6b7280)
12
+ * - --checkbox-label-fs: Label font size (default: 1rem)
13
+ * - --checkbox-label-lh: Label line height (default: 1.5)
14
+ * - --color-required: Required indicator color (default: #dc2626)
15
+ * - --checkbox-focus-ring-color: Focus ring color (default: #2563eb)
16
+ * - --checkbox-focus-ring-width: Focus ring width (default: 0.125rem)
17
+ * - --checkbox-focus-ring-offset: Focus ring offset (default: 0.125rem)
18
+ * - --checkbox-hover-label-color: Label color on hover (default: inherit)
19
+ * - --checkbox-error-label-color: Label color when invalid (default: #dc2626)
20
+ * - --checkbox-valid-label-color: Label color when valid (default: #16a34a)
21
+ * - --checkbox-focus-radius: Focus outline border radius (default: 0.125rem)
22
+ *
23
+ * WCAG 2.1 AA Compliance:
24
+ * - 2.4.7 Focus Visible: Focus-visible indicators with sufficient contrast
25
+ * - 2.3.3 Animation from Interactions: Respects prefers-reduced-motion
26
+ * - 3.3.1 Error Identification: Visual error states with color + text
27
+ * - 4.1.2 Name, Role, Value: ARIA attributes for assistive technologies
28
+ * - 1.4.13 Content on Hover or Focus: Hover states for visual feedback
29
+ */
30
+
31
+ // Checkbox wrapper styling using modern :has() selector
32
+ div:has(> input[type="checkbox"]) {
33
+ display: flex;
34
+ align-items: center;
35
+ gap: var(--checkbox-gap, 0.5rem);
36
+ position: relative;
37
+
38
+ // Ensure checkbox doesn't overlap label
39
+ > input[type="checkbox"] {
40
+ flex-shrink: 0; // Prevent checkbox from shrinking
41
+ order: -1; // Ensure checkbox appears first
42
+ }
43
+
44
+ // Hover state (only when not disabled) - WCAG 1.4.13 Content on Hover
45
+ &:not(:has(> input[aria-disabled="true"])):hover {
46
+ .checkbox-label {
47
+ color: var(--checkbox-hover-label-color, inherit);
48
+ }
49
+ }
50
+
51
+ // Focus-visible state for keyboard navigation - WCAG 2.4.7 Focus Visible
52
+ // Using :focus-visible to only show focus ring for keyboard users
53
+ &:has(> input:focus-visible) {
54
+ .checkbox-label {
55
+ outline: var(--checkbox-focus-ring-width, 0.125rem) solid
56
+ var(--checkbox-focus-ring-color, #2563eb);
57
+ outline-offset: var(--checkbox-focus-ring-offset, 0.125rem);
58
+ border-radius: var(--checkbox-focus-radius, 0.125rem);
59
+ }
60
+ }
61
+
62
+ // Disabled state styling using aria-disabled attribute selector
63
+ // WCAG 4.1.2: aria-disabled remains focusable for screen readers
64
+ &:has(> input[aria-disabled="true"]) {
65
+ opacity: var(--checkbox-disabled-opacity, 0.6);
66
+ cursor: not-allowed;
67
+
68
+ .checkbox-label {
69
+ color: var(--checkbox-disabled-color, #6b7280);
70
+ cursor: not-allowed;
71
+ }
72
+ }
73
+
74
+ // Invalid state styling - WCAG 3.3.1 Error Identification
75
+ // Color alone is not sufficient - error message text must also be provided
76
+ &:has(> input[aria-invalid="true"]) {
77
+ .checkbox-label {
78
+ color: var(--checkbox-error-label-color, #dc2626);
79
+ }
80
+ }
81
+
82
+ // Valid state styling (when checked and explicitly marked valid)
83
+ &:has(> input[aria-invalid="false"]:checked) {
84
+ .checkbox-label {
85
+ color: var(--checkbox-valid-label-color, #16a34a);
86
+ }
87
+ }
88
+ }
89
+
90
+ // Checkbox label styling
91
+ .checkbox-label {
92
+ cursor: pointer;
93
+ font-size: var(--checkbox-label-fs, 1rem);
94
+ line-height: var(--checkbox-label-lh, 1.5);
95
+ user-select: none;
96
+ margin: 0;
97
+ flex: 1; // Allow label to take remaining space
98
+ min-width: 0; // Prevent flex item from overflowing
99
+ transition: color 0.2s ease-in-out;
100
+
101
+ // Respect user's motion preferences - WCAG 2.3.3 Animation from Interactions
102
+ @media (prefers-reduced-motion: reduce) {
103
+ transition: none;
104
+ }
105
+
106
+ .checkbox-required {
107
+ color: var(--color-required, #dc2626);
108
+ font-weight: 600;
109
+ margin-inline-start: 0.125rem;
110
+ }
111
+ }
112
+
113
+ // Checkbox input element
114
+ .checkbox-input {
115
+ // High contrast mode support (Windows High Contrast Mode)
116
+ // forced-color-adjust: auto respects user's color scheme
117
+ @media (forced-colors: active) {
118
+ forced-color-adjust: auto;
119
+ }
120
+ }
121
+
122
+ // Optional: Container query support for responsive layouts
123
+ // Stacks checkbox and label vertically on small containers
124
+ @container (max-width: 400px) {
125
+ div:has(> input[type="checkbox"]) {
126
+ flex-direction: column;
127
+ align-items: flex-start;
128
+ }
129
+ }
@@ -0,0 +1,302 @@
1
+ import React from "react";
2
+ import { Input, type InputProps } from "./inputs";
3
+
4
+ /**
5
+ * Props for the Checkbox component
6
+ *
7
+ * A simplified, checkbox-specific interface that wraps the Input component.
8
+ * Provides a boolean onChange API and automatic label association.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * // Controlled mode
13
+ * <Checkbox
14
+ * id="terms"
15
+ * label="I accept the terms"
16
+ * checked={accepted}
17
+ * onChange={setAccepted}
18
+ * required
19
+ * />
20
+ * ```
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * // Uncontrolled mode with default
25
+ * <Checkbox
26
+ * id="newsletter"
27
+ * label="Subscribe to newsletter"
28
+ * defaultChecked={true}
29
+ * />
30
+ * ```
31
+ */
32
+ export interface CheckboxProps extends Omit<
33
+ InputProps,
34
+ 'type' | 'value' | 'onChange' | 'defaultValue' | 'placeholder'
35
+ > {
36
+ /**
37
+ * Unique identifier for the checkbox input.
38
+ * Required for proper label association via htmlFor attribute.
39
+ *
40
+ * @required
41
+ * @see {@link https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html|WCAG 4.1.2 Name, Role, Value}
42
+ */
43
+ id: string;
44
+
45
+ /**
46
+ * Label text or React node displayed next to the checkbox.
47
+ * Automatically associated with the checkbox via htmlFor.
48
+ *
49
+ * @required
50
+ * @see {@link https://www.w3.org/WAI/WCAG21/Understanding/labels-or-instructions.html|WCAG 3.3.2 Labels or Instructions}
51
+ */
52
+ label: React.ReactNode;
53
+
54
+ /**
55
+ * Controlled mode: Current checked state.
56
+ * When provided, component becomes controlled and requires onChange handler.
57
+ *
58
+ * @example
59
+ * ```tsx
60
+ * const [checked, setChecked] = useState(false);
61
+ * <Checkbox id="opt" label="Option" checked={checked} onChange={setChecked} />
62
+ * ```
63
+ */
64
+ checked?: boolean;
65
+
66
+ /**
67
+ * Uncontrolled mode: Initial checked state.
68
+ * Use this for forms where React doesn't need to track the checkbox state.
69
+ *
70
+ * @default false
71
+ * @example
72
+ * ```tsx
73
+ * <Checkbox id="opt" label="Option" defaultChecked={true} />
74
+ * ```
75
+ */
76
+ defaultChecked?: boolean;
77
+
78
+ /**
79
+ * Form submission value when checkbox is checked.
80
+ * This is the value submitted with the form when the checkbox is checked.
81
+ *
82
+ * @default "on"
83
+ */
84
+ value?: string;
85
+
86
+ /**
87
+ * Change handler with simplified boolean API.
88
+ * Receives true when checked, false when unchecked.
89
+ *
90
+ * @param checked - The new checked state
91
+ * @example
92
+ * ```tsx
93
+ * <Checkbox
94
+ * id="opt"
95
+ * label="Option"
96
+ * onChange={(checked) => console.log('Checked:', checked)}
97
+ * />
98
+ * ```
99
+ */
100
+ onChange?: (checked: boolean) => void;
101
+
102
+ /**
103
+ * Optional custom CSS classes for the wrapper div.
104
+ * Applied alongside automatic checkbox wrapper styling.
105
+ */
106
+ classes?: string;
107
+
108
+ /**
109
+ * Optional custom CSS classes for the input element.
110
+ *
111
+ * @default "checkbox-input"
112
+ */
113
+ inputClasses?: string;
114
+
115
+ /**
116
+ * CSS custom properties for theming.
117
+ *
118
+ * Available variables:
119
+ * - --checkbox-gap: Space between checkbox and label (default: 0.5rem)
120
+ * - --checkbox-disabled-opacity: Opacity for disabled state (default: 0.6)
121
+ * - --checkbox-disabled-color: Label color when disabled (default: #6b7280)
122
+ * - --checkbox-label-fs: Label font size (default: 1rem)
123
+ * - --checkbox-label-lh: Label line height (default: 1.5)
124
+ * - --color-required: Required indicator color (default: #dc2626)
125
+ * - --checkbox-focus-ring-color: Focus ring color (default: #2563eb)
126
+ * - --checkbox-focus-ring-width: Focus ring width (default: 0.125rem)
127
+ * - --checkbox-focus-ring-offset: Focus ring offset (default: 0.125rem)
128
+ * - --checkbox-hover-label-color: Label color on hover
129
+ * - --checkbox-error-label-color: Label color when invalid (default: #dc2626)
130
+ * - --checkbox-valid-label-color: Label color when valid (default: #16a34a)
131
+ *
132
+ * @example
133
+ * ```tsx
134
+ * <Checkbox
135
+ * id="custom"
136
+ * label="Custom styled"
137
+ * styles={{
138
+ * '--checkbox-gap': '1rem',
139
+ * '--checkbox-focus-ring-color': '#ff0000'
140
+ * }}
141
+ * />
142
+ * ```
143
+ */
144
+ styles?: React.CSSProperties;
145
+ }
146
+
147
+ /**
148
+ * Checkbox - Accessible checkbox input with automatic label association
149
+ *
150
+ * A thin wrapper around the Input component that provides a checkbox-specific API
151
+ * with simplified boolean onChange and automatic label rendering. Leverages all
152
+ * validation, disabled state, and ARIA logic from the base Input component.
153
+ *
154
+ * **Key Features:**
155
+ * - ✅ Boolean onChange API (`onChange={(checked) => ...}`)
156
+ * - ✅ Automatic label association via htmlFor
157
+ * - ✅ WCAG 2.1 AA compliant (uses aria-disabled pattern)
158
+ * - ✅ Supports both controlled and uncontrolled modes
159
+ * - ✅ Required indicator with asterisk
160
+ * - ✅ Validation states (invalid, valid, none)
161
+ * - ✅ Error messages and hint text via Input component
162
+ * - ✅ Customizable via CSS custom properties
163
+ * - ✅ Keyboard accessible (Space to toggle)
164
+ * - ✅ Focus-visible indicators
165
+ * - ✅ High contrast mode support
166
+ *
167
+ * @component
168
+ * @example
169
+ * ```tsx
170
+ * // Basic checkbox
171
+ * <Checkbox id="terms" label="I accept the terms and conditions" />
172
+ * ```
173
+ *
174
+ * @example
175
+ * ```tsx
176
+ * // Controlled checkbox with validation
177
+ * const [agreed, setAgreed] = useState(false);
178
+ * <Checkbox
179
+ * id="terms"
180
+ * label="I accept the terms"
181
+ * checked={agreed}
182
+ * onChange={setAgreed}
183
+ * required
184
+ * validationState={agreed ? "valid" : "invalid"}
185
+ * errorMessage={!agreed ? "You must accept the terms" : undefined}
186
+ * />
187
+ * ```
188
+ *
189
+ * @example
190
+ * ```tsx
191
+ * // Disabled checkbox
192
+ * <Checkbox
193
+ * id="disabled"
194
+ * label="Disabled option"
195
+ * disabled
196
+ * defaultChecked
197
+ * />
198
+ * ```
199
+ *
200
+ * @example
201
+ * ```tsx
202
+ * // Custom styling
203
+ * <Checkbox
204
+ * id="custom"
205
+ * label="Large checkbox"
206
+ * styles={{ '--checkbox-gap': '1rem' }}
207
+ * />
208
+ * ```
209
+ *
210
+ * @param {CheckboxProps} props - Component props
211
+ * @param {React.Ref<HTMLInputElement>} ref - Forwarded ref to the input element
212
+ * @returns {JSX.Element} Checkbox wrapper with input and label
213
+ *
214
+ * @see {@link https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html|WCAG 4.1.2 Name, Role, Value}
215
+ * @see {@link https://www.w3.org/WAI/WCAG21/Understanding/focus-visible.html|WCAG 2.4.7 Focus Visible}
216
+ * @see {@link https://www.w3.org/WAI/WCAG21/Understanding/error-identification.html|WCAG 3.3.1 Error Identification}
217
+ */
218
+ export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
219
+ ({
220
+ id, label, checked, defaultChecked, value = "on",
221
+ onChange, classes, inputClasses, styles,
222
+ name, disabled, required, validationState,
223
+ errorMessage, hintText, onBlur, onFocus, autoFocus,
224
+ ...props
225
+ }, ref) => {
226
+
227
+ // Convert boolean onChange to native event handler
228
+ // Memoized to prevent unnecessary re-renders of child components
229
+ const handleChange = React.useCallback(
230
+ (e: React.ChangeEvent<HTMLInputElement>) => {
231
+ onChange?.(e.target.checked);
232
+ },
233
+ [onChange]
234
+ );
235
+
236
+ // Controlled vs uncontrolled mode
237
+ const isControlled = checked !== undefined;
238
+ const checkedProp = isControlled ? { checked } : {};
239
+ const defaultCheckedProp = !isControlled && defaultChecked !== undefined
240
+ ? { defaultChecked }
241
+ : {};
242
+
243
+ // Dev-only validation: Warn if switching between controlled/uncontrolled
244
+ // This helps catch common React bugs where state management changes mid-lifecycle
245
+ const wasControlledRef = React.useRef(isControlled);
246
+
247
+ React.useEffect(() => {
248
+ if (process.env.NODE_ENV === 'development') {
249
+ if (wasControlledRef.current !== isControlled) {
250
+ // eslint-disable-next-line no-console
251
+ console.warn(
252
+ `Checkbox with id="${id}" is changing from ${
253
+ wasControlledRef.current ? 'controlled' : 'uncontrolled'
254
+ } to ${
255
+ isControlled ? 'controlled' : 'uncontrolled'
256
+ }. This is likely a bug. ` +
257
+ `Decide between using "checked" (controlled) or "defaultChecked" (uncontrolled) and stick with it.`
258
+ );
259
+ }
260
+ wasControlledRef.current = isControlled;
261
+ }
262
+ }, [isControlled, id]);
263
+
264
+ // Note: No need to manage disabled class - CSS uses :has() selector with aria-disabled
265
+ // The Input component handles aria-disabled automatically via useDisabledState hook
266
+ return (
267
+ <div className={classes} style={styles}>
268
+ <Input
269
+ ref={ref}
270
+ type="checkbox"
271
+ id={id}
272
+ name={name}
273
+ value={value}
274
+ {...checkedProp}
275
+ {...defaultCheckedProp}
276
+ classes={inputClasses || "checkbox-input"}
277
+ disabled={disabled}
278
+ required={required}
279
+ validationState={validationState}
280
+ errorMessage={errorMessage}
281
+ hintText={hintText}
282
+ onChange={handleChange}
283
+ onBlur={onBlur}
284
+ onFocus={onFocus}
285
+ autoFocus={autoFocus}
286
+ {...props}
287
+ />
288
+ <label htmlFor={id} className="checkbox-label">
289
+ {label}
290
+ {required && (
291
+ <span className="checkbox-required" aria-label="required">
292
+ {" *"}
293
+ </span>
294
+ )}
295
+ </label>
296
+ </div>
297
+ );
298
+ }
299
+ );
300
+
301
+ Checkbox.displayName = "Checkbox";
302
+ export default Checkbox;
@@ -1,3 +1,6 @@
1
+ // Import checkbox wrapper component styles
2
+ @use "./checkbox";
3
+
1
4
  :root {
2
5
  --input-border-color: gray;
3
6
  --input-appearance: none;
@@ -26,6 +29,26 @@
26
29
 
27
30
  --form-direction: column;
28
31
  --select-arrow: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='20'><polyline points='6,9 10,13 14,9' stroke='%23000000' stroke-width='1.5' fill='none' /></svg>");
32
+
33
+ /* ==========================================================================
34
+ Size Tokens
35
+ ========================================================================== */
36
+
37
+ --checkbox-size-sm: 1rem; /* 16px */
38
+ --checkbox-size-md: 1.25rem; /* 20px */
39
+ --checkbox-size-lg: 1.5rem; /* 24px */
40
+
41
+ /* ==========================================================================
42
+ Base Properties
43
+ ========================================================================== */
44
+
45
+ --checkbox-size: var(--checkbox-size-md);
46
+ --checkbox-bg: #ffffff;
47
+ --checkbox-border: 0.125rem solid #6b7280; /* 2px border */
48
+ --checkbox-border-color: #6b7280; /* Gray 500 */
49
+ --checkbox-radius: 0.25rem; /* 4px */
50
+ --checkbox-cursor: pointer;
51
+ --checkbox-transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
29
52
  }
30
53
 
31
54
  form {
@@ -43,9 +66,7 @@ form {
43
66
  }
44
67
  }
45
68
 
46
- input[type]:not([type='checkbox'], [type='radio']),
47
- textarea,
48
- select {
69
+ input {
49
70
  -webkit-appearance: var(--input-appearance);
50
71
  -moz-appearance: var(--input-appearance);
51
72
  appearance: var(--input-appearance);
@@ -57,37 +78,55 @@ select {
57
78
  border-radius: var(--input-radius);
58
79
  background-color: var(--input-bg, #fff);
59
80
 
81
+ &:focus-visible,
82
+ &:focus {
83
+ outline: var(--input-focus-outline);
84
+ outline-offset: var(--input-focus-outline-offset);
85
+ }
86
+
87
+ &[aria-disabled="true"],
88
+ &:disabled {
89
+ --input-border-color: lightgray;
90
+ background-color: var(--input-disabled-bg);
91
+ opacity: var(--input-disabled-opacity);
92
+ cursor: var(--input-disabled-cursor);
93
+ text-transform: capitalize;
94
+ text-decoration: line-through;
95
+ }
96
+ }
97
+
98
+ input[type]:not([type="checkbox"], [type="radio"]),
99
+ textarea,
100
+ select {
60
101
  &::placeholder {
61
102
  color: var(--placeholder-color);
62
103
  font-style: var(--placeholder-style);
63
104
  font-size: var(--placeholder-fs);
64
105
  text-transform: capitalize;
65
106
  }
66
-
67
- &:focus-visible, &:focus {
68
- outline: var(--input-focus-outline);
69
- outline-offset: var(--input-focus-outline-offset);
70
- }
71
-
72
-
73
- &[aria-required='true'] {
107
+ &[aria-required="true"] {
74
108
  &::placeholder {
75
109
  color: var(--color-required, var(--placeholder-color));
76
110
  font-weight: 600;
77
111
  &::after {
78
- content: '* ';
112
+ content: "* ";
79
113
  }
80
114
  }
81
115
  }
116
+ }
82
117
 
83
- &[aria-disabled='true'],
84
- &:disabled {
85
- --input-border-color: lightgray;
86
- background-color: var(--input-disabled-bg);
87
- opacity: var(--input-disabled-opacity);
88
- cursor: var(--input-disabled-cursor);
89
- text-transform: capitalize;
90
- text-decoration: line-through;
118
+ input[type="checkbox"] {
119
+ opacity: 1;
120
+ width: var(--checkbox-size);
121
+ height: var(--checkbox-size);
122
+ margin: 0;
123
+ cursor: var(--checkbox-cursor);
124
+ flex-shrink: 0; // Prevent checkbox from shrinking in flex layout
125
+
126
+ &:checked {
127
+ background-color: var(--checkbox-bg, red);
128
+ outline: rebeccapurple solid 2px;
129
+ background: #dbeafe;
91
130
  }
92
131
  }
93
132
 
@@ -376,3 +376,9 @@ export interface SelectProps extends Omit<React.ComponentPropsWithoutRef<'select
376
376
  */
377
377
  children?: React.ReactNode
378
378
  }
379
+
380
+ /**
381
+ * Checkbox component props
382
+ * Re-exported from checkbox.tsx for convenience
383
+ */
384
+ export type { CheckboxProps } from './checkbox'