├── .prettierignore
├── .gitignore
├── postcss.config.js
├── .prettierrc
├── examples
├── src
│ ├── index.tsx
│ ├── Basic.tsx
│ ├── Cut.tsx
│ ├── CounterClockwise.tsx
│ ├── WithPointer.tsx
│ ├── Countdown.scss
│ ├── InverseProgress.tsx
│ ├── Timer.tsx
│ ├── Examples.tsx
│ ├── CustomIndicator.tsx
│ ├── Examples.scss
│ └── Countdown.tsx
└── index.html
├── .eslintrc.js
├── tsconfig.json
├── vite.config.examples.ts
├── CHANGELOG.md
├── vite.config.library.ts
├── LICENSE
├── package.json
├── README.md
└── src
└── index.tsx
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist/
3 | .idea
4 | .awcache
5 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "es5",
4 | "tabWidth": 2,
5 | "semi": true
6 | }
7 |
--------------------------------------------------------------------------------
/examples/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import Examples from './Examples';
4 |
5 | const App = () => ;
6 |
7 | const container = document.getElementById('root') as HTMLElement;
8 | const root = ReactDOM.createRoot(container);
9 | root.render();
10 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React Customizable Progressbar Examples
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | extends: [
4 | 'eslint:recommended',
5 | 'plugin:@typescript-eslint/recommended',
6 | 'plugin:react/recommended',
7 | 'plugin:prettier/recommended',
8 | ],
9 | plugins: ['@typescript-eslint', 'react', 'prettier'],
10 | rules: {
11 | 'prettier/prettier': 'error',
12 | },
13 | settings: {
14 | react: {
15 | version: 'detect',
16 | },
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "jsx": "react-jsx",
6 | "strict": true,
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "outDir": "dist",
11 | "declaration": true,
12 | "declarationMap": true,
13 | "sourceMap": true,
14 | "moduleResolution": "node",
15 | "resolveJsonModule": true
16 | },
17 | "include": ["src", "examples"],
18 | "exclude": ["node_modules", "dist"]
19 | }
20 |
--------------------------------------------------------------------------------
/vite.config.examples.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 | import { resolve } from 'path';
4 |
5 | export default defineConfig({
6 | root: resolve(__dirname, 'examples'),
7 | plugins: [react()],
8 | server: {
9 | port: 3000,
10 | open: true,
11 | },
12 | resolve: {
13 | alias: {
14 | 'react-customizable-progressbar': resolve(__dirname, 'src/index.tsx'),
15 | },
16 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
17 | },
18 | build: {
19 | outDir: resolve(__dirname, 'examples/dist'),
20 | emptyOutDir: true,
21 | },
22 | });
23 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # [2.1.1] - 2025-07-09
2 |
3 | ## Fixes
4 | - React 19 - TypeError - Cannot read properties of undefined (reading 'recentlyCreatedOwnerStacks') #54
5 |
6 | # [2.1.0] - 2025-06-10
7 |
8 | ## Features
9 | - Added support for React 19
10 | - Updated peerDependencies to include React ^19.0.0
11 |
12 | # [2.0.1] - 2024-10-26
13 |
14 | ## Fixes
15 | - Fixed module entry in package.json.
16 | - Fixed generating of types file.
17 |
18 | # [2.0.0] - 2024-08-04
19 |
20 | ## Breaking Changes
21 | - Dropped support for React versions 0.14.0 and 15.x. The library now requires React versions ^16.14.0, ^17.0.0, or ^18.0.0. Projects using this library must update to these versions to avoid compatibility issues.
--------------------------------------------------------------------------------
/vite.config.library.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 | import dts from 'vite-plugin-dts';
4 | import { resolve } from 'path';
5 |
6 | export default defineConfig({
7 | plugins: [react(), dts({ rollupTypes: true })],
8 | build: {
9 | lib: {
10 | entry: resolve(__dirname, 'src/index.tsx'),
11 | name: 'ReactCustomizableProgressbar',
12 | fileName: (format) => `index.${format}.js`,
13 | formats: ['es', 'cjs'],
14 | },
15 | rollupOptions: {
16 | external: ['react', 'react-dom'],
17 | output: {
18 | globals: {
19 | react: 'React',
20 | 'react-dom': 'ReactDOM',
21 | },
22 | },
23 | },
24 | outDir: resolve(__dirname, 'dist'),
25 | emptyOutDir: true,
26 | },
27 | });
28 |
--------------------------------------------------------------------------------
/examples/src/Basic.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3 | // @ts-expect-error
4 | import ProgressBar from 'react-customizable-progressbar';
5 | import { ExampleProps } from './Examples';
6 |
7 | const Basic = ({ progress }: ExampleProps) => (
8 |
9 |
18 |
19 |
27 |
30 |
31 |
32 | );
33 |
34 | export default Basic;
35 |
--------------------------------------------------------------------------------
/examples/src/Cut.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3 | // @ts-expect-error
4 | import ProgressBar from 'react-customizable-progressbar';
5 | import { ExampleProps } from './Examples';
6 |
7 | const Cut = ({ progress }: ExampleProps) => (
8 |
9 |
18 |
19 |
30 |
33 |
34 |
35 | );
36 |
37 | export default Cut;
38 |
--------------------------------------------------------------------------------
/examples/src/CounterClockwise.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3 | // @ts-expect-error
4 | import ProgressBar from 'react-customizable-progressbar';
5 | import { ExampleProps } from './Examples';
6 |
7 | const CounterClockwise = ({ progress }: ExampleProps) => (
8 |
9 |
18 |
19 |
28 |
31 |
32 |
33 | );
34 |
35 | export default CounterClockwise;
36 |
--------------------------------------------------------------------------------
/examples/src/WithPointer.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3 | // @ts-expect-error
4 | import ProgressBar from 'react-customizable-progressbar';
5 | import { ExampleProps } from './Examples';
6 |
7 | const WithPointer = ({ progress }: ExampleProps) => (
8 |
9 |
18 |
19 |
29 |
32 |
33 |
34 | );
35 |
36 | export default WithPointer;
37 |
--------------------------------------------------------------------------------
/examples/src/Countdown.scss:
--------------------------------------------------------------------------------
1 | .countdown {
2 | display: flex;
3 | justify-content: center;
4 |
5 | .indicator-countdown {
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | text-align: center;
10 | position: absolute;
11 | top: 0;
12 | width: 100%;
13 | height: 100%;
14 | margin: 0 auto;
15 | font-size: 1.3em;
16 | }
17 |
18 | .caption {
19 | font-size: 0.8em;
20 | font-weight: 100;
21 | color: #777;
22 | margin-bottom: 5px;
23 | transition: 0.3s ease;
24 |
25 | &.big {
26 | margin-bottom: 0;
27 | font-size: 1.1em;
28 | color: indianred;
29 | animation: blinking 1s step-start 0s infinite;
30 |
31 | span {
32 | display: none;
33 | }
34 | }
35 | }
36 |
37 | .time {
38 | font-size: 1.6em;
39 | font-weight: 100;
40 | max-height: 100px;
41 | transition: 0.3s ease;
42 |
43 | &.hidden {
44 | max-height: 0;
45 | overflow: hidden;
46 | color: white;
47 | }
48 | }
49 |
50 | @keyframes blinking {
51 | 50% {
52 | opacity: 0;
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Martin Juzl
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/examples/src/InverseProgress.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3 | // @ts-expect-error
4 | import ProgressBar from 'react-customizable-progressbar';
5 | import { ExampleProps } from './Examples';
6 |
7 | const InverseProgress = ({ progress }: ExampleProps) => (
8 |
9 |
18 |
19 |
31 |
34 |
35 |
36 | );
37 |
38 | export default InverseProgress;
39 |
--------------------------------------------------------------------------------
/examples/src/Timer.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | interface TimerProps {
4 | initialSeconds: number;
5 | totalSeconds: number;
6 | onChange?: (value: number) => void;
7 | interval: number;
8 | }
9 |
10 | const Timer = ({
11 | initialSeconds,
12 | totalSeconds,
13 | onChange,
14 | interval,
15 | }: TimerProps) => {
16 | const [elapsed, setElapsed] = useState(0);
17 | const [intervalId, setIntervalId] = useState();
18 |
19 | useEffect(() => {
20 | start(intervalId);
21 |
22 | return () => clear(intervalId);
23 | }, []);
24 |
25 | useEffect(() => {
26 | onChange?.(elapsed);
27 | }, [elapsed]);
28 |
29 | const start = (intervalId: number | undefined) => {
30 | clear(intervalId);
31 |
32 | const newIntervalId = window.setInterval(() => {
33 | if (elapsed + initialSeconds === totalSeconds) return;
34 |
35 | setElapsed((elapsed) => elapsed + 1);
36 | setIntervalId(newIntervalId);
37 | }, interval);
38 | };
39 |
40 | const clear = (intervalId: number | undefined) => {
41 | if (intervalId !== undefined) {
42 | window.clearInterval(intervalId);
43 | }
44 | };
45 |
46 | return null;
47 | };
48 |
49 | export default Timer;
50 |
--------------------------------------------------------------------------------
/examples/src/Examples.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Basic from './Basic';
3 | import CounterClockwise from './CounterClockwise';
4 | import Cut from './Cut';
5 | import WithPointer from './WithPointer';
6 | import InverseProgress from './InverseProgress';
7 | import CustomIndicator from './CustomIndicator';
8 | import Countdown from './Countdown';
9 | import './Examples.scss';
10 |
11 | export interface ExampleProps {
12 | progress: number;
13 | }
14 |
15 | const Examples = () => {
16 | const [progress, setProgress] = useState(64);
17 |
18 | return (
19 |
20 |
21 |
Progress
22 |
setProgress(parseInt(e.target.value, 10))}
26 | min={0}
27 | max={100}
28 | />
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default Examples;
45 |
--------------------------------------------------------------------------------
/examples/src/CustomIndicator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3 | // @ts-expect-error
4 | import ProgressBar from 'react-customizable-progressbar';
5 | import { ExampleProps } from './Examples';
6 |
7 | const CustomIndicator = ({ progress }: ExampleProps) => (
8 |
9 |
18 |
19 |
35 |
36 |
37 |
38 |
39 |
40 |
{progress}%
41 |
42 |
43 |
44 |
45 | );
46 |
47 | export default CustomIndicator;
48 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-customizable-progressbar",
3 | "version": "2.1.1",
4 | "description": "Customizable circular SVG progress bar component for React",
5 | "main": "./dist/index.cjs.js",
6 | "module": "./dist/index.es.js",
7 | "types": "./dist/index.d.ts",
8 | "exports:": {
9 | "import": "./dist/index.es.js",
10 | "require": "./dist/index.cjs.js"
11 | },
12 | "files": [
13 | "dist"
14 | ],
15 | "scripts": {
16 | "lint": "eslint src --ext ts,tsx",
17 | "build:library": "vite build --config vite.config.library.ts",
18 | "dev:examples": "vite --config vite.config.examples.ts"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/martyan/react-customizable-progressbar.git"
23 | },
24 | "keywords": [
25 | "react",
26 | "circular",
27 | "progress",
28 | "bar",
29 | "component",
30 | "customizable",
31 | "svg",
32 | "animation"
33 | ],
34 | "homepage": "https://martinjuzl.com/react-customizable-progressbar",
35 | "author": "Martin Juzl",
36 | "license": "MIT",
37 | "devDependencies": {
38 | "@types/react": "^18.3.23",
39 | "@types/react-dom": "^18.3.7",
40 | "@typescript-eslint/eslint-plugin": "^8.0.0",
41 | "@typescript-eslint/parser": "^8.0.0",
42 | "@vitejs/plugin-react": "^3.1.0",
43 | "autoprefixer": "^10.4.12",
44 | "eslint": "^8.57.0",
45 | "eslint-config-prettier": "^9.1.0",
46 | "eslint-plugin-prettier": "^5.2.1",
47 | "eslint-plugin-react": "^7.32.1",
48 | "moment": "^2.30.1",
49 | "postcss": "^8.4.16",
50 | "postcss-loader": "^7.0.1",
51 | "prettier": "^3.3.3",
52 | "react": "^18.3.1",
53 | "react-dom": "^18.3.1",
54 | "sass": "^1.77.8",
55 | "typescript": "^5.2.2",
56 | "vite": "^4.5.0",
57 | "vite-plugin-dts": "^4.3.0",
58 | "vite-plugin-eslint": "^1.6.0"
59 | },
60 | "peerDependencies": {
61 | "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
62 | "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/examples/src/Examples.scss:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500');
2 | @import url('https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css');
3 |
4 | .examples {
5 | font-family: 'Roboto', sans-serif;
6 |
7 | .slider {
8 | max-width: 280px;
9 | display: none;
10 | margin: 30px auto;
11 | justify-content: space-between;
12 | align-items: center;
13 |
14 | .desc {
15 | display: inline-block;
16 | font-size: 1.05em;
17 | color: #777;
18 | font-weight: 300;
19 | }
20 |
21 | input {
22 | display: inline-block;
23 | }
24 |
25 | @media (min-width: 480px) {
26 | display: flex;
27 | }
28 | }
29 |
30 | .list {
31 | display: flex;
32 | flex-wrap: wrap;
33 | justify-content: center;
34 |
35 | .item {
36 | flex-basis: 236px;
37 | flex-shrink: 0;
38 | flex-grow: 0;
39 | padding: 5px 15px;
40 | background-color: white;
41 | border-radius: 5px;
42 | margin: 30px;
43 |
44 | .title {
45 | display: flex;
46 | justify-content: space-between;
47 | align-items: center;
48 | padding: 15px 5px;
49 | margin-bottom: 20px;
50 | font-weight: 400;
51 | text-transform: uppercase;
52 | color: #666;
53 | border-bottom: 1px solid #f5f5f5;
54 | font-size: 0.9em;
55 |
56 | span {
57 | flex: 1;
58 | margin-right: 10px;
59 | }
60 |
61 | a {
62 | text-decoration: none;
63 | color: #ccc;
64 | font-size: 0.9em;
65 | }
66 | }
67 |
68 | .indicator {
69 | display: flex;
70 | align-items: center;
71 | justify-content: center;
72 | text-align: center;
73 | position: absolute;
74 | top: 0;
75 | width: 100%;
76 | height: 100%;
77 | margin: 0 auto;
78 | font-size: 2.2em;
79 | font-weight: 100;
80 | color: #555;
81 | user-select: none;
82 | }
83 |
84 | .indicator-volume {
85 | display: flex;
86 | align-items: flex-end;
87 | justify-content: center;
88 | text-align: center;
89 | position: absolute;
90 | top: 0;
91 | width: 100%;
92 | height: 100%;
93 | margin: 0 auto;
94 | font-size: 1.3em;
95 |
96 | .inner {
97 | margin-bottom: 30px;
98 | }
99 |
100 | .percentage {
101 | font-size: 1.6em;
102 | color: #bbb;
103 | font-weight: 100;
104 | }
105 |
106 | .icon {
107 | font-size: 3em;
108 | color: #5d9cec;
109 | margin-bottom: 15px;
110 | }
111 | }
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/examples/src/Countdown.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3 | // @ts-expect-error
4 | import ProgressBar from 'react-customizable-progressbar';
5 | import moment, { Moment } from 'moment';
6 | import Timer from './Timer';
7 | import './Countdown.scss';
8 |
9 | const totalSeconds = 60;
10 | const initialSeconds = 15;
11 | const initialProgress = (initialSeconds / totalSeconds) * 100;
12 |
13 | const getText = (date: Moment) => {
14 | const h = date.hour();
15 | const m = date.minute();
16 |
17 | if (h) return date.format('h[h] m[m] s[s]');
18 | else if (m) return date.format('m[m] s[s]');
19 | else return date.format('s[s]');
20 | };
21 |
22 | interface IndicatorProps {
23 | elapsedSeconds: number;
24 | }
25 |
26 | const Indicator = ({ elapsedSeconds }: IndicatorProps) => {
27 | const seconds = totalSeconds - elapsedSeconds - initialSeconds;
28 | const date = moment().startOf('day').seconds(seconds);
29 |
30 | return (
31 |
32 |
33 |
0 ? 'caption' : 'caption big'}>
34 | Popcorn ready in
35 |
36 |
0 ? 'time' : 'time hidden'}>
37 | {getText(date)}
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | const Countdown = () => {
45 | const [elapsedSeconds, setElapsedSeconds] = useState(0);
46 | const [progress, setProgress] = useState(initialProgress);
47 |
48 | const roundProgress = (progress: number) => {
49 | const factor = Math.pow(10, 2);
50 | return Math.round(progress * factor) / factor;
51 | };
52 |
53 | const handleTimer = (elapsedSeconds: number) => {
54 | const progress = roundProgress(
55 | ((elapsedSeconds + initialSeconds) / totalSeconds) * 100
56 | );
57 |
58 | setProgress(progress);
59 | setElapsedSeconds(elapsedSeconds);
60 | };
61 |
62 | return (
63 |
64 |
74 |
75 |
76 |
88 |
89 |
90 |
91 |
97 |
98 |
99 | );
100 | };
101 |
102 | export default Countdown;
103 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-customizable-progressbar
2 |
3 | Customizable circular SVG progress bar component for React
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Check examples or generator to play around with all props
12 |
13 | ### Installation
14 |
15 | ```bash
16 | npm install --save react-customizable-progressbar
17 | ```
18 |
19 | or
20 |
21 | ```bash
22 | yarn add react-customizable-progressbar
23 | ```
24 |
25 | ### Usage
26 |
27 | ```jsx
28 | import ProgressBar from 'react-customizable-progressbar';
29 |
30 | ;
31 | ```
32 |
33 | ### Props
34 |
35 | | Name | Type | Default | Description |
36 | | ----------------------- | -------- | ------------- | ---------------------------------------------------- |
37 | | `radius` (required) | `number` | `100` | Progress bar radius |
38 | | `progress` (required) | `number` | `0` | Progress value (out of `steps`) |
39 | | `steps` | `number` | `100` | Total steps |
40 | | `cut` | `number` | `0` | Angle of the circle sector |
41 | | `rotate` | `number` | `-90` | Progress rotation |
42 | | `strokeWidth` | `number` | `20` | Stroke width |
43 | | `strokeColor` | `string` | `'indianred'` | Stroke color |
44 | | `strokeLinecap` | `string` | `'round'` | Stroke line cap |
45 | | `transition` | `string` | `'0.3s ease'` | Transition |
46 | | `trackStrokeWidth` | `number` | `20` | Track stroke width |
47 | | `trackStrokeColor` | `string` | `'#e6e6e6'` | Track stroke color |
48 | | `trackStrokeLinecap` | `string` | `'round'` | Track stroke line cap |
49 | | `trackTransition` | `string` | `'1s ease'` | Track transition |
50 | | `pointerRadius` | `number` | `0` | Pointer radius |
51 | | `pointerStrokeWidth` | `number` | `20` | Pointer stroke width |
52 | | `pointerStrokeColor` | `string` | `'indianred'` | Pointer stroke color |
53 | | `pointerFillColor` | `string` | `'white'` | Pointer fill color |
54 | | `initialAnimation` | `bool` | `false` | Initial animation |
55 | | `initialAnimationDelay` | `number` | `0` | Initial animation delay |
56 | | `inverse` | `bool` | `false` | Inverse |
57 | | `counterClockwise` | `bool` | `false` | Counter-clockwise |
58 | | `children` | `node` | `null` | Children - pass anything to show inside progress bar |
59 | | `className` | `string` | `''` | Progress bar class name |
60 |
61 | ### Styles
62 |
63 | ```css
64 | .RCP {
65 | }
66 | .RCP__track {
67 | }
68 | .RCP__progress {
69 | }
70 | .RCP__pointer {
71 | }
72 | ```
73 |
74 | You can use these default indicator styles to center it both horizontally and vertically:
75 |
76 | ```css
77 | .your-indicator {
78 | display: flex;
79 | justify-content: center;
80 | text-align: center;
81 | position: absolute;
82 | top: 0;
83 | width: 100%;
84 | height: 100%;
85 | margin: 0 auto;
86 | user-select: none;
87 | }
88 | ```
89 |
90 | ### Run examples locally
91 |
92 | ```bash
93 | npm install
94 | npm run dev
95 | ```
96 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | FunctionComponent,
3 | ReactNode,
4 | useEffect,
5 | useState,
6 | } from 'react';
7 |
8 | type ReactCustomizableProgressbarProps = {
9 | radius: number;
10 | progress: number;
11 | steps?: number;
12 | cut?: number;
13 | rotate?: number;
14 | strokeWidth?: number;
15 | strokeColor?: string;
16 | fillColor?: string;
17 | strokeLinecap?: 'round' | 'inherit' | 'butt' | 'square';
18 | transition?: string;
19 | pointerRadius?: number;
20 | pointerStrokeWidth?: number;
21 | pointerStrokeColor?: string;
22 | pointerFillColor?: string;
23 | trackStrokeColor?: string;
24 | trackStrokeWidth?: number;
25 | trackStrokeLinecap?: 'round' | 'inherit' | 'butt' | 'square';
26 | trackTransition?: string;
27 | counterClockwise?: boolean;
28 | inverse?: boolean;
29 | initialAnimation?: boolean;
30 | initialAnimationDelay?: number;
31 | className?: string;
32 | children?: ReactNode;
33 | };
34 |
35 | const Index: FunctionComponent = ({
36 | radius = defaultProps.radius,
37 | progress = defaultProps.progress,
38 | steps = defaultProps.steps,
39 | cut = defaultProps.cut,
40 | rotate = defaultProps.rotate,
41 | strokeWidth = defaultProps.strokeWidth,
42 | strokeColor = defaultProps.strokeColor,
43 | fillColor = defaultProps.fillColor,
44 | strokeLinecap = defaultProps.strokeLinecap,
45 | transition = defaultProps.transition,
46 | pointerRadius = defaultProps.pointerRadius,
47 | pointerStrokeWidth = defaultProps.pointerStrokeWidth,
48 | pointerStrokeColor = defaultProps.pointerStrokeColor,
49 | pointerFillColor = defaultProps.pointerFillColor,
50 | trackStrokeColor = defaultProps.trackStrokeColor,
51 | trackStrokeWidth = defaultProps.trackStrokeWidth,
52 | trackStrokeLinecap = defaultProps.trackStrokeLinecap,
53 | trackTransition = defaultProps.trackTransition,
54 | counterClockwise = defaultProps.counterClockwise,
55 | inverse = defaultProps.inverse,
56 | initialAnimation = defaultProps.initialAnimation,
57 | initialAnimationDelay = defaultProps.initialAnimationDelay,
58 | className = defaultProps.className,
59 | children = defaultProps.children,
60 | }) => {
61 | const [animationInited, setAnimationInited] = useState(false);
62 |
63 | useEffect(() => {
64 | let timeout: NodeJS.Timeout;
65 | if (initialAnimation)
66 | timeout = setTimeout(
67 | () => setAnimationInited(true),
68 | initialAnimationDelay
69 | );
70 |
71 | return () => clearTimeout(timeout);
72 | }, []);
73 |
74 | const getProgress = () =>
75 | initialAnimation && !animationInited ? 0 : progress;
76 |
77 | const getStrokeDashoffset = (strokeLength: number) => {
78 | const progress = getProgress();
79 | const progressLength = (strokeLength / steps!) * (steps! - progress);
80 |
81 | if (inverse) {
82 | return counterClockwise ? 0 : progressLength - strokeLength;
83 | }
84 |
85 | return counterClockwise ? -1 * progressLength : progressLength;
86 | };
87 |
88 | const getStrokeDashArray = (strokeLength: number, circumference: number) => {
89 | const progress = getProgress();
90 | const progressLength = (strokeLength / steps!) * (steps! - progress);
91 |
92 | if (inverse) {
93 | return `${progressLength}, ${circumference}`;
94 | }
95 |
96 | return counterClockwise
97 | ? `${strokeLength * (progress / 100)}, ${circumference}`
98 | : `${strokeLength}, ${circumference}`;
99 | };
100 |
101 | const getTrackStrokeDashArray = (
102 | strokeLength: number,
103 | circumference: number
104 | ) => {
105 | if (initialAnimation && !animationInited) {
106 | return `0, ${circumference}`;
107 | }
108 |
109 | return `${strokeLength}, ${circumference}`;
110 | };
111 |
112 | const getExtendedWidth = () => {
113 | const pointerWidth = pointerRadius! + pointerStrokeWidth!;
114 |
115 | if (pointerWidth > strokeWidth! && pointerWidth > trackStrokeWidth!) {
116 | return pointerWidth * 2;
117 | } else if (strokeWidth! > trackStrokeWidth!) {
118 | return strokeWidth! * 2;
119 | }
120 |
121 | return trackStrokeWidth! * 2;
122 | };
123 |
124 | const getPointerAngle = () => {
125 | const progress = getProgress();
126 |
127 | return counterClockwise
128 | ? ((360 - cut!) / steps!) * (steps! - progress)
129 | : ((360 - cut!) / steps!) * progress;
130 | };
131 |
132 | const d = 2 * radius;
133 | const width = d + getExtendedWidth();
134 |
135 | const circumference = 2 * Math.PI * radius;
136 | const strokeLength = (circumference / 360) * (360 - cut!);
137 |
138 | return (
139 |
146 |
203 |
204 | {children || null}
205 |
206 | );
207 | };
208 |
209 | const defaultProps: ReactCustomizableProgressbarProps = {
210 | radius: 100,
211 | progress: 0,
212 | steps: 100,
213 | cut: 0,
214 | rotate: -90,
215 | strokeWidth: 20,
216 | strokeColor: 'indianred',
217 | fillColor: 'none',
218 | strokeLinecap: 'round',
219 | transition: '.3s ease',
220 | pointerRadius: 0,
221 | pointerStrokeWidth: 20,
222 | pointerStrokeColor: 'indianred',
223 | pointerFillColor: 'white',
224 | trackStrokeColor: '#e6e6e6',
225 | trackStrokeWidth: 20,
226 | trackStrokeLinecap: 'round',
227 | trackTransition: '.3s ease',
228 | counterClockwise: false,
229 | inverse: false,
230 | initialAnimation: false,
231 | initialAnimationDelay: 0,
232 | className: '',
233 | };
234 |
235 | export default Index;
236 |
--------------------------------------------------------------------------------