Skip to content

kenyontu/frontend-mentor-bookmark-landing-page

Repository files navigation

Frontend Mentor - Bookmark landing page solution

This is a solution to the Bookmark landing page challenge on Frontend Mentor. Frontend Mentor challenges help you improve your coding skills by building realistic projects.

Table of contents

Overview

The challenge

Users should be able to:

  • View the optimal layout for the site depending on their device's screen size
  • See hover states for all interactive elements on the page
  • Receive an error message when the newsletter form is submitted if:
    • The input field is empty
    • The email address is not formatted correctly

Screenshots

Click to open the full-size screenshot.

Mobile layout Desktop layout

Links

My process

Built with

  • Semantic HTML5 markup
  • Flexbox
  • CSS Grid
  • Mobile-first workflow
  • React - JS library
  • Remix - React framework
  • Tailwind CSS - CSS framework

What I learned

Dynamically sized accordion items

At first I've implemented the accordion's opening and closing animations by toggling between 0 and fixed value max-height like 10rem. But this approach has the following issues:

  • Animations are not consistent between items of different sizes. Since the transitions are between 0 and a fixed value, not the actual height of the content.
  • Setting a low max-height has the chance of cutting out part of the content. And setting it too high makes the animations too quick.

Doing some research on other methods, found this Codepen by Aaron Bushnell, where he showcases a way to get the appropriate max-height value for each item with vanilla JS.

Based on his approach I came up with this solution on React:

function AccordionItem({ open, title, text, onClick }: Props) {
  // Will hold the max-height for this item, the negative value means that it
  // is not initialized and we need to measure it
  const [maxHeight, setMaxHeight] = useState(-1)
  const container = useRef<HTMLDivElement>(null)

  useEffect(() => {
    // We use the scrollHeight value of container as the max-height, but this
    // requires it to actually be rendered. Which is why we set it to be
    // absolute positioned and invisible at first, to allow measuring
    if (maxHeight < 0) setMaxHeight(answerContainer.current?.scrollHeight ?? 0)
  }, [maxHeight])

  useEffect(() => {
    // In the event where the user decides to resize the screen, the content's
    // dimensions might change, so we need to measure a new max-height, we do
    // this by setting maxHeight to a negative value, thus triggering the
    // effect above
    const onResize = () => setMaxHeight(-1)

    window.addEventListener('resize', onResize)

    return () => window.removeEventListener('resize', onResize)
  }, [])

  return (
    <div>
      <button onClick={onClick}>{title}</button>
      <div
        ref={container}
        style={{ maxHeight: maxHeight < 0 ? undefined : open ? maxHeight : 0 }}
        className={clsx(
          'overflow-hidden transition-[max-height] duration-300 ease-in-out',
          {
            'invisible absolute': maxHeight < 0,
          }
        )}
      >
        <p>{text}</p>
      </div>
    </div>
  )
}