Do you know that feeling when you create your own component instead of improving the default HTML components? Your design team created something beautiful, but browsers won't support it out of the box, and fixing it everywhere becomes a nightmare. We all know this pain, but these challenges make our job interesting. Today, I wanted to talk about one pitfall that awaits us during this thrilling journey: the placement of dropdown elements, such as the select menus or the date pickers. Absolutely Wrong At first, it looks like position: absolute solves all our problems, and it does to some extent. But then, modal windows ruin everything. If the dropdown overflows, it gets cut off. Sure, you can scroll down and see it, but you’d better pray that your designer cannot reach you with a sharp object. That’s far from perfect—we can do better. Fix It With 'fixed' If we want to display the content over everything, we need position: fixed. The only problem is that we will lose the parent element coordinates: fixed elements are quite independent by nature. This means the only thing we need to do is determine the exact coordinates of the drop-down element in these situations: When we display it. When its content is changed. When we scroll the window and/or the scrollable parent. When we resize the window and/or the scrollable parent. We also need to decide whether to display it above the toggler if it is too close to the bottom of the screen. Feels doable. I will use Vue.js, but it should be easy to follow even if you prefer React or Angular. Let’s Rock This Joint Here’s the structure we’ll use: export const useDropdownAttributes = () => { const dropdownWidth = ref(''); const dropdownTop = ref(''); const dropdownBottom = ref(''); const dropdownLeft = ref(''); const isDirectedUpwards = ref(false); const togglerRect = ref<DOMRect>(); const dropdownRect = ref<DOMRect>(); const autodetectPosition = ( isDropdownDisplayed: Ref<boolean>, togglerElement: HTMLElement | null = null, dropdownElement: HTMLDivElement | null = null, dropdownContent: Ref<unknown> | ComputedRef<unknown> = ref([]), isUpwardPreferred = false, ) => { // ... } return { autodetectPosition, dropdownTop, dropdownBottom, dropdownLeft, dropdownWidth, isDirectedUpwards, togglerRect, dropdownRect, }; }; There are four variables for the dropdown position plus the isDirectedUpwards flag and a function that updates them all. We also return two variables for the toggler’s and dropdown’s Rects: this might be convenient, for example, for the tooltips that need to be aligned to the middle of the content. As you may remember, we also need to handle the scrolling and resizing of the scrollable parent, so let’s create a function to find it: const getFirstScrollableParent = (element: HTMLElement | null): HTMLElement => { const parentElement = element?.parentElement; if (!parentElement) return document.body; const overflowY = window.getComputedStyle(parentElement).overflowY; if (overflowY === 'scroll' || overflowY === 'auto') return parentElement; return getFirstScrollableParent(parentElement); }; Now, let’s add the main function: const autodetectPosition = ( isDropdownDisplayed: Ref<boolean>, togglerElement: HTMLElement | null = null, dropdownElement: HTMLElement | null = null, dropdownContent: Ref<unknown> | ComputedRef<unknown> = ref([]), isUpwardPreferred = false, ) => { if (!togglerElement || !dropdownElement) return; const updateDropdownAttributes = () => { togglerRect.value = togglerElement.getBoundingClientRect(); dropdownRect.value = dropdownElement.getBoundingClientRect(); dropdownWidth.value = `${togglerRect.value.width}px`; dropdownBottom.value = `${window.innerHeight - togglerRect.value.top}px`; dropdownTop.value = `${ window.innerHeight - togglerRect.value.bottom - dropdownRect.value.height }px`; dropdownLeft.value = `${togglerRect.value.left}px`; }; const handleResize = () => { requestAnimationFrame(updateDropdownAttributes); }; const handleScroll = () => { requestAnimationFrame(updateDropdownAttributes); }; watch( [isDropdownDisplayed, dropdownContent], ([newVal, _]) => { const scrollableParent = getFirstScrollableParent(togglerElement); if (!newVal) { window.removeEventListener('resize', handleResize); window.removeEventListener('scroll', handleScroll); scrollableParent.removeEventListener('resize', handleResize); scrollableParent.removeEventListener('scroll', handleScroll); return; } requestAnimationFrame(() => { const distanceFromBottom = window.innerHeight - togglerElement.getBoundingClientRect().bottom; const distanceFromTop = togglerElement.getBoundingClientRect().top; const dropdownHeight = dropdownElement.offsetHeight; isDirectedUpwards.value = isUpwardPreferred ? distanceFromTop > dropdownHeight : distanceFromBottom < dropdownHeight && distanceFromTop > dropdownHeight; updateDropdownAttributes(); window.addEventListener('resize', handleResize); window.addEventListener('scroll', handleScroll); scrollableParent.addEventListener('resize', handleResize); scrollableParent.addEventListener('scroll', handleScroll); }); }, { deep: true }, ); }; We pass isDropdownDisplayed and dropdownContent so we can react to their updates. We also pass togglerElement and dropdownElementwhich we need to calculate the position. Finally, there’s isUpwardPreferred in case you want the dropdown above the toggler by default. Time to Relax and Enjoy In your component, you will need something like this (I assume you have added refs to your toggler and dropdown in the template): const { autodetectPosition, dropdownTop, dropdownBottom, dropdownLeft, dropdownWidth, isDirectedUpwards, } = useDropdownAttributes(); const togglerRef = ref<HTMLElement>(); const dropdownRef = ref<HTMLElement>(); const isDropdownShown = ref(false); onMounted(() => { autodetectPosition(isDropdownShown, togglerRef.value?.$el, dropdownRef.value?.$el); }); And the CSS will look like this: .dropdown { position: fixed; bottom: v-bind('isDirectedUpwards ? dropdownBottom : dropdownTop'); left: v-bind('dropdownLeft'); width: v-bind('dropdownWidth'); min-width: 0; } Voilà. The dropdown is displayed properly even when overflowing and moves above the toggler if there isn’t enough space below. And since we’re at the end of the article, I’d love to leave you with something cheerful—but I’m out of ideas. So, I'm afraid "good luck" is all I have this time. Good luck. 👋 You can find the complete code on GitHub. Do you know that feeling when you create your own component instead of improving the default HTML components? Your design team created something beautiful, but browsers won't support it out of the box, and fixing it everywhere becomes a nightmare. We all know this pain, but these challenges make our job interesting. Today, I wanted to talk about one pitfall that awaits us during this thrilling journey: the placement of dropdown elements, such as the select menus or the date pickers. Absolutely Wrong At first, it looks like position: absolute solves all our problems, and it does to some extent. But then, modal windows ruin everything. position: absolute If the dropdown overflows, it gets cut off. Sure, you can scroll down and see it, but you’d better pray that your designer cannot reach you with a sharp object. That’s far from perfect—we can do better. Fix It With 'fixed' 'fixed' If we want to display the content over everything, we need position: fixed . The only problem is that we will lose the parent element coordinates: fixed elements are quite independent by nature. This means the only thing we need to do is determine the exact coordinates of the drop-down element in these situations: position: fixed When we display it. When its content is changed. When we scroll the window and/or the scrollable parent. When we resize the window and/or the scrollable parent. When we display it. When its content is changed. When we scroll the window and/or the scrollable parent. When we resize the window and/or the scrollable parent. We also need to decide whether to display it above the toggler if it is too close to the bottom of the screen. Feels doable. I will use Vue.js, but it should be easy to follow even if you prefer React or Angular. Let’s Rock This Joint Here’s the structure we’ll use: export const useDropdownAttributes = () => { const dropdownWidth = ref(''); const dropdownTop = ref(''); const dropdownBottom = ref(''); const dropdownLeft = ref(''); const isDirectedUpwards = ref(false); const togglerRect = ref<DOMRect>(); const dropdownRect = ref<DOMRect>(); const autodetectPosition = ( isDropdownDisplayed: Ref<boolean>, togglerElement: HTMLElement | null = null, dropdownElement: HTMLDivElement | null = null, dropdownContent: Ref<unknown> | ComputedRef<unknown> = ref([]), isUpwardPreferred = false, ) => { // ... } return { autodetectPosition, dropdownTop, dropdownBottom, dropdownLeft, dropdownWidth, isDirectedUpwards, togglerRect, dropdownRect, }; }; export const useDropdownAttributes = () => { const dropdownWidth = ref(''); const dropdownTop = ref(''); const dropdownBottom = ref(''); const dropdownLeft = ref(''); const isDirectedUpwards = ref(false); const togglerRect = ref<DOMRect>(); const dropdownRect = ref<DOMRect>(); const autodetectPosition = ( isDropdownDisplayed: Ref<boolean>, togglerElement: HTMLElement | null = null, dropdownElement: HTMLDivElement | null = null, dropdownContent: Ref<unknown> | ComputedRef<unknown> = ref([]), isUpwardPreferred = false, ) => { // ... } return { autodetectPosition, dropdownTop, dropdownBottom, dropdownLeft, dropdownWidth, isDirectedUpwards, togglerRect, dropdownRect, }; }; There are four variables for the dropdown position plus the isDirectedUpwards flag and a function that updates them all. We also return two variables for the toggler’s and dropdown’s Rects: this might be convenient, for example, for the tooltips that need to be aligned to the middle of the content. isDirectedUpwards As you may remember, we also need to handle the scrolling and resizing of the scrollable parent, so let’s create a function to find it: const getFirstScrollableParent = (element: HTMLElement | null): HTMLElement => { const parentElement = element?.parentElement; if (!parentElement) return document.body; const overflowY = window.getComputedStyle(parentElement).overflowY; if (overflowY === 'scroll' || overflowY === 'auto') return parentElement; return getFirstScrollableParent(parentElement); }; const getFirstScrollableParent = (element: HTMLElement | null): HTMLElement => { const parentElement = element?.parentElement; if (!parentElement) return document.body; const overflowY = window.getComputedStyle(parentElement).overflowY; if (overflowY === 'scroll' || overflowY === 'auto') return parentElement; return getFirstScrollableParent(parentElement); }; Now, let’s add the main function: const autodetectPosition = ( isDropdownDisplayed: Ref<boolean>, togglerElement: HTMLElement | null = null, dropdownElement: HTMLElement | null = null, dropdownContent: Ref<unknown> | ComputedRef<unknown> = ref([]), isUpwardPreferred = false, ) => { if (!togglerElement || !dropdownElement) return; const updateDropdownAttributes = () => { togglerRect.value = togglerElement.getBoundingClientRect(); dropdownRect.value = dropdownElement.getBoundingClientRect(); dropdownWidth.value = `${togglerRect.value.width}px`; dropdownBottom.value = `${window.innerHeight - togglerRect.value.top}px`; dropdownTop.value = `${ window.innerHeight - togglerRect.value.bottom - dropdownRect.value.height }px`; dropdownLeft.value = `${togglerRect.value.left}px`; }; const handleResize = () => { requestAnimationFrame(updateDropdownAttributes); }; const handleScroll = () => { requestAnimationFrame(updateDropdownAttributes); }; watch( [isDropdownDisplayed, dropdownContent], ([newVal, _]) => { const scrollableParent = getFirstScrollableParent(togglerElement); if (!newVal) { window.removeEventListener('resize', handleResize); window.removeEventListener('scroll', handleScroll); scrollableParent.removeEventListener('resize', handleResize); scrollableParent.removeEventListener('scroll', handleScroll); return; } requestAnimationFrame(() => { const distanceFromBottom = window.innerHeight - togglerElement.getBoundingClientRect().bottom; const distanceFromTop = togglerElement.getBoundingClientRect().top; const dropdownHeight = dropdownElement.offsetHeight; isDirectedUpwards.value = isUpwardPreferred ? distanceFromTop > dropdownHeight : distanceFromBottom < dropdownHeight && distanceFromTop > dropdownHeight; updateDropdownAttributes(); window.addEventListener('resize', handleResize); window.addEventListener('scroll', handleScroll); scrollableParent.addEventListener('resize', handleResize); scrollableParent.addEventListener('scroll', handleScroll); }); }, { deep: true }, ); }; const autodetectPosition = ( isDropdownDisplayed: Ref<boolean>, togglerElement: HTMLElement | null = null, dropdownElement: HTMLElement | null = null, dropdownContent: Ref<unknown> | ComputedRef<unknown> = ref([]), isUpwardPreferred = false, ) => { if (!togglerElement || !dropdownElement) return; const updateDropdownAttributes = () => { togglerRect.value = togglerElement.getBoundingClientRect(); dropdownRect.value = dropdownElement.getBoundingClientRect(); dropdownWidth.value = `${togglerRect.value.width}px`; dropdownBottom.value = `${window.innerHeight - togglerRect.value.top}px`; dropdownTop.value = `${ window.innerHeight - togglerRect.value.bottom - dropdownRect.value.height }px`; dropdownLeft.value = `${togglerRect.value.left}px`; }; const handleResize = () => { requestAnimationFrame(updateDropdownAttributes); }; const handleScroll = () => { requestAnimationFrame(updateDropdownAttributes); }; watch( [isDropdownDisplayed, dropdownContent], ([newVal, _]) => { const scrollableParent = getFirstScrollableParent(togglerElement); if (!newVal) { window.removeEventListener('resize', handleResize); window.removeEventListener('scroll', handleScroll); scrollableParent.removeEventListener('resize', handleResize); scrollableParent.removeEventListener('scroll', handleScroll); return; } requestAnimationFrame(() => { const distanceFromBottom = window.innerHeight - togglerElement.getBoundingClientRect().bottom; const distanceFromTop = togglerElement.getBoundingClientRect().top; const dropdownHeight = dropdownElement.offsetHeight; isDirectedUpwards.value = isUpwardPreferred ? distanceFromTop > dropdownHeight : distanceFromBottom < dropdownHeight && distanceFromTop > dropdownHeight; updateDropdownAttributes(); window.addEventListener('resize', handleResize); window.addEventListener('scroll', handleScroll); scrollableParent.addEventListener('resize', handleResize); scrollableParent.addEventListener('scroll', handleScroll); }); }, { deep: true }, ); }; We pass isDropdownDisplayed and dropdownContent so we can react to their updates. isDropdownDisplayed dropdownContent We also pass togglerElement and dropdownElement which we need to calculate the position. togglerElement dropdownElement Finally, there’s isUpwardPreferred in case you want the dropdown above the toggler by default. isUpwardPreferred Time to Relax and Enjoy In your component, you will need something like this (I assume you have added refs to your toggler and dropdown in the template): const { autodetectPosition, dropdownTop, dropdownBottom, dropdownLeft, dropdownWidth, isDirectedUpwards, } = useDropdownAttributes(); const togglerRef = ref<HTMLElement>(); const dropdownRef = ref<HTMLElement>(); const isDropdownShown = ref(false); onMounted(() => { autodetectPosition(isDropdownShown, togglerRef.value?.$el, dropdownRef.value?.$el); }); const { autodetectPosition, dropdownTop, dropdownBottom, dropdownLeft, dropdownWidth, isDirectedUpwards, } = useDropdownAttributes(); const togglerRef = ref<HTMLElement>(); const dropdownRef = ref<HTMLElement>(); const isDropdownShown = ref(false); onMounted(() => { autodetectPosition(isDropdownShown, togglerRef.value?.$el, dropdownRef.value?.$el); }); And the CSS will look like this: .dropdown { position: fixed; bottom: v-bind('isDirectedUpwards ? dropdownBottom : dropdownTop'); left: v-bind('dropdownLeft'); width: v-bind('dropdownWidth'); min-width: 0; } .dropdown { position: fixed; bottom: v-bind('isDirectedUpwards ? dropdownBottom : dropdownTop'); left: v-bind('dropdownLeft'); width: v-bind('dropdownWidth'); min-width: 0; } Voilà. The dropdown is displayed properly even when overflowing and moves above the toggler if there isn’t enough space below. And since we’re at the end of the article, I’d love to leave you with something cheerful—but I’m out of ideas. So, I'm afraid "good luck" is all I have this time. Good luck. 👋 You can find the complete code on GitHub. You can find the complete code on GitHub . GitHub