[WebUI]Tailwind + nextjs(Drop down menu) โ€“ 4

๐Ÿ‘‰๐Ÿป ์•„๋ž˜๋Š” Dropdown Menu ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค.
The section below is the dropdown menu.

๐Ÿ‘‰๐Ÿป ๋ ˆ์ด์•„์›ƒ, ํฐํŠธ์‚ฌ์ด์ฆˆ๋‚˜ ๋Œ€๋ถ€๋ถ„์˜ css,tailwindcss ๋‚ด์šฉ์€ ์ด์ „ ํฌ์ŠคํŠธ์˜ ๋‚ด์šฉ๊ณผ ๊ฑฐ์˜ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค.
The layout, font size, and most of the CSS and Tailwind CSS details are nearly identical to those in the previous post.

๐Ÿ‘‰๐Ÿป ์ปดํฌ๋„ŒํŠธ ํ˜•์‹์œผ๋กœ ์‚ฌ์šฉํ• ์ˆ˜ ์žˆ๋„๋ก ui๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
A ui directory has been added to enable usage in component form.

๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ / Project Structure

tailwindcss-nextjs/
โ””โ”€โ”€ app/
    โ”œโ”€โ”€ button-css/
    โ”œโ”€โ”€ button-tailwind/
    โ”œโ”€โ”€ card-css/
    โ”œโ”€โ”€ card-tailwind/
    โ”œโ”€โ”€ form-css/
    โ”œโ”€โ”€ form-tailwind/
    โ”œโ”€โ”€ dropdown-css/
    โ”œโ”€โ”€ dropdown-tailwind/
    โ””โ”€โ”€ ui/
        โ”œโ”€โ”€ page.tsx
        โ””โ”€โ”€ _components/
            โ”œโ”€โ”€ button.tsx     
            โ””โ”€โ”€ dropdown.tsx   

๐Ÿ“ Dropdown Menu – Tailwind & CSS

โœ”๏ธ Tailwind

— ์Šคํฌ๋ฆฐ ์ƒท / Screen Shoot

tailwindcss

— http://localhost:3000/dropdown-tailwind

'use client'
import { useState } from 'react'

const OPTIONS = [
  { value: '9s', label: '9S' },
  { value: '2b', label: '2B' },
  { value: '4b', label: '4B' },
]

export default function Page() {
  const [open, setOpen] = useState(false)
  const [value, setValue] = useState(OPTIONS[0])

  return (
    <main className="min-h-screen flex items-center justify-center bg-gray-50 p-4">
      <div className="w-full max-w-sm bg-white rounded-2xl shadow-lg p-6">
        <h2 className="text-base font-bold">๋“œ๋กญ๋‹ค์šด/Drop down(Tailwindcss)</h2>
        <p className="text-sm text-gray-400 mt-1">freelifemakers</p>

        <div className="mt-6 flex flex-col gap-1.5">
          <label className="text-sm font-bold text-gray-700">์ง์—… / Role</label>

          {/* Select */}
          <div className="relative">
            {/* 1. ๋ฒ„ํŠผ - ํ•ญ์ƒ ๋ณด์ด๋Š” ๋ถ€๋ถ„ / Button - Always visible part */}
            <button
              onClick={() => setOpen(!open)}
              className="w-full flex items-center justify-between px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl text-sm text-left focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-all"
            >
              <span>{value.label}</span>
              <span className={`transition-transform ${open? 'rotate-180' : ''}`}>โ–ผ</span>
            </button>

            {/* 2. ๋ฆฌ์ŠคํŠธ - open์ผ ๋•Œ๋งŒ ๋ณด์ด๋Š” ๋ถ€๋ถ„ / List โ€“ section visible only when open */}
            {open && (
              <div className="absolute z-10 mt-2 w-full bg-white border border-gray-200 rounded-xl shadow-lg overflow-hidden">
                {OPTIONS.map((opt) => (
                  <button
                    key={opt.value}
                    onClick={() => {
                      setValue(opt)
                      setOpen(false)
                    }}
                    className={`w-full text-left px-4 py-3 text-sm hover:bg-gray-50 transition-colors ${
                      value.value === opt.value? 'bg-blue-50 text-blue-600 font-bold' : 'text-gray-700'
                    }`}
                  >
                    {opt.label}
                  </button>
                ))}
              </div>
            )}
          </div>

          <p className="text-xs text-gray-400 mt-2">์„ ํƒ๋œ ๊ฐ’/Selected Value : {value.value}</p>
        </div>
      </div>
    </main>
  )
}

โœ”๏ธ CSS

— ์Šคํฌ๋ฆฐ ์ƒท / ScreenShot

css

— http://localhost:3000/dropdown-css

'use client'
import { useState } from 'react'

const OPTIONS = [
  { value: '9s', label: '9S' },
  { value: '2b', label: '2B' },
  { value: '4b', label: '4B' },
]

export default function Page() {
  const [open, setOpen] = useState(false)
  const [value, setValue] = useState(OPTIONS[0])

  return (
    <>
      <main className="page">
        <div className="card">
          <h2 className="title">๋“œ๋กญ๋‹ค์šด/Drop down(CSS)</h2>
          <p className="subtitle">freelifemakers</p>

          <div className="field-group">
            <label className="label">์ง์—… / Role</label>

            {/* Select ๋ณธ์ฒด - relative ๊ธฐ์ค€์  */}
            <div className="select-wrap">
              {/* 1. ๋ฒ„ํŠผ */}
              <button
                onClick={() => setOpen(!open)}
                className="select-btn"
              >
                <span>{value.label}</span>
                <span className={`arrow ${open? 'arrow-open' : ''}`}>โ–ผ</span>
              </button>

              {/* 2. ๋ฆฌ์ŠคํŠธ */}
              {open && (
                <div className="select-list">
                  {OPTIONS.map((opt) => (
                    <button
                      key={opt.value}
                      onClick={() => {
                        setValue(opt)
                        setOpen(false)
                      }}
                      className={`select-option ${value.value === opt.value? 'selected' : ''}`}
                    >
                      {opt.label}
                    </button>
                  ))}
                </div>
              )}
            </div>

            <p className="hint">์„ ํƒ๋œ ๊ฐ’/Selected Value : {value.value}</p>
          </div>
        </div>
      </main>

      <style>{`
       .page {
          min-height: 100vh;
          display: flex;
          align-items: center;
          justify-content: center;
          background: #f9fafb;
          padding: 16px;
        }
       .card {
          width: 100%;
          max-width: 384px;
          background: white;
          border-radius: 16px;
          box-shadow: 0 10px 15px rgba(0,0,0,0.1);
          padding: 24px;
        }
       .title { font-size: 16px; font-weight: 700; }
       .subtitle { font-size: 14px; color: #9ca3af; margin-top: 4px; }

       .field-group {
          margin-top: 24px;
          display: flex;
          flex-direction: column;
          gap: 6px;
        }
       .label {
          font-size: 14px;
          font-weight: 700;
          color: #374151;
        }
       .hint {
          font-size: 12px;
          color: #9ca3af;
          margin-top: 8px;
        }

        /* Dropdown ๊ธฐ์ค€์  / benchmark - relative */
       .select-wrap {
          position: relative;
        }

        /* 1. ๋ฒ„ํŠผ / button- w-full flex justify-between */
       .select-btn {
          width: 100%;
          display: flex;
          align-items: center;
          justify-content: space-between;
          padding: 12px 16px;
          background: #f9fafb;
          border: 1px solid #e5e7eb;
          border-radius: 12px;
          font-size: 14px;
          text-align: left;
          cursor: pointer;
          transition: all 0.15s;
        }
       .select-btn:focus {
          outline: none;
          background: white;
          border-color: #3b82f6;
          box-shadow: 0 0 0 2px rgba(59,130,246,0.3);
        }

        /* ํ™”์‚ดํ‘œ ํšŒ์ „ / Rotate arrow */
       .arrow {
          transition: transform 0.2s;
          font-size: 12px;
        }
       .arrow-open {
          transform: rotate(180deg);
        }

        /* 2. ๋ฆฌ์ŠคํŠธ / List - absolute z-10 mt-2 */
       .select-list {
          position: absolute;
          z-index: 10;
          margin-top: 8px;
          width: 100%;
          background: white;
          border: 1px solid #e5e7eb;
          border-radius: 12px;
          box-shadow: 0 10px 15px rgba(0,0,0,0.1);
          overflow: hidden;
        }

        /* ์˜ต์…˜ ํ•˜๋‚˜ / One option */
       .select-option {
          width: 100%;
          text-align: left;
          padding: 12px 16px;
          font-size: 14px;
          border: none;
          background: white;
          color: #374151;
          cursor: pointer;
          transition: background 0.15s;
        }
       .select-option:hover {
          background: #f9fafb;
        }
       .select-option.selected {
          background: #eff6ff;
          color: #2563eb;
          font-weight: 700;
        }
      `}</style>
    </>
  )
}

๐Ÿ“ ์ปดํฌ๋„ŒํŠธ ํ˜•์‹์œผ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ / Using as a component

— ์Šคํฌ๋ฆฐ ์ƒท / ScreenShot

UI Library

— http://localhost:3000/ui

โœ”๏ธ ํ˜„์žฌ ๋ฒ„ํŠผ๊ณผ ๋“œ๋กญ๋‹ค์šด ๋ฉ”๋‰ด๊ฐ€ _compoents์— ์žˆ์Šต๋‹ˆ๋‹ค.(button.tsx dropdown.tsx)
Currently, the button and dropdown menu are located in _components (button.tsx, dropdown.tsx).

โœ”๏ธ app/ui/page.tsx์—์„œ ์‚ฌ์šฉ์‹œ ์•„๋ž˜์ฒ˜๋Ÿผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
When using it in app/ui/page.tsx, use it as shown below.

// compoents
import { Button }  from './_components/button'
import { Dropdown } from './_components/dropdown';

export default function UIPage() {
  return (
    <main className="min-h-screen flex items-center justify-center bg-gray-50 p-4">
      <div className="w-full max-w-sm bg-white rounded-2xl shadow-lg p-6 flex flex-col gap-6">
        <h1 className="text-xl font-bold">๋‚ด UI ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ / </h1>

        <Dropdown />
        <Button>์ €์žฅํ•˜๊ธฐ/Save</Button>
      </div>
    </main>
  )
}

๋งˆํƒœ๋ณต์Œ 21์žฅ 22์ ˆ / Matthew 21: 22

“๋„ˆํฌ๊ฐ€ ๊ธฐ๋„ํ•  ๋•Œ์— ๋ฌด์—‡์ด๋“ ์ง€ ๋ฏฟ๊ณ  ๊ตฌํ•˜๋Š” ๊ฒƒ์€ ๋‹ค ๋ฐ›์œผ๋ฆฌ๋ผ ํ•˜์‹œ๋‹ˆ๋ผ”

“And everything you ask in prayer, believing, you will receive.”

Leave a Reply