[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