React MyUI - Input

1. Design

Flexibility Through Props

CSS Variable System

The component uses CSS variables for theming:

1

2. Implementation (css and tsx)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
.myui-input-wrapper {
display: inline-flex;
flex-direction: column;
margin-bottom: 1rem;
}

.myui-input-wrapper--full-width {
width: 100%;
}

.myui-input__label {
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.25rem;
color: var(--color-text);
}

.myui-input__container {
position: relative;
display: flex;
align-items: center;
}

.myui-input {
width: 100%;
font-family: inherit;
font-size: 1rem;
line-height: 1.5;
color: var(--color-text);
background-color: var(--color-background);
border: 1px solid var(--color-border);
border-radius: 4px;
outline: none;
transition: all 0.2s ease-in-out;
}

.myui-input:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 2px rgba(var(--color-primary-rgb, 52, 152, 219), 0.25);
}

.myui-input:disabled {
opacity: 0.6;
cursor: not-allowed;
background-color: color-mix(
in srgb,
var(--color-border) 20%,
var(--color-background)
);
}

/* Variants */
.myui-input--outlined {
background-color: var(--color-background);
border: 1px solid var(--color-border);
}

.myui-input--filled {
background-color: color-mix(
in srgb,
var(--color-border) 15%,
var(--color-background)
);
border: 1px solid transparent;
}

.myui-input--filled:focus {
background-color: color-mix(
in srgb,
var(--color-border) 10%,
var(--color-background)
);
}

.myui-input--standard {
background-color: transparent;
border-width: 0;
border-bottom-width: 1px;
border-radius: 0;
}

.myui-input--standard:focus {
box-shadow: none;
}

/* Sizes */
.myui-input--small {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}

.myui-input--medium {
padding: 0.5rem 0.75rem;
font-size: 1rem;
}

.myui-input--large {
padding: 0.75rem 1rem;
font-size: 1.125rem;
}

/* Error state */
.myui-input--error {
border-color: var(--color-accent) !important;
}

.myui-input--error:focus {
box-shadow: 0 0 0 2px rgba(var(--color-accent-rgb, 231, 76, 60), 0.25) !important;
}

.myui-input__helper-text {
margin-top: 0.25rem;
font-size: 0.75rem;
color: color-mix(in srgb, var(--color-text) 70%, var(--color-background));
}

.myui-input__helper-text--error {
color: var(--color-accent);
}

/* Icons */
.myui-input__icon {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
color: color-mix(in srgb, var(--color-text) 60%, var(--color-background));
pointer-events: none;
}

.myui-input__icon--start {
left: 0.75rem;
}

.myui-input__icon--end {
right: 0.75rem;
}

/* Adjust padding when icons are present */
.myui-input__icon--start ~ .myui-input {
padding-left: 2.5rem;
}

.myui-input__icon--end ~ .myui-input {
padding-right: 2.5rem;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import React, { InputHTMLAttributes, forwardRef } from 'react';
import './Input.css';

export type InputSize = 'small' | 'medium' | 'large';
export type InputVariant = 'outlined' | 'filled' | 'standard';

export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
/**
* Input label
*/
label?: string;

/**
* Input size
* @default 'medium'
*/
size?: InputSize;

/**
* Input variant
* @default 'outlined'
*/
variant?: InputVariant;

/**
* Error message
*/
error?: string;

/**
* Helper text
*/
helperText?: string;

/**
* Icon to display at the start of input
*/
startIcon?: React.ReactNode;

/**
* Icon to display at the end of input
*/
endIcon?: React.ReactNode;

/**
* Is input full width
* @default false
*/
fullWidth?: boolean;
}

export const Input = forwardRef<HTMLInputElement, InputProps>(({
label,
size = 'medium',
variant = 'outlined',
error,
helperText,
startIcon,
endIcon,
fullWidth = false,
className = '',
id,
...rest
}, ref) => {
// Generate unique ID if not provided
const inputId = id || `myui-input-${Math.random().toString(36).slice(2, 11)}`;

const inputClasses = [
'myui-input',
`myui-input--${variant}`,
`myui-input--${size}`,
error ? 'myui-input--error' : '',
fullWidth ? 'myui-input--full-width' : '',
className
].filter(Boolean).join(' ');

const wrapperClasses = [
'myui-input-wrapper',
fullWidth ? 'myui-input-wrapper--full-width' : '',
].filter(Boolean).join(' ');

return (
<div className={wrapperClasses}>
{label && (
<label htmlFor={inputId} className="myui-input__label">
{label}
</label>
)}

<div className="myui-input__container">
{startIcon && (
<div className="myui-input__icon myui-input__icon--start">
{startIcon}
</div>
)}

<input
ref={ref}
id={inputId}
className={inputClasses}
aria-invalid={!!error}
{...rest}
/>

{endIcon && (
<div className="myui-input__icon myui-input__icon--end">
{endIcon}
</div>
)}
</div>

{(error || helperText) && (
<div className={`myui-input__helper-text ${error ? 'myui-input__helper-text--error' : ''}`}>
{error || helperText}
</div>
)}
</div>
);
});

Input.displayName = 'Input';

export default Input;

3. Export Config

package/core/src/components/Input/index.ts

1
2
export * from './Input'
export { default } from './Input'

packages/core/src/index.ts
Add

1
export * from './components/Input';

4. StoryBook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import React from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { Input } from '@hardyhu-ui/core';

const meta: Meta<typeof Input> = {
title: 'Components/Input',
component: Input,
argTypes: {
variant: {
control: { type: 'select' },
options: ['outlined', 'filled', 'standard'],
description: 'Input variant style',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'outlined' },
},
},
size: {
control: { type: 'select' },
options: ['small', 'medium', 'large'],
description: 'Input size',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'medium' },
},
},
label: {
control: 'text',
description: 'Input label',
},
placeholder: {
control: 'text',
description: 'Input placeholder',
},
disabled: {
control: 'boolean',
description: 'Disables the input',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false' },
},
},
error: {
control: 'text',
description: 'Error message',
},
helperText: {
control: 'text',
description: 'Helper text',
},
fullWidth: {
control: 'boolean',
description: 'Makes input full width',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'false' },
},
},
onChange: { action: 'changed' },
},
parameters: {
docs: {
description: {
component: 'A customizable input component with different variants and sizes.',
},
},
},
};

export default meta;
type Story = StoryObj<typeof Input>;

export const Default: Story = {
args: {
label: 'Default Input',
placeholder: 'Enter text...',
},
};

export const Outlined: Story = {
args: {
label: 'Outlined Input',
placeholder: 'Enter text...',
variant: 'outlined',
},
};

export const Filled: Story = {
args: {
label: 'Filled Input',
placeholder: 'Enter text...',
variant: 'filled',
},
};

export const Standard: Story = {
args: {
label: 'Standard Input',
placeholder: 'Enter text...',
variant: 'standard',
},
};

export const Sizes: Story = {
render: () => (
<div style={{ display: 'flex', gap: '1rem', flexDirection: 'column' }}>
<Input size="small" label="Small Input" placeholder="Small..." />
<Input size="medium" label="Medium Input" placeholder="Medium..." />
<Input size="large" label="Large Input" placeholder="Large..." />
</div>
),
};

export const WithError: Story = {
args: {
label: 'Email',
placeholder: 'Enter your email',
error: 'Please enter a valid email address',
},
};

export const WithHelperText: Story = {
args: {
label: 'Password',
type: 'password',
placeholder: 'Enter your password',
helperText: 'Password must be at least 8 characters',
},
};

export const Disabled: Story = {
args: {
label: 'Disabled Input',
placeholder: 'You cannot edit this field',
disabled: true,
},
};

export const FullWidth: Story = {
args: {
label: 'Full Width Input',
placeholder: 'This input takes up the full width',
fullWidth: true,
},
};

export const WithIcons: Story = {
render: () => (
<div style={{ display: 'flex', gap: '1rem', flexDirection: 'column' }}>
<Input
label="Search"
placeholder="Search..."
startIcon={<span style={{ fontSize: '1.2em' }}>🔍</span>}
/>
<Input
label="Password"
type="password"
placeholder="Enter password"
endIcon={<span style={{ fontSize: '1.2em' }}>👁️</span>}
/>
<Input
label="Location"
placeholder="Enter your location"
startIcon={<span style={{ fontSize: '1.2em' }}>📍</span>}
endIcon={<span style={{ fontSize: '1.2em' }}>📌</span>}
/>
</div>
),
};

5. Jest

1


React MyUI - Input
https://www.hardyhu.cn/2025/02/15/React-MyUI-Input/
Author
John Doe
Posted on
February 15, 2025
Licensed under