scroll-bar
는 어느 웹 사이트를 방문하더라도 쉽게 볼 수 있는 UI
이다. 그런데 웹 사이트를 만들 때 스크롤이 레이아웃을 종종 망가뜨릴 때가 있었기에 이번에 Scrollprogress-bar
를 만들어 보려 한다.
|-- components
| `-- ScrollProgress
| |-- ScrollProgress.stories.tsx
| |-- ScrollProgress.styles.ts
| |-- ScrollProgress.tsx
| `-- ScrollProgress.types.ts
|-- libs
| |-- hooks
| | `-- useScrollProgress.ts
| `-- utils
| `-- moveScrollProgress.ts
|-- App.tsx
|-- main.tsx
`-- vite-env.d.ts
현재 viewpoint
가 스크롤바
의 몇 퍼센트 위치에 있는지 계산하는 커스텀 훅이다.
import { useEffect, useState } from "react";
const useScrollProgress = () => {
const [scroll, setScroll] = useState(0);
const handleProgress = () => {
const totalScroll = document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
const scroll = (totalScroll / windowHeight) * 100;
setScroll(scroll);
};
useEffect(() => {
window.addEventListener("scroll", handleProgress);
return () => {
window.removeEventListener("scroll", handleProgress);
};
}, [handleProgress]);
return scroll;
};
export default useScrollProgress;
클릭된 스크롤바
의 지점에 맞게 viewpoint
를 옮기는 함수
/**
*
* @param clientX - x 좌표
* @param scrollWidth - ScrollProgress의 최대 길이
*/
const moveScrollProgress = (clientX: number, scrollWidth: number) => {
window.scrollTo({
top: window.innerHeight * (clientX / scrollWidth),
behavior: "smooth",
});
};
export default moveScrollProgress;
import { DetailedHTMLProps, HTMLAttributes } from "react";
export interface ScrollProgressState
extends DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
colors: string | string[];
progress: number;
}
import useScrollProgress from "@/libs/hooks/useScrollProgress";
import moveScrollProgress from "@/libs/utils/moveScrollProgress";
import { useRef } from "react";
import * as S from "./ScrollProgress.styles";
import { ScrollProgressState } from "./ScrollProgress.types";
/**
* @param colors - ScrollProgress의 색상 변경
** string형식으로 입력 시 ex) colors="blue" => background :"blue"
** string[]형식으로 입력 시 ex) colors=["blue","yellow"] => background : linear-gradient(to left, "blue", "yellow")
*
*
*/
const ScrollProgress = ({
colors,
...rest
}: Omit<ScrollProgressState, "progress">) => {
const scroll = useScrollProgress();
const progressRef = useRef<HTMLDivElement>(null);
const onClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const { clientX } = event;
if (progressRef && progressRef.current) {
const { scrollWidth } = progressRef.current;
moveScrollProgress(clientX, scrollWidth);
}
};
return (
<S.Container {...rest} onClick={onClick} ref={progressRef}>
<S.ProgressBar colors={colors} progress={scroll} />
</S.Container>
);
};
export default ScrollProgress;
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { ScrollProgressState } from "./ScrollProgress.types";
export const Container = styled.div`
width: 100%;
position: fixed;
top: 0;
left: 0;
background: #ccc;
`;
export const ProgressBar = styled.div<ScrollProgressState>`
${({ colors }) =>
Array.isArray(colors)
? css`
background: linear-gradient(to left, ${colors.join(",")});
`
: css`
background: ${colors};
`}
height: 0.5rem;
width: ${({ progress }) => progress + "%"};
`;
import ScrollProgress from "./ScrollProgress";
import { ComponentStory, ComponentMeta } from "@storybook/react";
export default {
title: "ScrollProgress",
components: ScrollProgress,
} as ComponentMeta<typeof ScrollProgress>;
const Template: ComponentStory<typeof ScrollProgress> = (args) => (
<div style={{ height: "200vh" }}>
<ScrollProgress {...args} />
</div>
);
export const ProgressArray = Template.bind({});
ProgressArray.args = {
colors: ["rgb(255, 166, 166)", "rgb(126, 197, 255)"],
};
export const ProgressString = Template.bind({});
ProgressString.args = {
colors: "blue",
};