Search code examples
reactjstailwind-cssvite

How to implement arrows outside button from the given design?


enter image description here

I am building my react app from a design like this From the design in the picture: There are 3 Cols Left side is dynamic image Central is dynamic text Right side is dynamic button.

When A button is selected, the picture and text dynamically changes (This functionality works fine), but the arrows on extreme right (in red colour) are a headache for me. How to implement and execute them?

I have the svg icon of the arrow from design should I use that? I have tried various solutions such as path etc. to no avail.

I am extremely new in react and totally understand this may sound basic to you, but I am a beginner hence need a bit help.

I am building this app on vite using react and tailwind css for styling.

Here is my existing code.

import { useState } from 'react';
import Heading from '../Heading/Heading';
import DiscoveryButton from '../DiscoveryButton/DiscoveryButton';
import { responses } from './discoveryResponses'; // Adjust path as necessary
import { buttonNames } from '../DiscoveryButton/buttonNames';
import './DiscoveryResponse.css';

const DiscoveryResponse = () => {
  const [activeResponse, setActiveResponse] = useState(0);

  const handleButtonClick = (index) => {
    setActiveResponse(index);
  };

  return (
    <section className="py-12 px-4 md:px-40">
      <div className="text-center pt-4 pb-8">
        <Heading title="Discovery Response Generator (Discovery Workflow)" titleFirst={true} />
      </div>

      <div className="grid grid-cols-1 md:grid-cols-[30%_35%_35%] py-10 gap-10">
        {/* Left Side (Image Display) */}
        <div className="hidden md:block">
          <img
            src={responses[activeResponse].image}
            alt="Feature"
            className="w-full h-auto border-2 border-l-0 border-black p-0 m-0"
          />
        </div>

        {/* Center (Text Display) */}
        <div className="flex items-center justify-center border-2 border-gray-200 rounded-2xl">
          <div className="overflow-auto" style={{ maxHeight: '400px' }}>
            <h1 className="text-lg p-4">{buttonNames[activeResponse]}</h1>
            <p className="text-lg p-4">{responses[activeResponse].text}</p>
          </div>
        </div>

        {/* Right Side (Buttons and Navigation) */}
        <div className="grid grid-cols-[80%_20%] gap-4 relative">
          {/* Button Grid */}
          <div className="grid grid-cols-1 items-start border-2 space-y-4">
            {responses.map((response, index) => (
              <DiscoveryButton
                key={index}
                index={index}
                active={activeResponse === index}
                onClick={handleButtonClick}
              />
            ))}
          </div>

          {/* Navigation Arrows */}
          <div className="grid grid-cols-1 border-r-2 border-black items-start justify-center space-y-4 relative">
            {responses.map((response, index) => (
              <div key={index} className="relative w-full flex items-center">
                <p className="invisible">arrow</p> {/* Placeholder to maintain space */}
                <svg
                  className="absolute left-0 transform -translate-x-full"
                  width="34"
                  height="34"
                  viewBox="0 0 30 4"
                  fill="none"
                  xmlns="http://www.w3.org/2000/svg"
                >
                  <path
                    d="M16 12l6 6 1.4-1.4L18.8 12 23.4 7.4 21 6l-6 6z"
                    fill="currentColor"
                  />
                  <line
                    x1="30"
                    y1="12"
                    x2="16"
                    y2="12"
                    stroke="currentColor"
                    strokeWidth="2"
                  />
                </svg>
              </div>
            ))}
          </div>
        </div>
      </div>
    </section>
  );
};

export default DiscoveryResponse;

the arrows on extreme right (in red colour) are a headache for me. How to implement and execute them?


Solution

    1. Rework the SVG arrow to only be the arrow head. "Shrink-wrap" the SVG so that the view box is taken up fully by the bounds of the visible areas. This makes it easier to layout and means we don't need to reason with SVG scaling. Remove their absolute positioning:

      <svg width="8.4" height="12" viewBox="0 0 8.4 12" fill="none">
        <path d="M0 6l6 6 1.4-1.4L2.8 6 7.4 1.4 5 0l-6 6z" fill="currentColor"/>
      </svg>
      
    2. Remove the placeholders:

      <p className="invisible">arrow</p> {/* Placeholder to maintain space */}
      
    3. Remove the gap between the two columns so the arrows are flush with the right side of the buttons container:

      <div className="grid grid-cols-[80%_20%] relative">
      
    4. Rework the arrow grid so that the children take up the full height of the grid row track. Use pt-4 first:pt-0 classes to emulate space-y-4 but so the border box is still the full height of the grid row tracks.

      <div className="relative grid grid-cols-1 border-r-2 border-black justify-center">
        {responses.map((response, index) => (
          <div className="… pt-4 first:pt-0">
      
    5. Remove the classes on the arrow element wrappers – unnecessary complications:

      <div className="relative grid grid-cols-1 justify-center">
        {responses.map((response, index) => (
          <div className="">
      
    6. Line up the SVGs so they are vertically centered with respect to their button:

      <svg className="translate-y-[calc((theme(fontSize.base.1.lineHeight)-12px)*.5)]" …>
      
    7. Draw the arrow shafts using a linear gradient background. At this point, we abstract the padding-top to a CSS variable so we have a single source of truth since we need its value to position the gradient correctly:

      <div className="relative w-full pt-[--pt] bg-gradient-to-t from-black to-black bg-[position:0_calc((theme(fontSize.base.1.lineHeight)*.5)-1px+var(--pt))] bg-[length:100%_2px] bg-no-repeat [--pt:theme(padding.4)] first:[--pt:0%]">
      
    8. Remove the right border:

      <div className="grid grid-cols-1 justify-center relative">
      
    9. Draw the right vertical line, using another background linear gradient:

      <div className="… bg-[image:linear-gradient(#000,#000),linear-gradient(#000,#000)] bg-[position:0_calc((theme(fontSize.base.1.lineHeight)*.5)-1px+var(--pt)),100%_var(--y,0%)] bg-[length:100%_2px,2px_var(--h,100%)] bg-no-repeat [--pt:theme(padding.4)] first:[--pt:0%] first:[--y:calc(theme(fontSize.base.1.lineHeight)*.5)] last:[--h:calc((theme(fontSize.base.1.lineHeight)*.5)+var(--pt))]
      

    const { useState } = React;
    
    const Heading = () => 'Heading';
    const DiscoveryButton = ({ index, onClick }) => {
      return (
        <button onClick={() => onClick(index)}>
          DiscoveryButton
        </button>
      );
    }
    
    const responses = [
      {
        image: 'https://picsum.photos/300/300',
        text: 'Foo',
      },
      {
        image: 'https://picsum.photos/300/300?',
        text: 'Bar',
      },
      {
        image: 'https://picsum.photos/300/300?0',
        text: 'Qux',
      },
      {
        image: 'https://picsum.photos/300/300?1',
        text: 'Quux',
      },
    ];
    
    const buttonNames = ['Foo', 'Bar', 'Qux', 'Quux'];
    
    const DiscoveryResponse = () => {
      const [activeResponse, setActiveResponse] = useState(0);
    
      const handleButtonClick = (index) => {
        setActiveResponse(index);
      };
    
      return (
        <section className="py-12 px-4 md:px-40">
          <div className="text-center pt-4 pb-8">
            <Heading title="Discovery Response Generator (Discovery Workflow)" titleFirst={true} />
          </div>
    
          <div className="grid grid-cols-1 md:grid-cols-[30%_35%_35%] py-10 gap-10">
            {/* Left Side (Image Display) */}
            <div className="hidden md:block">
              <img
                src={responses[activeResponse].image}
                alt="Feature"
                className="w-full h-auto border-2 border-l-0 border-black p-0 m-0"
              />
            </div>
    
            {/* Center (Text Display) */}
            <div className="flex items-center justify-center border-2 border-gray-200 rounded-2xl">
              <div className="overflow-auto" style={{ maxHeight: '400px' }}>
                <h1 className="text-lg p-4">{buttonNames[activeResponse]}</h1>
                <p className="text-lg p-4">{responses[activeResponse].text}</p>
              </div>
            </div>
    
            {/* Right Side (Buttons and Navigation) */}
            <div className="grid grid-cols-[80%_20%] relative">
              {/* Button Grid */}
              <div className="grid grid-cols-1 items-start border-2 space-y-4">
                {responses.map((response, index) => (
                  <DiscoveryButton
                    key={index}
                    index={index}
                    active={activeResponse === index}
                    onClick={handleButtonClick}
                  />
                ))}
              </div>
    
              {/* Navigation Arrows */}
              <div className="grid grid-cols-1 justify-center relative">
                {responses.map((response, index) => (
                  <div key={index} className="relative w-full pt-[--pt] bg-[image:linear-gradient(#000,#000),linear-gradient(#000,#000)] bg-[position:0_calc((theme(fontSize.base.1.lineHeight)*.5)-1px+var(--pt)),100%_var(--y,0%)] bg-[length:100%_2px,2px_var(--h,100%)] bg-no-repeat [--pt:theme(padding.4)] first:[--pt:0%] first:[--y:calc(theme(fontSize.base.1.lineHeight)*.5)] last:[--h:calc((theme(fontSize.base.1.lineHeight)*.5)+var(--pt))]">
                <svg className="translate-y-[calc((theme(fontSize.base.1.lineHeight)-12px)*.5)]" width="8.4" height="12" viewBox="0 0 8.4 12" fill="none">
                  <path d="M0 6l6 6 1.4-1.4L2.8 6 7.4 1.4 5 0l-6 6z" fill="currentColor"/>
                </svg>
                  </div>
                ))}
              </div>
            </div>
          </div>
        </section>
      );
    };
    
    ReactDOM.createRoot(document.getElementById('app')).render(<DiscoveryResponse/>);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.3.1/umd/react.production.min.js" integrity="sha512-QVs8Lo43F9lSuBykadDb0oSXDL/BbZ588urWVCRwSIoewQv/Ewg1f84mK3U790bZ0FfhFa1YSQUmIhG+pIRKeg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.3.1/umd/react-dom.production.min.js" integrity="sha512-6a1107rTlA4gYpgHAqbwLAtxmWipBdJFcq8y5S/aTge3Bp+VAklABm2LO+Kg51vOWR9JMZq1Ovjl5tpluNpTeQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="https://cdn.tailwindcss.com/3.4.4"></script>
    
    <div id="app"></div>