Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                    <div id="root"></div>
              
            
!

CSS

              
                html,
body {
  width: 100%;
  height: 100%;
  min-height: 100%;
  margin: 0;
  padding: 0;
  position: relative;
  font-family: "Roboto", "Helvetica Nue", "arial",  sans-serif;
}
#root {
  display: flex;
  width: 100%;
  height: 100%;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 1rem;
  padding: 1rem 1rem 3rem;
  box-sizing: border-box;
}

h1 {
  text-align: center;
  margin: 0;
  display: block;
  font-size: 1.8rem;
  max-width: 400px;
  border-bottom: 2px solid #000;
  padding: 0.2rem 0;
}
p {
  width: 100%;
  max-width: 400px;
  font-size: 14px;
  word-break: keep-all;
  // text-align: center;
  &:nth-child(2) {
    color: #999;
  }
}

.wrapper {
  display: block;
  position: relative;
  width: 100%;
  max-width: 400px;
  margin: 0 auto;

  div {
    border-radius: 0.5rem;
    position: relative;
    font-weight: 600;
  }
}
select {
  -moz-appearance: none;
  -webkit-appearance: none;
  -o-appearance: none;
  -ms-appearance: none;
  appearance: none;
  border-radius: 0;
  background: none transparent;
  vertical-align: middle;
  font-size: inherit;
  color: inherit;
  box-sizing: content-box;
  margin: 0;
  width: 120px;
  border-radius: 0.5rem;
  padding: 0.75rem 2.5rem 0.65rem 0.75rem;
  box-shadow: none;
  box-sizing: border-box;
  display: block;
  width: 100%;
  border: 2px solid #ddd;
  line-height: 1.06;
}

ul {
  position: absolute;
  z-index: 99;
  width: 100%;
  margin-top: 0.25rem;
  border-radius: 0.5rem;
  border: 2px solid black;
  padding: 0.5rem 0.5rem;
  background: white;
  list-style: none;
  box-sizing: border-box;

  button {
    display: block;
    width: 100%;
    text-align: left;
    transition: all 0.3s;
    padding: 0.5rem;
    border-radius: 0.5rem;
    border: 0;
    outline: 0;
    background: none;

    &:hover {
      background: #eee;
    }
    &:active {
      background: #aaa;
    }
    &.selected {
      background: #000;
      color: #fff;
    }
  }
}

.arrow {
  width: 20px;
  height: 20px;
  background: #000;
  display: block;
  position: absolute;
  right: 0.5rem;
  top: 10px;
  border-radius: 0.25rem;
  pointer-events: none;
  &:before,
  &:after {
    content: "";
    background-color: transparent;
    width: 2px;
    height: 12px;
    background: yellow;
    border-bottom: 7px solid white;
    display: block;
    position: absolute;
    background: none;
    box-sizing: border-box;
    transform: rotate(0);
    transform-origin: center;
    top: 2px;
    left: 9px;
    transition: all 0.3s;
  }
  &.is-expanded {
    &:before,
    &:after {
      top: 6px;
    }
    &:before {
      transform: rotate(135deg);
    }
    &:after {
      transform: rotate(-135deg);
    }
  }
  &:before {
    transform: rotate(45deg);
  }
  &:after {
    transform: rotate(-45deg);
  }
}

              
            
!

JS

              
                const { useState, useEffect, useRef, useMemo } = React;
const { createRoot } = ReactDOM;

// dummy data
const optionData = [
  { optionKey: "key01", optionName: "my option 1" },
  { optionKey: "key02", optionName: "my option 2" }
];

// const
const moWidth = 575;

// custom hook
const useDevice = () => {
  // 모바일 디바이스인지 아닌지 체크
  // uaParser 패키지 이용
  const uaParser = new UAParser(window.navigator.userAgent);
  return useMemo(() => {
    try {
      return uaParser.getDevice();
    } catch (err) {
      return null;
    }
  }, []);
};

const useResize = () => {
  // 브라우저 가로 너비 감지
  const [state, setState] = useState({
    w: window?.innerWidth,
    h: window?.innerHeight
  });

  const debounce = (func, delay) => {
    let timeoutId = null;
    return (...args) => {
      if (timeoutId) {
        clearTimeout(timeoutId);
      }
      timeoutId = setTimeout(() => func(...args), delay);
    };
  };

  const onResize = (e) => {
    setState(() => {
      return {
        w: e.target.innerWidth,
        h: e.target.innerHeight
      };
    });
  };

  useEffect(() => {
    window.addEventListener("resize", debounce(onResize, 100));
    return () => {
      window.removeEventListener("resize", onResize);
    };
  }, []);

  return state;
};

const CustomSelect = () => {
  // state
  const [isExpand, setIsExpand] = useState(false);
  const [selected, setSelected] = useState("key01");

  const { type: deviceType } = useDevice();
  const { w: deviceWidth } = useResize();

  const handleKeydown = (e) => {
    // 키보드 제어
    // KeyCode
    // 38 : 화살표 위 | 40 : 화살표 아래 | 13:엔터
    if (e.KeyCode === 38 || e.KeyCode === 40 || e.keyCode === 13) {
      e.preventDefault(); // 기본동작을 막아 options 비노출
    }

    if (e.keyCode === 38 || e.keyCode === 40) {
      // 위, 아래 키 눌렀을 때 선택한 데이터 변경
      setIsExpand(() => true);
      setSelected((prev) => {
        const newIdx = () => {
          const oldIdx = optionData.findIndex(
            (option) => option.optionKey === prev
          );
          if (e.keyCode === 38) {
            return oldIdx === 0 ? oldIdx : oldIdx - 1;
          }
          if (e.keyCode === 40) {
            return oldIdx === optionData.length - 1 ? oldIdx : oldIdx + 1;
          }
        };

        return optionData[newIdx()].optionKey;
      });
    }
    if (e.keyCode === 13) {
      // 엔터 키 눌렀을 때 ul리스트 토글
      setIsExpand((prev) => !prev);
    }
  };

  const handleMouseDown = (e) => {
    // select태그를 누르는 순간 option 리스트가 노출되므로
    // 마우스를 뗄 때 실행되는 onClick이 아닌
    // onMouseDown일 때 기본 동작을 막아야 함
    e.preventDefault();

    // select 리스트가 열려 있는 상태에서 다시 누른 상황이라면
    // focus되어 있는지 체크하고
    // focus되어 있는 상태라면 blur 처리
    if (e.target.matches(":focus")) {
      setIsExpand((prev) => !prev);
    } else {
      e.target.focus();
      setIsExpand(() => true);
    }

    return false;
  };

  return (
    <>
      <div
        className="wrapper"
        onBlur={() => {
          // onBlur일 때 하단 드롭다운 메뉴를 닫는다
          // select 태그가 아니라 ul리스트도 함께 감싼 wrapper에
          // onBlur를 넣어줘야 ul태그의 버튼 이벤트를 onClick에 넣을 수 있다
          setIsExpand(() => false);
        }}
        onKeyDown={(e) => {
          if (deviceWidth > moWidth || deviceType !== "mobile")
            handleKeydown(e);
        }}
        onMouseDown={(e) => {
          if (deviceWidth > moWidth || deviceType !== "mobile") handleMouseDown(e);
        }}
      >
        <div>
          <span className={`arrow ${isExpand ? "is-expanded" : ""}`}></span>
          <select
            name="select"
            value={selected}
            onChange={(e) => {
              // option을 선택하면 selected 값을 변경
              setSelected(e.target.value);
            }}
          >
            {optionData.length > 0 &&
              optionData.map(({ optionKey, optionName }) => {
                // optionData를 이용해 옵션 렌더링
                return (
                  <option key={optionKey} value={optionKey}>
                    {optionName}
                  </option>
                );
              })}
          </select>
        </div>
        {isExpand && (
          <ul>
            {optionData.length > 0 &&
              optionData.map(({ optionKey, optionName }) => {
                // optionData를 이용해 리스트 렌더링
                return (
                  <li key={optionKey}>
                    <button
                      buttonid={optionKey}
                      type="button"
                      onClick={() => {
                        // select option을 선택하면  onchange를 이용해 state 값을 변경한다
                        // selected state를 바로 변경함
                        setSelected(optionKey);
                        setIsExpand(false);
                      }}
                      className={selected === optionKey ? "selected" : ""}
                    >
                      {optionName}
                    </button>
                  </li>
                );
              })}
          </ul>
        )}
      </div>
    </>
  );
};

const App = () => {
  return (
    <>
      <h1>React Custom Selectbox</h1>
      <div>
        <p>
          자유롭게 스타일링할 수 있는 셀렉트 박스. 사이즈가 작은 모바일
          브라우저에서는 기본 셀렉트박스로 이용할 수 있습니다.
        </p>
        <p>
          A customizable select box that can be styled freely. It can be used as
          a default select box in small-sized mobile browsers.
        </p>
      </div>

      <CustomSelect />
    </>
  );
};

const root = createRoot(document.getElementById("root"));
root.render(<App />);

              
            
!
999px

Console