@fpkit/acss 6.2.0 → 6.4.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.
Files changed (121) hide show
  1. package/libs/chunk-25KCUE3R.cjs +17 -0
  2. package/libs/chunk-25KCUE3R.cjs.map +1 -0
  3. package/libs/chunk-34NWHFHP.js +10 -0
  4. package/libs/chunk-34NWHFHP.js.map +1 -0
  5. package/libs/{chunk-SQ44OCJ2.js → chunk-6NMLU5FA.js} +2 -2
  6. package/libs/{chunk-GVVCXXKI.cjs → chunk-6YVR4TDM.cjs} +3 -3
  7. package/libs/chunk-DSQ2TUCR.js +7 -0
  8. package/libs/chunk-DSQ2TUCR.js.map +1 -0
  9. package/libs/{chunk-H6A2CUWA.js → chunk-VQTCTLFN.js} +2 -2
  10. package/libs/chunk-ZJ4RUKI2.cjs +14 -0
  11. package/libs/chunk-ZJ4RUKI2.cjs.map +1 -0
  12. package/libs/{chunk-H4JRUNKU.cjs → chunk-ZOPHCNFD.cjs} +3 -3
  13. package/libs/components/alert/alert.css +1 -1
  14. package/libs/components/alert/alert.css.map +1 -1
  15. package/libs/components/alert/alert.min.css +2 -2
  16. package/libs/components/button.cjs +3 -3
  17. package/libs/components/button.d.cts +34 -1
  18. package/libs/components/button.d.ts +34 -1
  19. package/libs/components/button.js +1 -1
  20. package/libs/components/buttons/button.css +1 -1
  21. package/libs/components/buttons/button.css.map +1 -1
  22. package/libs/components/buttons/button.min.css +2 -2
  23. package/libs/components/buttons/icon-button.css +1 -0
  24. package/libs/components/buttons/icon-button.css.map +1 -0
  25. package/libs/components/buttons/icon-button.min.css +3 -0
  26. package/libs/components/dialog/dialog.cjs +4 -4
  27. package/libs/components/dialog/dialog.css +1 -1
  28. package/libs/components/dialog/dialog.css.map +1 -1
  29. package/libs/components/dialog/dialog.js +2 -2
  30. package/libs/components/dialog/dialog.min.css +2 -2
  31. package/libs/components/link/link.css +1 -1
  32. package/libs/components/link/link.min.css +1 -1
  33. package/libs/components/modal.cjs +3 -3
  34. package/libs/components/modal.js +2 -2
  35. package/libs/components/popover/popover.cjs +3 -8
  36. package/libs/components/popover/popover.css +1 -0
  37. package/libs/components/popover/popover.css.map +1 -0
  38. package/libs/components/popover/popover.d.cts +54 -26
  39. package/libs/components/popover/popover.d.ts +54 -26
  40. package/libs/components/popover/popover.js +1 -2
  41. package/libs/components/popover/popover.min.css +3 -0
  42. package/libs/hooks.cjs +3 -6
  43. package/libs/hooks.cjs.map +1 -1
  44. package/libs/hooks.d.cts +30 -10
  45. package/libs/hooks.d.ts +30 -10
  46. package/libs/hooks.js +5 -1
  47. package/libs/hooks.js.map +1 -1
  48. package/libs/index.cjs +35 -35
  49. package/libs/index.cjs.map +1 -1
  50. package/libs/index.css +1 -1
  51. package/libs/index.css.map +1 -1
  52. package/libs/index.d.cts +65 -3
  53. package/libs/index.d.ts +65 -3
  54. package/libs/index.js +9 -10
  55. package/libs/index.js.map +1 -1
  56. package/package.json +2 -2
  57. package/src/components/alert/alert.scss +0 -13
  58. package/src/components/buttons/README.mdx +107 -11
  59. package/src/components/buttons/STYLES.mdx +182 -47
  60. package/src/components/buttons/button.scss +93 -16
  61. package/src/components/buttons/button.stories.tsx +149 -0
  62. package/src/components/buttons/button.test.tsx +12 -0
  63. package/src/components/buttons/button.tsx +50 -6
  64. package/src/components/buttons/icon-button.mdx +204 -0
  65. package/src/components/buttons/icon-button.scss +83 -0
  66. package/src/components/buttons/icon-button.stories.tsx +200 -0
  67. package/src/components/buttons/icon-button.test.tsx +132 -0
  68. package/src/components/buttons/icon-button.tsx +75 -0
  69. package/src/components/dialog/dialog-modal.stories.tsx +71 -0
  70. package/src/components/dialog/dialog-modal.tsx +29 -3
  71. package/src/components/dialog/dialog.scss +1 -0
  72. package/src/components/dialog/dialog.test.tsx +119 -0
  73. package/src/components/dialog/dialog.types.ts +8 -1
  74. package/src/components/form/select.tsx +55 -51
  75. package/src/components/link/link.scss +2 -2
  76. package/src/components/popover/README.mdx +478 -0
  77. package/src/components/popover/STYLES.mdx +389 -0
  78. package/src/components/popover/index.ts +3 -0
  79. package/src/components/popover/popover.scss +249 -0
  80. package/src/components/popover/popover.stories.tsx +315 -15
  81. package/src/components/popover/popover.test.tsx +249 -37
  82. package/src/components/popover/popover.tsx +165 -62
  83. package/src/hooks/popover/popover.tsx +26 -10
  84. package/src/hooks/popover/use-popover.tsx +30 -10
  85. package/src/hooks.ts +5 -0
  86. package/src/index.scss +1 -0
  87. package/src/index.ts +1 -0
  88. package/src/sass/utilities/_display.scss +156 -0
  89. package/src/sass/utilities/_index.scss +3 -0
  90. package/src/sass/utilities/display.mdx +203 -0
  91. package/src/sass/utilities/display.stories.tsx +141 -0
  92. package/src/styles/alert/alert.css +0 -13
  93. package/src/styles/alert/alert.css.map +1 -1
  94. package/src/styles/buttons/button.css +78 -16
  95. package/src/styles/buttons/button.css.map +1 -1
  96. package/src/styles/buttons/icon-button.css +71 -0
  97. package/src/styles/buttons/icon-button.css.map +1 -0
  98. package/src/styles/dialog/dialog.css +1 -0
  99. package/src/styles/dialog/dialog.css.map +1 -1
  100. package/src/styles/index.css +404 -31
  101. package/src/styles/index.css.map +1 -1
  102. package/src/styles/link/link.css +2 -2
  103. package/src/styles/popover/popover.css +190 -0
  104. package/src/styles/popover/popover.css.map +1 -0
  105. package/src/types/popover.d.ts +64 -0
  106. package/libs/chunk-4I5MF54P.js +0 -8
  107. package/libs/chunk-4I5MF54P.js.map +0 -1
  108. package/libs/chunk-GCGKYLDG.js +0 -7
  109. package/libs/chunk-GCGKYLDG.js.map +0 -1
  110. package/libs/chunk-NZVSXRTB.cjs +0 -16
  111. package/libs/chunk-NZVSXRTB.cjs.map +0 -1
  112. package/libs/chunk-PDD4N5P5.cjs +0 -10
  113. package/libs/chunk-PDD4N5P5.cjs.map +0 -1
  114. package/libs/chunk-S7NIA6PI.cjs +0 -17
  115. package/libs/chunk-S7NIA6PI.cjs.map +0 -1
  116. package/libs/chunk-X2RDXWH5.js +0 -10
  117. package/libs/chunk-X2RDXWH5.js.map +0 -1
  118. /package/libs/{chunk-SQ44OCJ2.js.map → chunk-6NMLU5FA.js.map} +0 -0
  119. /package/libs/{chunk-GVVCXXKI.cjs.map → chunk-6YVR4TDM.cjs.map} +0 -0
  120. /package/libs/{chunk-H6A2CUWA.js.map → chunk-VQTCTLFN.js.map} +0 -0
  121. /package/libs/{chunk-H4JRUNKU.cjs.map → chunk-ZOPHCNFD.cjs.map} +0 -0
@@ -4,10 +4,14 @@ button {
4
4
  --btn-size-sm: 0.8125rem;
5
5
  --btn-size-md: 0.9375rem;
6
6
  --btn-size-lg: 1.125rem;
7
- --btn-pill: 100rem;
7
+ --btn-size-xl: 1.375rem;
8
+ --btn-size-2xl: 1.75rem;
9
+ --btn-pill: 100vw;
8
10
  --btn-fs: var(--btn-size-md);
9
- --btn-height: calc(var(--btn-fs) * 2.25);
10
- --btn-bg: var(--color-neutral-300);
11
+
12
+ --btn-height: calc(var(--btn-fs) * 2.75);
13
+ --btn-bg: var(--color-primary);
14
+ --btn-color: var(--color-text-inverse);
11
15
  --btn-width: max-content;
12
16
 
13
17
  font-size: var(--btn-fs);
@@ -36,7 +40,7 @@ button {
36
40
  line-height: 0cap;
37
41
 
38
42
  &[type] {
39
- background-color: var(--btn-bg, var(--color-neutral-300));
43
+ background-color: var(--btn-bg, var(--color-primary));
40
44
  --btn-border: solid var(--btn-sg);
41
45
  }
42
46
 
@@ -93,8 +97,77 @@ button {
93
97
 
94
98
  &[data-fp-btn~="pill"],
95
99
  &[data-btn~="pill"],
96
- &[data-style~="pill"] {
97
- border-radius: var(--btn-pill, 100rem);
100
+ &[data-style~="pill"],
101
+ &.btn-pill {
102
+ border-radius: var(--btn-pill, 100vw);
103
+ }
104
+
105
+ // Color variants — map to semantic tokens from index.css
106
+ &[data-color="primary"] {
107
+ --btn-bg: var(--color-primary);
108
+ --btn-color: var(--color-text-inverse);
109
+ }
110
+
111
+ &[data-color="secondary"] {
112
+ --btn-bg: var(--color-secondary);
113
+ --btn-color: var(--color-text-inverse);
114
+ }
115
+
116
+ &[data-color="danger"] {
117
+ --btn-bg: var(--color-error);
118
+ --btn-color: var(--color-text-inverse);
119
+ }
120
+
121
+ &[data-color="success"] {
122
+ --btn-bg: var(--color-success);
123
+ --btn-color: var(--color-text-inverse);
124
+ }
125
+
126
+ &[data-color="warning"] {
127
+ --btn-bg: var(--color-warning);
128
+ --btn-color: var(--color-text-inverse);
129
+ }
130
+
131
+ // Style variants via data-style (variant prop) — mirrors link button patterns
132
+ &[data-style~="outline"] {
133
+ --btn-bg: transparent;
134
+ --btn-color: currentColor;
135
+ --btn-border: 0.125rem solid currentColor;
136
+
137
+ &:is(:hover, :focus) {
138
+ background-color: color-mix(in srgb, currentColor 10%, transparent);
139
+ filter: none;
140
+ outline: 0.025rem solid currentColor;
141
+ outline-offset: 0;
142
+ }
143
+ }
144
+
145
+ &[data-style~="text"] {
146
+ --btn-bg: transparent;
147
+ --btn-color: currentColor;
148
+ --btn-border: none;
149
+ --btn-height: unset;
150
+ --btn-width: unset;
151
+ --btn-padding-block: 0.75rem;
152
+ --btn-padding-inline: 0.75rem;
153
+ &:is(:hover, :focus) {
154
+ background-color: color-mix(in srgb, var(--btn-color) 10%, transparent);
155
+ outline: 0.025rem solid var(--btn-color);
156
+ outline-offset: 0;
157
+ filter: none;
158
+ }
159
+ }
160
+
161
+ &[data-style~="icon"] {
162
+ padding: unset;
163
+ height: unset;
164
+ --btn-bg: transparent;
165
+ min-width: 1.5rem;
166
+ min-height: 1.5rem;
167
+ text-align: center;
168
+ display: inline-flex;
169
+ align-items: center;
170
+ justify-content: center;
98
171
  }
99
172
 
100
173
  &[data-btn~="xs"],
@@ -118,16 +191,20 @@ button {
118
191
  --btn-fs: var(--btn-size-lg);
119
192
  }
120
193
 
121
- &[data-btn~="icon"],
122
- .btn-icon {
123
- padding: unset;
124
- height: unset;
125
- --btn-bg: transparent;
126
- min-width: 1.5rem;
127
- min-height: 1.5rem;
128
- text-align: center;
129
- display: inline-flex;
130
- align-items: center;
194
+ &[data-btn~="xl"],
195
+ .btn-xl {
196
+ --btn-fs: var(--btn-size-xl);
197
+ }
198
+
199
+ &[data-btn~="2xl"],
200
+ .btn-2xl {
201
+ --btn-fs: var(--btn-size-2xl);
202
+ }
203
+
204
+ &[data-btn~="block"],
205
+ .btn-block {
206
+ --btn-width: 100%;
207
+ display: flex;
131
208
  justify-content: center;
132
209
  }
133
210
 
@@ -14,6 +14,27 @@ const meta = {
14
14
  children: "Click me",
15
15
  onClick: buttonClicked,
16
16
  },
17
+ argTypes: {
18
+ size: {
19
+ control: "select",
20
+ options: ["xs", "sm", "md", "lg", "xl", "2xl"],
21
+ description: "Size token — maps to data-btn attribute",
22
+ },
23
+ block: {
24
+ control: "boolean",
25
+ description: "Stretch button to 100% container width — composes with size and variant",
26
+ },
27
+ variant: {
28
+ control: "select",
29
+ options: ["text", "pill", "icon", "outline"],
30
+ description: "Style variant — maps to data-style attribute",
31
+ },
32
+ color: {
33
+ control: "select",
34
+ options: ["primary", "secondary", "danger", "success", "warning"],
35
+ description: "Color variant using semantic design tokens — maps to data-color attribute",
36
+ },
37
+ },
17
38
  parameters: {},
18
39
  } as Meta;
19
40
 
@@ -109,6 +130,132 @@ export const Large: Story = {
109
130
  },
110
131
  } as Story;
111
132
 
133
+ // --- Size prop stories (typed API instead of raw data-btn) ---
134
+
135
+ export const SizeXS: Story = {
136
+ args: { size: "xs", children: "Extra Small" },
137
+ } as Story;
138
+
139
+ export const SizeSM: Story = {
140
+ args: { size: "sm", children: "Small" },
141
+ } as Story;
142
+
143
+ export const SizeLG: Story = {
144
+ args: { size: "lg", children: "Large" },
145
+ } as Story;
146
+
147
+ export const SizeXL: Story = {
148
+ args: { size: "xl", children: "Extra Large" },
149
+ } as Story;
150
+
151
+ export const Size2XL: Story = {
152
+ args: { size: "2xl", children: "2X Large" },
153
+ } as Story;
154
+
155
+ // --- Block stories ---
156
+
157
+ /**
158
+ * Block button — stretches to 100% container width at the default size.
159
+ */
160
+ export const Block: Story = {
161
+ args: { block: true, children: "Block Button" },
162
+ } as Story;
163
+
164
+ /**
165
+ * Block button composed with size and color variants.
166
+ */
167
+ export const BlockVariants: Story = {
168
+ render: () => (
169
+ <div style={{ display: "flex", flexDirection: "column", gap: "1rem", maxWidth: "32rem" }}>
170
+ <Button type="button" block size="sm">Block Small</Button>
171
+ <Button type="button" block>Block Default</Button>
172
+ <Button type="button" block size="lg">Block Large</Button>
173
+ <Button type="button" block size="xl" color="primary">Block XL Primary</Button>
174
+ <Button type="button" block color="danger" variant="outline">Block Danger Outline</Button>
175
+ </div>
176
+ ),
177
+ } as Story;
178
+
179
+ // --- Variant stories ---
180
+
181
+ export const Outline: Story = {
182
+ args: { variant: "outline", children: "Outline" },
183
+ } as Story;
184
+
185
+ export const Pill: Story = {
186
+ args: { variant: "pill", children: "Pill" },
187
+ } as Story;
188
+
189
+ export const TextVariant: Story = {
190
+ args: { variant: "text", children: "Text Button" },
191
+ } as Story;
192
+
193
+ // --- Color stories ---
194
+
195
+ export const Primary: Story = {
196
+ args: { color: "primary", children: "Primary" },
197
+ } as Story;
198
+
199
+ export const Secondary: Story = {
200
+ args: { color: "secondary", children: "Secondary" },
201
+ } as Story;
202
+
203
+ export const Danger: Story = {
204
+ args: { color: "danger", children: "Danger" },
205
+ } as Story;
206
+
207
+ export const Success: Story = {
208
+ args: { color: "success", children: "Success" },
209
+ } as Story;
210
+
211
+ export const Warning: Story = {
212
+ args: { color: "warning", children: "Warning" },
213
+ } as Story;
214
+
215
+ // --- Combination stories ---
216
+
217
+ export const PrimaryOutline: Story = {
218
+ args: { color: "primary", variant: "outline", children: "Primary Outline" },
219
+ } as Story;
220
+
221
+ export const DangerPill: Story = {
222
+ args: { color: "danger", variant: "pill", children: "Danger Pill" },
223
+ } as Story;
224
+
225
+ export const SuccessOutline: Story = {
226
+ args: { color: "success", variant: "outline", children: "Success Outline" },
227
+ } as Story;
228
+
229
+ /**
230
+ * All color variants side by side.
231
+ */
232
+ export const AllColors: Story = {
233
+ render: () => (
234
+ <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
235
+ <Button type="button" color="primary">Primary</Button>
236
+ <Button type="button" color="secondary">Secondary</Button>
237
+ <Button type="button" color="danger">Danger</Button>
238
+ <Button type="button" color="success">Success</Button>
239
+ <Button type="button" color="warning">Warning</Button>
240
+ </div>
241
+ ),
242
+ } as Story;
243
+
244
+ /**
245
+ * All variant styles side by side.
246
+ */
247
+ export const AllVariants: Story = {
248
+ render: () => (
249
+ <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap", alignItems: "center" }}>
250
+ <Button type="button" variant="outline">Outline</Button>
251
+ <Button type="button" variant="pill">Pill</Button>
252
+ <Button type="button" variant="text">Text</Button>
253
+ <Button type="button" color="primary" variant="outline">Primary Outline</Button>
254
+ <Button type="button" color="danger" variant="pill">Danger Pill</Button>
255
+ </div>
256
+ ),
257
+ } as Story;
258
+
112
259
  export const Custom: Story = {
113
260
  args: {
114
261
  styles: {
@@ -377,6 +524,8 @@ export const Customization: Story = {
377
524
  - \`--btn-size-sm\`: 0.8125rem (13px)
378
525
  - \`--btn-size-md\`: 0.9375rem (15px)
379
526
  - \`--btn-size-lg\`: 1.125rem (18px)
527
+ - \`--btn-size-xl\`: 1.375rem (22px)
528
+ - \`--btn-size-2xl\`: 1.75rem (28px)
380
529
 
381
530
  ### Base Properties
382
531
  - \`--btn-padding-inline\`: Horizontal padding (logical property)
@@ -79,6 +79,18 @@ describe("Button", () => {
79
79
  expect(handlePointerEvents).toHaveBeenCalledTimes(1);
80
80
  });
81
81
 
82
+ it("applies xl size via data-btn attribute", () => {
83
+ render(<Button type="button" size="xl">XL</Button>);
84
+ const button = screen.getByRole("button");
85
+ expect(button).toHaveAttribute("data-btn", "xl");
86
+ });
87
+
88
+ it("applies 2xl size via data-btn attribute", () => {
89
+ render(<Button type="button" size="2xl">2XL</Button>);
90
+ const button = screen.getByRole("button");
91
+ expect(button).toHaveAttribute("data-btn", "2xl");
92
+ });
93
+
82
94
  it("it is disabled when disabled is true", () => {
83
95
  const handleClick = jest.fn();
84
96
  render(
@@ -11,6 +11,39 @@ export type ButtonProps = Partial<React.ComponentProps<typeof UI>> &
11
11
  * Required - 'button' | 'submit' | 'reset'
12
12
  */
13
13
  type: "button" | "submit" | "reset";
14
+ /**
15
+ * Raw data-btn tokens. Merged with `size` and `block` — all three contribute
16
+ * whitespace-separated words to the final `data-btn` attribute value.
17
+ * @example <Button data-btn="pill">Pill button</Button>
18
+ */
19
+ "data-btn"?: string;
20
+ /**
21
+ * Size variant - maps to `data-btn` attribute, aligns with SCSS size tokens.
22
+ * Can coexist with a directly passed `data-btn` attribute (values are merged).
23
+ * @example <Button size="sm">Small</Button>
24
+ */
25
+ size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
26
+ /**
27
+ * Style variant - maps to `data-style` attribute.
28
+ * - `"outline"` — transparent bg with border (mirrors link button style)
29
+ * - `"pill"` — fully rounded corners
30
+ * - `"text"` — ghost text button with subtle hover
31
+ * - `"icon"` — square icon-only, no padding
32
+ * @example <Button variant="outline">Bordered</Button>
33
+ */
34
+ variant?: "text" | "pill" | "icon" | "outline";
35
+ /**
36
+ * Color variant - maps to `data-color` attribute using semantic color tokens.
37
+ * @example <Button color="danger">Delete</Button>
38
+ */
39
+ color?: "primary" | "secondary" | "danger" | "success" | "warning";
40
+ /**
41
+ * Block layout — stretches the button to 100% of its container width.
42
+ * Composes with `size` and other `data-btn` values.
43
+ * @example <Button block>Full Width</Button>
44
+ * @example <Button size="lg" block>Large Full Width</Button>
45
+ */
46
+ block?: boolean;
14
47
  };
15
48
 
16
49
  /**
@@ -73,6 +106,10 @@ export const Button = ({
73
106
  disabled,
74
107
  isDisabled,
75
108
  classes,
109
+ size,
110
+ variant,
111
+ color,
112
+ block,
76
113
  onPointerDown,
77
114
  onPointerOver,
78
115
  onPointerLeave,
@@ -96,30 +133,37 @@ export const Button = ({
96
133
  className: classes,
97
134
  // Note: onPointerOver and onPointerLeave are intentionally NOT wrapped
98
135
  // to allow hover effects on disabled buttons for visual feedback
99
- }
136
+ },
100
137
  );
101
138
 
139
+ // Merge size, block, and any explicit data-btn passed by the consumer.
140
+ // SCSS uses [data-btn~="value"] (whitespace word match), so "lg block" targets both.
141
+ const { "data-btn": dataBtnProp, ...restProps } = props;
142
+ const dataBtnValue =
143
+ [size, block ? "block" : undefined, dataBtnProp]
144
+ .filter(Boolean)
145
+ .join(" ") || undefined;
146
+
102
147
  /* Returning a button element with accessible disabled state */
103
148
  return (
104
149
  <UI
105
150
  as="button"
106
151
  type={type}
152
+ data-btn={dataBtnValue}
153
+ data-style={variant}
154
+ data-color={color}
107
155
  aria-disabled={disabledProps["aria-disabled"]}
108
156
  onPointerOver={onPointerOver}
109
157
  onPointerLeave={onPointerLeave}
110
158
  style={styles}
111
159
  className={disabledProps.className}
160
+ {...restProps}
112
161
  {...handlers}
113
- {...props}
114
162
  >
115
163
  {children}
116
164
  </UI>
117
165
  );
118
166
  };
119
167
 
120
- export const IconButton = ({ icon, ...props }: ButtonProps) => {
121
- return <Button {...props}>{icon}</Button>;
122
- };
123
-
124
168
  export default Button;
125
169
  Button.displayName = "Button";
@@ -0,0 +1,204 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="FP.React Components/Buttons/IconButton/Readme" />
4
+
5
+ # IconButton
6
+
7
+ ## Summary
8
+
9
+ `IconButton` is an accessible icon-button component that wraps the base `Button`
10
+ with enforced accessible labelling and icon-specific defaults.
11
+
12
+ It handles the two most common icon-button patterns:
13
+
14
+ - **Icon-only** — a square tap target with a screen-reader label
15
+ - **Icon + label** — icon with visible text that hides on narrow viewports
16
+
17
+ `IconButton` extends all `Button` props (`size`, `variant`, `color`, `disabled`,
18
+ etc.) and enforces one critical constraint at the TypeScript level: exactly one
19
+ of `aria-label` or `aria-labelledby` must be present — passing both or neither
20
+ is a compile-time error.
21
+
22
+ ---
23
+
24
+ ## Props
25
+
26
+ | Prop | Type | Default | Description |
27
+ | ----------------- | -------------------------------------------------------------------- | ---------- | -------------------------------------------------------------------------------------------- |
28
+ | `icon` | `React.ReactNode` | — | **Required.** The icon element rendered inside the button. |
29
+ | `aria-label` | `string` | — | Accessible name for icon-only buttons. XOR with `aria-labelledby`. |
30
+ | `aria-labelledby` | `string` | — | References an external element's `id` as the accessible name. XOR with `aria-label`. |
31
+ | `label` | `string` | — | Optional label text alongside the icon. Hidden below `48rem` (768px); always in the accessibility tree.|
32
+ | `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | Button type attribute. Required. |
33
+ | `variant` | `'icon' \| 'outline' \| 'text' \| 'pill'` | `'icon'` | Style variant. Default `'icon'` gives a transparent, square, no-padding button. |
34
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'` | — | Size token. Scales font size and height via `--btn-fs`. |
35
+ | `color` | `'primary' \| 'secondary' \| 'danger' \| 'success' \| 'warning'` | — | Semantic color token. Sets `--btn-bg` and `--btn-color` via `data-color`. |
36
+ | `disabled` | `boolean` | `false` | Disables the button using WCAG-compliant `aria-disabled` (stays keyboard-focusable). |
37
+ | `onClick` | `(e: React.MouseEvent<HTMLButtonElement>) => void` | — | Click handler. |
38
+ | `styles` | `React.CSSProperties` | — | Inline styles — use to override CSS custom properties e.g. `--btn-color: red`. |
39
+ | `classes` | `string` | — | Additional CSS classes appended to the button element. |
40
+
41
+ ---
42
+
43
+ ## Sizing — 3rem Fixed Tap Target
44
+
45
+ The default `variant="icon"` applies a fixed `width: 3rem; height: 3rem` to
46
+ every `IconButton` instance:
47
+
48
+ ```css
49
+ button[data-icon-btn] {
50
+ width: 3rem; /* 48px at default font size */
51
+ height: 3rem; /* 48px at default font size */
52
+ }
53
+ ```
54
+
55
+ `3rem` equals **48px** at the browser's default 16px root font size, meeting
56
+ the WCAG 2.5.5 (AAA) minimum target size recommendation of 44×44 CSS pixels.
57
+ The fixed size ensures a consistent, touchable target regardless of icon size
58
+ or parent context.
59
+
60
+ When a `label` is present (the `has-label` variant), width becomes `max-content`
61
+ and padding is restored to `0.75rem` inline, but the `3rem` height is preserved
62
+ for a consistent touch target.
63
+
64
+ ---
65
+
66
+ ## Label Visibility
67
+
68
+ When `label` is provided, the text is rendered in a `<span data-icon-label>`.
69
+ Visibility is controlled entirely by CSS — no prop required:
70
+
71
+ - **Below `48rem` (768px):** the span is visually hidden via the visually-hidden
72
+ technique applied by the `[data-icon-label]` media query in `icon-button.scss`.
73
+ The span remains in the DOM and the accessibility tree — screen readers
74
+ announce the label at every viewport size.
75
+ - **At `48rem` and above:** the label is fully visible alongside the icon.
76
+
77
+ The breakpoint is a SCSS variable (not a CSS custom property) because media
78
+ query conditions are evaluated at parse time and cannot reference runtime values:
79
+
80
+ ```scss
81
+ $icon-label-bp: 48rem !default; // Override before import to customise
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Usage Examples
87
+
88
+ ### Icon-only button
89
+
90
+ Requires `aria-label`. The label is only announced by screen readers — not
91
+ visible on screen.
92
+
93
+ ```tsx
94
+ import { IconButton } from "@fpkit/acss";
95
+
96
+ <IconButton
97
+ type="button"
98
+ aria-label="Close menu"
99
+ icon={<CloseIcon />}
100
+ />
101
+ ```
102
+
103
+ ### Icon + visible label
104
+
105
+ Use `variant="outline"` (or any non-`icon` variant) to restore padding alongside
106
+ the label. The default `variant="icon"` sets `padding: 0`, which collapses the
107
+ layout around a label. The label hides automatically on mobile (< 48rem) via CSS.
108
+
109
+ ```tsx
110
+ <IconButton
111
+ type="button"
112
+ aria-label="Settings"
113
+ icon={<SettingsIcon />}
114
+ label="Settings"
115
+ variant="outline"
116
+ />
117
+ ```
118
+
119
+ ### Labelled by external element
120
+
121
+ Use `aria-labelledby` to reference an existing label element in the DOM. Useful
122
+ when the button sits next to a heading or description that already names the action.
123
+
124
+ ```tsx
125
+ <span id="delete-label">Delete item</span>
126
+ <IconButton
127
+ type="button"
128
+ aria-labelledby="delete-label"
129
+ icon={<TrashIcon />}
130
+ />
131
+ ```
132
+
133
+ ### Semantic color variants
134
+
135
+ Color tokens apply to the icon via `currentColor` — the icon's `stroke` or
136
+ `fill` inherits the button's `--btn-color`.
137
+
138
+ ```tsx
139
+ <IconButton type="button" aria-label="Delete" icon={<TrashIcon />} color="danger" />
140
+ <IconButton type="button" aria-label="Confirm" icon={<CheckIcon />} color="success" />
141
+ <IconButton type="button" aria-label="Settings" icon={<GearIcon />} color="primary" variant="outline" />
142
+ ```
143
+
144
+ ### Disabled state
145
+
146
+ Uses the WCAG-compliant `aria-disabled` pattern — the button remains focusable
147
+ and visible in the tab order so keyboard users can discover it and read any
148
+ associated tooltip or help text.
149
+
150
+ ```tsx
151
+ <IconButton
152
+ type="button"
153
+ aria-label="Upload (unavailable)"
154
+ icon={<UploadIcon />}
155
+ disabled
156
+ />
157
+ ```
158
+
159
+ ### Custom color via CSS custom property
160
+
161
+ ```tsx
162
+ <IconButton
163
+ type="button"
164
+ aria-label="Star"
165
+ icon={<StarIcon />}
166
+ styles={{ "--btn-color": "#f59e0b" }}
167
+ />
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Accessibility Notes
173
+
174
+ - **`aria-label` is required** for icon-only buttons. An icon with no label
175
+ gives screen reader users no indication of the button's purpose.
176
+ - **XOR enforcement** — the TypeScript type allows exactly one of `aria-label`
177
+ or `aria-labelledby`. Passing both or neither is a compile-time error.
178
+ - **`label` does not replace `aria-label`** — even when a visible `label` is
179
+ provided, you must still pass `aria-label` (or `aria-labelledby`). The `label`
180
+ prop controls visual presentation only; `aria-label` provides the computed
181
+ accessible name.
182
+ - **`disabled` keeps focus** — `IconButton` uses `aria-disabled="true"` instead
183
+ of the native `disabled` attribute. The button stays in the tab order and can
184
+ receive focus, satisfying WCAG 2.1.1 (Keyboard) and 4.1.2 (Name, Role, Value).
185
+ - **Icon `aria-hidden`** — always render icons with `aria-hidden="true"` so
186
+ screen readers do not double-announce both the SVG content and the button label.
187
+
188
+ ```tsx
189
+ // Correct — icon is decorative, label carries the meaning
190
+ <IconButton
191
+ type="button"
192
+ aria-label="Close"
193
+ icon={<svg aria-hidden="true">...</svg>}
194
+ />
195
+ ```
196
+
197
+ ---
198
+
199
+ ## Related
200
+
201
+ - [Button README](./README.mdx) — base `Button` props and variants
202
+ - [Button Styles](./STYLES.mdx) — full CSS custom property reference
203
+ - [WCAG 2.5.5 Target Size](https://www.w3.org/WAI/WCAG21/Understanding/target-size.html)
204
+ - [WCAG 2.4.1 Bypass Blocks](https://www.w3.org/WAI/WCAG21/Understanding/bypass-blocks.html)