| | |

React Hook Form 06 ๐Ÿ“‹ Built-in Validation Rules โ€” required, minLength, pattern, and more

โฑ๏ธ Estimated read: 14 min | |

React Hook Form’s built-in validation rules are the constraints you pass to the register call or to the Controller component. They run through the library’s own validation engine, not through the browser’s Constraint Validation API, which means the error messages are React state, the errors render inside the component, and the validation runs on the values that the DOM holds. The rules are the first layer of validation, and the resolver is the second layer: a project can use the built-in rules alone, or the resolver alone, or both, and the two are not mutually exclusive in the configuration even though the resolver takes precedence when both are present .

The rules map closely to the HTML constraints, but the behaviour differs in ways that matter. The required rule checks the presence of the value; the minLength and maxLength apply to the string length; the min and max apply to the numeric or date value; the pattern applies a regular expression to the string; the validate accepts a custom function or an object of functions. Each rule returns a boolean, and when it returns false the field becomes invalid and the rule’s message is stored in the formState.errors under the field’s name. The messages are the mechanism the UI uses to display the error, and the message property of the rule object is what the UI reads.

Key point: The built-in rules are passed to the register function’s second argument or to the rules prop of the Controller. Each rule can be a boolean, a value, or an object with a value and a message. The rule runs when the validation triggers, and the field becomes invalid when the rule fails. The formState.errors object holds the error message under the field’s path, and the error’s type property tells which rule failed.


Why the built-in rules exist

The resolver problem. A project that uses a schema resolver for the complex validation does not need the built-in rules, because the resolver validates the entire form data against the schema. But a project that does not want to add a schema library for a simple login form would have to write the validation by hand, and the hand-written validation would duplicate the same checks in every form. The built-in rules are the lightweight alternative: they cover the common cases without the dependency, and they store the errors in the same formState.errors object that the UI already reads . The two layers coexist, and the choice is the project’s.

The error-state problem. A hand-written validation would store the error messages in a useState, and the component would re-render on every keystroke when the validation ran. The built-in rules store the errors in the internal form state, and the formState proxy re-renders only the components that read the changed error properties . The built-in rules are the mechanism that keeps the validation off the component’s state and on the form’s state, and the proxy is the mechanism that keeps the re-renders proportional to the changes.

The message problem. Each rule accepts the message in the message property of the rule object. The message is the string the UI displays, and it can be a template with the {{value}} placeholder for the rule’s value. The message is the mechanism that keeps the error text out of the component’s render logic, and it is the mechanism that lets the same rule produce the different messages in the different forms.

The browser problem. The required attribute, the minLength attribute, and the pattern attribute on the native input are enforced by the browser before the JavaScript runs. The built-in rules duplicate these constraints, and the two can conflict: the browser’s native validation can block the form submission before the React Hook Form’s handleSubmit runs, and the user sees the browser’s tooltip instead of the component’s error message. The noValidate attribute on the <form> is the mechanism that disables the browser’s native validation and lets the built-in rules own the validation . The progressive option on the register is the mechanism that keeps the native attributes for the server-side rendering, while the noValidate is the mechanism for the client-side validation.


a. The presence and the length rules

The required rule is the presence check. The rule accepts a boolean, a string, or an object with a value and a message. The string form is the shorthand for { value: true, message: "..." }. The value can be a boolean or a string; the string form is the mechanism that requires a non-empty string and the boolean form is the mechanism that requires the value to be present.

// The required rule with the string message
<input
  {...register("email", {
    required: "The email is required.",
  })}
/>

// The required rule with the object form
<input
  {...register("email", {
    required: {
      value: true,
      message: "The email is required.",
    },
  })}
/>

// The required rule with the string value
<input
  {...register("confirm", {
    required: {
      value: "yes",
      message: "The confirm must be 'yes'.",
    },
  })}
/>

The string value is the mechanism that requires an exact match, and the boolean value is the mechanism that requires any non-empty value. The difference matters for the checkbox: a checkbox with the boolean required is valid when it is checked, and a checkbox with the required: "on" is valid when its value is the string “on”. The value property is the mechanism that the rule uses to decide whether the field is present, and the message property is the mechanism that the UI displays.

The minLength and the maxLength rules apply to the string length. The rule accepts a number or an object with a value and a message. The rule checks the value.length of the field, and the minLength fails when the length is less than the value, and the maxLength fails when the length is greater than the value .

// The length rules
<input
  {...register("username", {
    minLength: {
      value: 3,
      message: "The username must be at least 3 characters.",
    },
    maxLength: {
      value: 20,
      message: "The username must be at most 20 characters.",
    },
  })}
/>

The length rules apply to the string fields and the array fields. The minLength on the array field checks the number of the elements, and the maxLength checks the maximum. The rule is the mechanism that constrains the length, and the message is the mechanism that explains the constraint to the user.

The min and the max rules apply to the numeric or the date value. The rule accepts a number or a date, and the field’s value is compared against it. The valueAsNumber and the valueAsDate on the register are the mechanism that converts the string to the number or the date before the rule runs.

// The numeric range rules
<input
  type="number"
  {...register("age", {
    valueAsNumber: true,
    min: {
      value: 18,
      message: "The age must be at least 18.",
    },
    max: {
      value: 120,
      message: "The age must be at most 120.",
    },
  })}
/>

The min and the max rules on a string field compare the strings lexicographically, which is rarely what the developer wants. The valueAsNumber is the mechanism that makes the rule compare the numbers, and it is the reason the numeric field needs the conversion before the rule runs.


b. The pattern and the custom validation

The pattern rule applies a regular expression to the string value. The rule accepts a RegExp or an object with a value and a message. The value is the regular expression, and the message is the error text. The rule fails when the value does not match the pattern.

// The pattern rule
<input
  {...register("email", {
    pattern: {
      value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
      message: "The email is invalid.",
    },
  })}
/>

// The pattern rule with the RegExp shorthand
<input
  {...register("zip", {
    pattern: /^\d{5}$/,
  })}
/>

The pattern rule is the mechanism that enforces the format, and the message is the mechanism that explains the format. The rule is the string check, and the regex is the constraint. The type: "email" on the input is the browser-level check, and the pattern is the library-level check; the two are the different layers, and the noValidate on the form is the mechanism that lets the library-level check own the validation.

The validate rule is the custom validation. The rule accepts a function or an object of the functions. The function receives the field value and the form values, and it returns true or undefined for a valid field, or a string for an invalid field with the message.

// The custom validation function
<input
  {...register("username", {
    validate: (value) => {
      if (value.length < 3) return "The username is too short.";
      if (value === "admin") return "The username is reserved.";
      return true;
    },
  })}
/>

The validate object form is the mechanism for the multiple custom rules on the same field. Each key is a rule name, and each value is the function. The error message is the first failing rule’s message, and the formState.errors.username.type is the rule name.

// The custom validation object
<input
  {...register("password", {
    validate: {
      hasUpperCase: (value) =>
        /[A-Z]/.test(value) || "The password must contain an uppercase letter.",
      hasNumber: (value) =>
        /\d/.test(value) || "The password must contain a number.",
    },
  })}
/>

The custom validation is the mechanism that covers the rules the built-in rules cannot express. The password confirmation, the username availability, the date range, and the other business rules are the custom functions, and the function is the escape hatch for the cases the built-in rules do not cover.


Complete Example Session

// ============================================
// PART 1: THE BASIC REGISTRATION
// ============================================

import { useForm } from "react-hook-form";

function SignUpForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    defaultValues: { email: "", password: "", age: "" },
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))} noValidate>
      <input {...register("email")} />
      <input {...register("password")} type="password" />
      <input {...register("age")} type="number" />
    </form>
  );
}

// ============================================
// PART 2: THE REQUIRED RULE
// ============================================

<input
  {...register("email", {
    required: "The email is required.",
  })}
/>

// The string form is the message.
// The value defaults to true.

// ============================================
// PART 3: THE REQUIRED OBJECT FORM
// ============================================

<input
  {...register("email", {
    required: {
      value: true,
      message: "The email is required.",
    },
  })}
/>

// The object form is the explicit value and message.

// ============================================
// PART 4: THE MIN LENGTH
// ============================================

<input
  {...register("username", {
    minLength: {
      value: 3,
      message: "The username must be at least 3 characters.",
    },
  })}
/>

// The rule checks the value.length.

// ============================================
// PART 5: THE MAX LENGTH
// ============================================

<input
  {...register("username", {
    maxLength: {
      value: 20,
      message: "The username must be at most 20 characters.",
    },
  })}
/>

// The rule checks the maximum length.

// ============================================
// PART 6: THE MIN AND MAX
// ============================================

<input
  type="number"
  {...register("age", {
    valueAsNumber: true,
    min: {
      value: 18,
      message: "The age must be at least 18.",
    },
    max: {
      value: 120,
      message: "The age must be at most 120.",
    },
  })}
/>

// The valueAsNumber is the conversion before the rule.

// ============================================
// PART 7: THE PATTERN RULE
// ============================================

<input
  {...register("email", {
    pattern: {
      value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
      message: "The email is invalid.",
    },
  })}
/>

// The pattern is the regex check.

// ============================================
// PART 8: THE CUSTOM VALIDATION
// ============================================

<input
  {...register("username", {
    validate: (value) => {
      if (value === "admin") return "The username is reserved.";
      return true;
    },
  })}
/>

// The validate is the custom function.

// ============================================
// PART 9: THE MULTIPLE CUSTOM RULES
// ============================================

<input
  {...register("password", {
    validate: {
      hasUpperCase: (value) =>
        /[A-Z]/.test(value) || "Must contain an uppercase letter.",
      hasNumber: (value) =>
        /\d/.test(value) || "Must contain a number.",
    },
  })}
/>

// The object form is the multiple rules.

// ============================================
// PART 10: THE CHECKLIST
// ============================================

// [ ] The form has the noValidate attribute.
// [ ] The required rule has the message.
// [ ] The length rules have the value and the message.
// [ ] The numeric rules have the valueAsNumber.
// [ ] The pattern rule has the RegExp and the message.
// [ ] The custom validation returns the string or the true.

The ten parts covered the basic registration, the required rule, the required object form, the min length, the max length, the min and max, the pattern rule, the custom validation, the multiple custom rules, and the checklist. The through-line is that the rules are the register options, and the errors are the form state.


Quick Reference

The built-in rules

RuleApplies toValue type
requiredAnyboolean | string
minLengthString, arraynumber
maxLengthString, arraynumber
minNumber, datenumber | Date
maxNumber, datenumber | Date
patternStringRegExp
validateAnyfunction | object

The rule object shape

ShapeExamplePurpose
The stringrequired: "Message"The message only
The numbermin: 18The value only
The objectmin: { value: 18, message: "..." }The value and the message

The error object

PropertyPurpose
typeThe failed rule name
messageThe error message
refThe DOM element

The form attributes

AttributePurpose
noValidateDisable the browser validation
progressiveKeep the native attributes

Best Practices

โœ… Do This:

// Use the noValidate attribute on the form
<form noValidate onSubmit={handleSubmit(onSubmit)}>                // โœ…

// Use the message in the rule object
register("email", { required: "Required" })                       // โœ…

// Use the valueAsNumber for the numeric rules
register("age", { valueAsNumber: true, min: 18 })                 // โœ…

// Use the object form for the multiple custom rules
register("password", { validate: { hasNumber: fn, hasUpper: fn } }) // โœ…

โŒ Don’t Do This:

// Rely on the browser validation
<input required /> // the browser tooltip instead of the RHF error // โŒ

// Compare the strings with min/max
register("age", { min: 18 }) // the string comparison             // โŒ

// Throw inside the custom validation
validate: (v) => { if (!v) throw new Error() } // the form crashes // โŒ

Common Pitfalls

PitfallWhy It HappensFix
The error message does not appearThe noValidate is missingAdd the noValidate to the form
The numeric rule does not workThe valueAsNumber is missingAdd the valueAsNumber: true
The custom validation does not runThe rule is in the wrong placePut it in the register options
The multiple custom rules report oneThe object form is not usedUse the validate: { name: fn } form

Real-World Examples

1. The required rule

register("email", { required: "Required" })

2. The min length

register("username", { minLength: 3 })

3. The max length

register("username", { maxLength: 20 })

4. The min number

register("age", { valueAsNumber: true, min: 18 })

5. The max number

register("age", { valueAsNumber: true, max: 120 })

6. The pattern

register("email", { pattern: /^\S+@\S+$/i })

7. The custom validation

register("username", { validate: (v) => v !== "admin" || "Reserved" })

8. The multiple custom rules

register("password", { validate: { hasNumber: fn, hasUpper: fn } })

9. The error message

{errors.email && <p>{errors.email.message}</p>}

10. The noValidate

<form noValidate onSubmit={handleSubmit(onSubmit)}>

Visual

The Rule Object Shapes

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  THE RULE OBJECT SHAPES                                      โ”‚
โ”‚                                                              โ”‚
โ”‚  The string:                                                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ required: "Message" โ”‚  The message only                  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚                                                              โ”‚
โ”‚  The number:                                                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ min: 18             โ”‚  The value only                    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚                                                              โ”‚
โ”‚  The object:                                                 โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ min: {              โ”‚  The value and the message         โ”‚
โ”‚  โ”‚   value: 18,        โ”‚                                    โ”‚
โ”‚  โ”‚   message: "..."    โ”‚                                    โ”‚
โ”‚  โ”‚ }                   โ”‚                                    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Validation Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  THE VALIDATION FLOW                                         โ”‚
โ”‚                                                              โ”‚
โ”‚  The user submits                                            โ”‚
โ”‚         โ”‚                                                    โ”‚
โ”‚         โ–ผ                                                    โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ The handleSubmit    โ”‚  The validation runs               โ”‚
โ”‚  โ”‚ runs the rules      โ”‚                                    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚             โ”‚                                                โ”‚
โ”‚      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”                                         โ”‚
โ”‚      โ–ผ             โ–ผ                                         โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ Valid  โ”‚   โ”‚Invalid โ”‚                                    โ”‚
โ”‚  โ”‚        โ”‚   โ”‚        โ”‚                                    โ”‚
โ”‚  โ”‚onSubmitโ”‚   โ”‚errors  โ”‚                                    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The noValidate

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  THE noValidate DISABLES THE BROWSER VALIDATION              โ”‚
โ”‚                                                              โ”‚
โ”‚  Without the noValidate:                                     โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ The browser blocks  โ”‚  The tooltip appears               โ”‚
โ”‚  โ”‚ the submission      โ”‚  The RHF errors do not show        โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ”‚                                                              โ”‚
โ”‚  With the noValidate:                                        โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                    โ”‚
โ”‚  โ”‚ The RHF handles     โ”‚  The errors render                 โ”‚
โ”‚  โ”‚ the validation      โ”‚  inside the component              โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
The built-in rulesrequired, minLength, maxLength, min, max, pattern, validate
The rule objectThe string, the number, or the { value, message } object
The custom validationThe function or the object of the functions
The error storageThe formState.errors
The browser validationThe noValidate attribute
The numeric conversionThe valueAsNumber

Key takeaways:

  • The built-in rules are the register options. The required, the minLength, the maxLength, the min, the max, and the pattern are the built-in rules, and the validate is the custom rule . The rules run through the library’s validation, not the browser’s.
  • The rule object can be a string, a number, or an object. The string is the message, the number is the value, and the object is the { value, message } pair . The shorthand is the mechanism that keeps the common cases short.
  • The validate rule is the custom validation. The function returns true or undefined for a valid field, or a string for an invalid field. The object form is the mechanism for the multiple custom rules on the same field.
  • The noValidate attribute disables the browser’s native validation. Without it, the browser’s validation can block the submission before the handleSubmit runs, and the user sees the browser’s tooltip instead of the component’s error message .
  • The valueAsNumber and the valueAsDate are the conversion before the rules. The min and the max compare the converted values, and the conversion is the mechanism that makes the numeric and the date rules work.

Remember: The React Hook Form built-in rules are the register options. The required, the minLength, the maxLength, the min, the max, and the pattern are the rules, and the validate is the custom escape. The rules store the errors in the formState.errors, and the noValidate attribute keeps the browser’s validation from competing. The rule object can be a string, a number, or the { value, message } pair.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!