Composant Circle Choice ReactJS

Découvrez comment créer un composant React innovant pour remplacer select/radio, avec React-hook-form, Tailwind, et NextJS. Intuitif et personnalisable ! 🚀
Hey les développeurs et développeuses, 👋
Aujourd'hui nous allons développer un composant React pour remplacer un composant select ou radio.
Dernièrement je vous parlais d'un composant ReactJS que j'ai designé de manière à rendre plus pratique un choix multiple de 3, 4 ou 5 items:
Surtout dans une situation où on l'on peut mettre une icone ou une image pour un choix, cela rend les choses beaucoup plus simple à visualiser.
Pour réaliser ce composant, j'ai utilisé React, React-hook-form, Tailwind sur NextJS 14.
Composants
Je commence par créer un composant CircleButton.tsx:
export type CircleButtonProps = {
children: React.ReactNode;
onClick: () => void;
}
export const CircleButton = ({ children, onClick }: CircleButtonProps) => {
return (
<div
className="w-28 h-28 bg-white border border-gray-200 rounded-full shadow dark:bg-gray-800 dark:border-gray-700 flex flex-col items-center justify-center cursor-pointer hover:bg-gray-300"
onClick={onClick}
>
{children}
</div>
);
};
Ensuite, je crée un composant CircleChoice.tsx:
import { useState } from "react";
import { Control, Controller } from "react-hook-form";
import { CircleButton } from "./circleButton";
import cn from "classnames";
import styles from "./CircleChoice.module.css"
type Choice = {
value: string;
content: React.ReactNode;
};
export type CircleChoiceProps<T> = {
fieldTitle: string;
fieldName: string;
control: Control<any>;
choices: Choice[];
};
export const CircleChoice = <T,>({ fieldName, control, fieldTitle, choices }: CircleChoiceProps<T>) => {
const [isOpened, setIsOpened] = useState(false);
const radius = 120;
return (
<div className="mb-6 relative flex flex-col items-center justify-center">
<Controller
control={control}
name={fieldName}
render={({ field }) => (
<>
<div
className="fixed top-0 left-0 w-full h-full bg-black opacity-50 z-10"
style={{ display: isOpened ? "block" : "none" }}
onClick={() => setIsOpened(false)}
>
</div>
<div className="relative z-20">
<CircleButton
onClick={() => {
setIsOpened(!isOpened);
field.onChange(field.value);
}}
>
{
field.value ?
choices.find(choice => choice.value === field.value)?.content :
fieldTitle
}
</CircleButton>
{isOpened && (
<div
className={`absolute inset-0 transition-all duration-500 scale-100`}
onClick={() => setIsOpened(false)}
>
<div
onClick={() => setIsOpened(false)}
className="absolute w-12 h-12 bg-gray-300 border border-gray-400 rounded-full shadow dark:bg-gray-800 dark:border-gray-700 flex flex-col items-center justify-center cursor-pointer hover:bg-gray-400"
style={{
transform: `translate(${radius / 4}px, ${radius / 4}px)`,
}}>
✖
</div>
{choices.map((choice, index) => {
return (
<div
key={choice.value}
className={cn("absolute transition-transform duration-500",
styles[`circle-button-anim-${choices.length}-${index}`]
)}
>
<CircleButton
onClick={() => {
field.onChange(choice.value);
setIsOpened(false);
}}
>
{choice.content}
</CircleButton>
</div>
);
})}
</div>
)}
</div>
</>
)}
/>
</div>
);
};
Et enfin j'ajoute mon module pour le CSS dans un fichier CircleChoice.module.css:
@keyframes moveAway-3-0 {
from {
transform: translate(0px, 0px);
opacity: 0;
}
to {
transform: translate(0px, -120px);
opacity: 1;
}
}
@keyframes moveAway-3-1 {
from {
transform: translate(0px, 0px);
opacity: 0;
}
to {
transform: translate(103.92px, 60px);
opacity: 1;
}
}
@keyframes moveAway-3-2 {
from {
transform: translate(0px, 0px);
opacity: 0;
}
to {
transform: translate(-103.92px, 60px);
opacity: 1;
}
}
.circle-button-anim-3-0 {
animation: moveAway-3-0 0.5s ease-out forwards;
}
.circle-button-anim-3-1 {
animation: moveAway-3-1 0.5s ease-out forwards;
}
.circle-button-anim-3-2 {
animation: moveAway-3-2 0.5s ease-out forwards;
}
@keyframes moveAway-4-0 {
from {
transform: translate(0px, 0px);
opacity: 0;
}
to {
transform: translate(120px, 0px);
opacity: 1;
}
}
@keyframes moveAway-4-1 {
from {
transform: translate(0px, 0px);
opacity: 0;
}
to {
transform: translate(0px, 120px);
opacity: 1;
}
}
@keyframes moveAway-4-2 {
from {
transform: translate(0px, 0px);
opacity: 0;
}
to {
transform: translate(-120px, 0px);
opacity: 1;
}
}
@keyframes moveAway-4-3 {
from {
transform: translate(0px, 0px);
opacity: 0;
}
to {
transform: translate(0px, -120px);
opacity: 1;
}
}
.circle-button-anim-4-0 {
animation: moveAway-4-0 0.5s ease-out forwards;
}
.circle-button-anim-4-1 {
animation: moveAway-4-1 0.5s ease-out forwards;
}
.circle-button-anim-4-2 {
animation: moveAway-4-2 0.5s ease-out forwards;
}
.circle-button-anim-4-3 {
animation: moveAway-4-3 0.5s ease-out forwards;
}
@keyframes moveAway-5-0 {
from {
transform: translate(0px, 0px);
opacity: 0;
}
to {
transform: translate(120px, 0px);
opacity: 1;
}
}
@keyframes moveAway-5-1 {
from {
transform: translate(0px, 0px);
opacity: 0;
}
to {
transform: translate(37.08px, 114.43px);
opacity: 1;
}
}
@keyframes moveAway-5-2 {
from {
transform: translate(0px, 0px);
opacity: 0;
}
to {
transform: translate(-97.08px, 70.63px);
opacity: 1;
}
}
@keyframes moveAway-5-3 {
from {
transform: translate(0px, 0px);
opacity: 0;
}
to {
transform: translate(-97.08px, -70.63px);
opacity: 1;
}
}
@keyframes moveAway-5-4 {
from {
transform: translate(0px, 0px);
opacity: 0;
}
to {
transform: translate(37.08px, -114.43px);
opacity: 1;
}
}
.circle-button-anim-5-0 {
animation: moveAway-5-0 0.5s ease-out forwards;
}
.circle-button-anim-5-1 {
animation: moveAway-5-1 0.5s ease-out forwards;
}
.circle-button-anim-5-2 {
animation: moveAway-5-2 0.5s ease-out forwards;
}
.circle-button-anim-5-3 {
animation: moveAway-5-3 0.5s ease-out forwards;
}
.circle-button-anim-5-4 {
animation: moveAway-5-4 0.5s ease-out forwards;
}
Enfin, à l'utilisation, j'ajoute dans mon composant le bout de code suivant:
const VEHICLES = {
"car": "Voiture",
"plane": "Avion",
"train": "Train",
"bus": "Bus"
}
// ...
<CircleChoice
fieldName="vehicle"
control={control}
fieldTitle="Choix du véhicule"
choices={Object.entries(VEHICLES).map(([key, value]) => ({
value,
content: (<>
<img
src={`/vehicles/${value.toLowerCase()}.png`}
alt={key} className="w-10 h-10 mb-2" />
<span className="text-sm">{key}</span>
</>)
}))}
/>
Voilà, c'est assez simple et efficace je trouve, bon code à vous ! 😉
Je vous laisse personnaliser cela selon vos besoins.

Alexandre P.
Développeur passionné depuis plus de 20 ans, j'ai une appétence particulière pour les défis techniques et changer de technologie ne me fait pas froid aux yeux.
Poursuivre la lecture dans la rubrique Dev