Search code examples
cssreactjsstyled-components

Event triggered styled component css change


I'm only now switching to styled component and could not find an answer for this fairly simple logic: if the validation for a field fails, it should have red borders.

With classic CSS classes:

const validateMail = (e) => {
    let mail = e.target.value;
    if (!mailRegex.test(mail)) {
        e.target.classList.add("error");
        return;
    }
    e.target.classList.remove("error");
}

How can I do something similar to e.target.classList.add("error")? I assume I would have to, somehow, change this list to something that changes the style of the field to begin with. Here's the complete class of the input styled component that triggers this function:

const Field = styled.div`
    display: flex;
    flex-direction: column;
`;

const Label = styled.label`
    font: ${fonts.body1};
    font-size: .75rem;
`;

const Input = styled.input`
    font: ${fonts.body1};
    padding: 10px;
    border-radius: 5px;
    outline: none;
    border: 1px solid rgba(0, 0, 0, 0.3);
    width: 100%;
    resize: none;
    box-sizing: border-box;
    &:focus {
        border: 1px solid ${palette.primary_blue};
    }
`;

function InputField(props) {
    return (
        <Field>
            <Label htmlFor={"txt" + props.name}>{props.name + ":"}</Label>
            <Input name={"txt" + props.name} onKeyUp={props.onKeyUp}></Input>
        </Field>
    );
}

export default InputField;

Solution

  • As you've identified, styled components don't follow the classic class list paradigm. There are several ways to achieve the result you're after. I've demonstrated 2.

    Approach 1: Pass a prop to the styled component.

    You can pass a custom isValid prop to the styled component like this:

    <Input
      isValid={isValid}
      name={"txt" + props.name}
      onKeyUp={props.onKeyUp}
    />
    

    You can then add conditional rendering logic inside the styled component based on how the prop is evaluated.

    Note that the following arrow function in the styled component definition (in place of styled.input) ensures that the prop is treated as a component prop and not as an actual DOM element attribute (which would not be valid markup and could cause errors):

    const Input = styled(({
      isValid,
      ...props
    }) => <input {...props} />)`
      border: 1px solid ${({ isValid }) =>
      isValid ? `rgba(0, 0, 0, 0.3)` : `red`)};
    
      /* ... the rest of your styles... */
    `;
    

    For a live example of the above, see this Code Sandbox. Note line 41 where isValid is being set to false as a stand-in for actual validation logic. Try changing it to true and you'll see that the border color changes.


    Approach 2: Create 2 separate styled components for valid and error states:

    const Input = styled.input`
      border: 1px solid rgba(0, 0, 0, 0.3);
    
      /* ... the rest of your styles... */
    `;
    
    const ErrorInput = styled(Input)`
      border-color: red;
    `;
    
    const StyledInput = isValid ? Input : ErrorInput;
    
    return (
      <Field>
        <Label htmlFor={"txt" + props.name}>{props.name + ":"}</Label>
        <StyledInput
          name={"txt" + props.name}
          onKeyUp={props.onKeyUp}
        />
      </Field>
    );
    

    Here's a Code Sandbox of this approach. Same as with the first approach, see line 45 for hard-coded isValid value.

    Note about transitions

    I prefer the first solution because it allows for smooth transitions between changing values (like with classes). The second approach works fine for static rendered elements, but it will basically destroy/create a new element on the fly when the component reference changes from Input to ErrorInput. If you want smooth transitions then you should use the first approach.