├── .gitignore
├── .npmignore
├── README.md
├── package.json
├── postcss.config.js
├── rollup-dev.config.mjs
├── rollup.config.mjs
├── src
├── KoFiButton.js
├── KoFiDialog.js
├── KoFiPanel.js
├── KoFiWidget.js
├── index.js
├── kofi.css
└── styles.css
└── tailwind.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist/
3 | .DS_Store
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Ko-Fi ☕
2 |
3 | React Ko-Fi is a collection of React components that displays various Ko-Fi widgets in React:
4 |
5 | 1. **KoFiDialog**: A Ko-Fi button that opens dialog displaying the Ko-Fi widget.
6 | 2. **KoFiButton**: A button that opens a Ko-Fi website in a new tab.
7 | 3. **KoFiWidget**: The Ko-Fi floating widget as a React component.
8 | 4. **KoFiPanel**: The Ko-Fi panel widget as a React component.
9 |
10 | These components are made by [Prototypr](https://prototypr.io) and make it easier to display and customise Ko-Fi widgets in your React project. There's also an additional dialog component that opens a Ko-Fi widget in a modal.
11 |
12 | `npm install react-kofi`
13 |
14 | To use it in your project, import the components you need, along with the css:
15 |
16 | ```jsx
17 | import { KoFiDialog, KoFiButton, KoFiWidget, KoFiPanel } from "react-kofi";
18 | import "react-kofi/dist/styles.css";
19 | ```
20 |
21 | #### Support us on Ko-Fi
22 |
23 | If you find this package useful, you can support us on Ko-Fi too (or just star the repo):
24 |
25 | [](https://ko-fi.com/Y8Y71QU45)
26 |
27 | ---
28 |
29 | ### KofiDialog
30 |
31 | The KofiDialog component displays a Ko-Fi button that opens a dialog containing the Ko-Fi donation panel.
32 | It's a modal that opens when the user clicks the button.
33 |
34 | Created with a [Radix Dialog](https://www.radix-ui.com/docs/components/dialog), this component essentially embeds the Ko-Fi Panel inside a dialog, so you can present the panel on click:
35 |
36 | 
37 |
38 | ```jsx
39 |
49 | ```
50 |
51 | | Prop | Description | Default Value |
52 | | ------------ | -------------------------------------------------------------------------------------------------- | ------------- |
53 | | color | Background color of the Ko-Fi button | "#00b4f7" |
54 | | textColor | Text color of the Ko-Fi button | "#fff" |
55 | | id | Your Ko-Fi ID (see your profile url) | "prototypr" |
56 | | label | Text displayed on the Ko-Fi button | "Support me" |
57 | | padding | Padding around the dialog content | 0 |
58 | | width | Width of the dialog | 400 |
59 | | iframe | Not needed if you supply the id prop. Here you can paste in the iframe code from the Ko-Fi website | false |
60 | | buttonRadius | Border radius of the Ko-Fi button | "8px" |
61 |
62 | ---
63 |
64 | ### KoFiButton
65 |
66 | Just a button that opens a Ko-Fi website in a new tab. The button is styled with a bit of tailwindcss and a separate kofi css sheet.
67 |
68 | 
69 |
70 | ```jsx
71 |
77 | ```
78 |
79 | | Prop | Description | Default Value |
80 | | ------ | ------------------------------------ | --------------------- |
81 | | color | Background color of the Ko-Fi button | "#00b4f7" |
82 | | id | Your Ko-Fi ID (see your profile url) | "prototypr" |
83 | | label | Text displayed on the Ko-Fi button | "Support me on Ko-Fi" |
84 | | radius | Border radius of the Ko-Fi button | "12px" |
85 |
86 | ---
87 |
88 | ### KoFiWidget
89 |
90 | The KoFiWidget component displays a Ko-Fi floating widget as a React component. The Ko-Fi button is attached to the bottom left of the page, and you can click it to open the donation panel.
91 |
92 | 
93 |
94 |
95 | ```jsx
96 |
103 | ```
104 |
105 | | Prop | Description | Default Value |
106 | | -------------- | -------------------------------------- | ------------- |
107 | | attachOnScroll | Whether to attach the widget on scroll | false |
108 | | id | Your Ko-Fi ID (see your profile url) | "prototypr" |
109 | | label | Text displayed on the Ko-Fi widget | "Support me" |
110 | | color | Background color of the Ko-Fi widget | "#00b9fe" |
111 | | textColor | Text color of the Ko-Fi widget | "#fff" |
112 |
113 | **Note**: if you use `attachOnScroll`, the Ko-Fi widget script loads and attaches only when you scroll down the page. This is useful if you don't want to load the widget straight away which can be better for page load performance.
114 |
115 | ---
116 |
117 | ### KoFiPanel
118 |
119 | The KoFiPanel component displays the full Ko-Fi donation panel as a React component.
120 |
121 | 
122 |
123 |
124 | ```jsx
125 |
126 | ```
127 |
128 | ---
129 |
130 | ## Optional for tailwind projects
131 |
132 | The general styles (e.g. the modal, padding, etc.) are implemented with tailwindcss, and then a separate kofi.css stylesheet is used to style the Ko-Fi widget.
133 |
134 | If your project already uses tailwindcss, you can add this to your tailwind config:
135 |
136 | ```
137 | module.exports = {
138 | content: ["./node_modules/react-kofi/dist/**/*.{js,ts,jsx,tsx}",],
139 | ```
140 |
141 | and then only import the kofi stylesheet separately to avoid conflicts:
142 |
143 | ```jsx
144 | import "react-kofi/dist/kofi.css";
145 | ```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-kofi",
3 | "version": "0.0.2",
4 | "description": "Ko-fi buttons, modals, and react components built with Radix for React.js projects (unofficial)",
5 | "main": "dist/cjs/index.js",
6 | "module": "dist/esm/index.js",
7 | "scripts": {
8 | "build": "rollup -c",
9 | "build-dev": "rollup --config rollup-dev.config.mjs",
10 | "prepublishOnly": "npm run build",
11 | "watch": "rollup --config rollup-dev.config.mjs --watch",
12 | "clean": "rm -rf node_modules",
13 | "test": "echo \"Error: no test specified\" && exit 1",
14 | "reinstall": "npm run clean && npm install"
15 | },
16 | "keywords": [
17 | "ko-fi",
18 | "donation",
19 | "react",
20 | "radix"
21 | ],
22 | "author": "Graeme Fulton",
23 | "license": "MIT",
24 | "dependencies": {
25 | "@radix-ui/react-dialog": "^1.1.1",
26 | "@radix-ui/react-icons": "^1.3.0"
27 | },
28 | "peerDependencies": {
29 | "react": "^18.3.1",
30 | "react-dom": "^18.3.1"
31 | },
32 | "devDependencies": {
33 | "@babel/cli": "^7.24.8",
34 | "@babel/core": "^7.24.9",
35 | "@babel/plugin-transform-runtime": "^7.24.7",
36 | "@babel/preset-env": "^7.x.x",
37 | "@babel/preset-react": "^7.x.x",
38 | "@rollup/plugin-commonjs": "^26.0.1",
39 | "@rollup/plugin-json": "^6.1.0",
40 | "@rollup/plugin-node-resolve": "^15.2.3",
41 | "autoprefixer": "^10.4.19",
42 | "babel-plugin-module-resolver": "^5.0.2",
43 | "clean-css": "^5.3.3",
44 | "cssnano": "^7.0.5",
45 | "postcss": "^8.4.40",
46 | "react-dom": "^18.3.1",
47 | "rollup": "^2.79.1",
48 | "rollup-plugin-babel": "^4.4.0",
49 | "rollup-plugin-copy": "^3.5.0",
50 | "rollup-plugin-peer-deps-external": "^2.2.4",
51 | "rollup-plugin-postcss": "^4.0.2",
52 | "rollup-plugin-terser": "^7.0.2",
53 | "tailwindcss": "^3.4.6"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/rollup-dev.config.mjs:
--------------------------------------------------------------------------------
1 | import babel from "rollup-plugin-babel";
2 | import resolve from "@rollup/plugin-node-resolve";
3 | import external from "rollup-plugin-peer-deps-external";
4 | import commonjs from "@rollup/plugin-commonjs"; // Ensure you have this import
5 |
6 | import postcss from "rollup-plugin-postcss";
7 | import json from "@rollup/plugin-json"; // Add this import
8 |
9 | import autoprefixer from "autoprefixer";
10 | import tailwindcss from "tailwindcss";
11 |
12 | export default [
13 | {
14 | input: "./src/index.js",
15 | watch: {
16 | include: "./src/**",
17 | clearScreen: false,
18 | },
19 | output: [
20 | {
21 | dir: "dist/esm",
22 | format: "es",
23 | exports: "named",
24 | preserveModules: false,
25 | },
26 | {
27 | dir: "dist/cjs",
28 | format: "cjs",
29 | exports: "named",
30 | preserveModules: false,
31 | },
32 | {
33 | file: "dist/styles.css", // Add this output for CSS
34 | format: "es",
35 | },
36 | ],
37 | plugins: [
38 | postcss({
39 | plugins: [
40 | tailwindcss("./tailwind.config.js"), // path to your tailwind.config.js
41 | autoprefixer(),
42 | ],
43 | extract: "styles.css", // Add this line to extract CSS
44 | minimize: true,
45 | }),
46 | babel({
47 | runtimeHelpers: true,
48 |
49 | exclude: "node_modules/**",
50 | presets: ["@babel/preset-react"],
51 | }),
52 | commonjs({
53 | sourceMap: false
54 | }),
55 | external({
56 | includeDependencies: false,
57 | }),
58 | resolve(),
59 | json(),
60 | // ,
61 | // terser()
62 | ],
63 | },
64 | ];
65 |
--------------------------------------------------------------------------------
/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import babel from "rollup-plugin-babel";
2 | import resolve from "@rollup/plugin-node-resolve";
3 | import external from "rollup-plugin-peer-deps-external";
4 | import commonjs from "@rollup/plugin-commonjs"; // Ensure you have this import
5 |
6 | import postcss from "rollup-plugin-postcss";
7 | import json from "@rollup/plugin-json"; // Add this import
8 | import { terser } from "rollup-plugin-terser";
9 |
10 | import autoprefixer from "autoprefixer";
11 | import tailwindcss from "tailwindcss";
12 | import copy from "rollup-plugin-copy";
13 |
14 | export default [
15 | {
16 | input: "./src/index.js",
17 | watch: {
18 | include: "./src/**",
19 | clearScreen: false,
20 | },
21 | output: [
22 | {
23 | dir: "dist/esm",
24 | format: "es",
25 | exports: "named",
26 | preserveModules: false,
27 | },
28 | {
29 | dir: "dist/cjs",
30 | format: "cjs",
31 | exports: "named",
32 | preserveModules: false,
33 | },
34 | {
35 | file: "dist/styles.css", // Add this output for CSS
36 | format: "es",
37 | }
38 | ],
39 | plugins: [
40 | postcss({
41 | plugins: [
42 | tailwindcss("./tailwind.config.js"), // path to your tailwind.config.js
43 | autoprefixer(),
44 | ],
45 | extract: "styles.css", // Change this line
46 | minimize: true,
47 | }),
48 | copy({
49 | targets: [
50 | { src: 'src/kofi.css', dest: 'dist' } // Adjust the source path as needed
51 | ]
52 | }),
53 | babel({
54 | runtimeHelpers: true,
55 |
56 | exclude: "node_modules/**",
57 | presets: ["@babel/preset-react"],
58 | }),
59 | commonjs({
60 | sourceMap: false
61 | }),
62 | external(),
63 | resolve(),
64 | json(),
65 | terser(),
66 | ],
67 | },
68 | ];
--------------------------------------------------------------------------------
/src/KoFiButton.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | export default function KoFiButton({
3 | color = "#00b4f7",
4 | id = "prototypr",
5 | label = "Support me on Ko-Fi",
6 | size = "w-10 h-10",
7 | radius="12px"
8 | }) {
9 | return (
10 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/KoFiDialog.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import * as Dialog from "@radix-ui/react-dialog";
3 | import { Cross2Icon } from "@radix-ui/react-icons";
4 |
5 | const KoFiDialog = ({
6 | color = "#00b4f7",
7 | textColor = "#fff",
8 | id = "prototypr",
9 | label = "Support me",
10 | padding = 0,
11 | width = 400,
12 | backgroundColor = "#fff",
13 | iframe,
14 | buttonRadius = "8px",
15 | }) => {
16 | const [isOpen, setIsOpen] = useState(false);
17 |
18 | return (
19 |
20 |
21 | setIsOpen(!isOpen)}
26 | >
27 |
28 |

33 |
37 | {label}
38 |
39 |
40 |
41 |
42 |
43 |
44 |
52 |
53 |
54 |
55 |
56 |

61 |
62 | Loading...
63 |
64 |
65 |
66 |
67 |
68 | {iframe ? (
69 |
77 | ) : (
78 |
89 | )}
90 |
91 |
92 |
93 |
96 |
97 |
98 |
99 |
100 | );
101 | };
102 |
103 | export default KoFiDialog;
104 |
--------------------------------------------------------------------------------
/src/KoFiPanel.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function KoFiPanel({ id = "prototypr" }) {
4 | return (
5 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/KoFiWidget.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 |
3 | export default function KoFiWidget({
4 | attachOnScroll = false,
5 | id = "prototypr",
6 | label = "Support me",
7 | color = "#00b9fe",
8 | textColor = "#fff",
9 | }) {
10 | useEffect(() => {
11 | if (typeof window !== "undefined") {
12 | //if there's no ko-fi button added
13 | if (!window.kofiWidgetOverlay) {
14 | if (attachOnScroll) {
15 | //attach scroll listener, so ko-fi scripts and button can be added on scroll
16 | window.addEventListener("scroll", kofiScrollListener, false);
17 | // Remove event listener on unmount
18 | return () => {
19 | window.removeEventListener("scroll", kofiScrollListener, false);
20 | };
21 | } else {
22 | openKofi();
23 | }
24 | }
25 | }
26 | }, []);
27 |
28 | /**
29 | * kofiScrollListener
30 | * Detect if scrolled past halfway, and add button
31 | */
32 | function kofiScrollListener() {
33 | if (window.innerHeight + window.scrollY >= document.body.scrollHeight / 2) {
34 | openKofi();
35 | }
36 | }
37 |
38 | /**
39 | * openKofi
40 | * called from the scroll listener - adds all the ko-fi scripts
41 | * also removes the scroll listener, so it only happens once
42 | */
43 | function openKofi() {
44 | if (attachOnScroll) {
45 | window.removeEventListener("scroll", kofiScrollListener, false);
46 | }
47 | var kofiLoaderScript = document.createElement("script");
48 | kofiLoaderScript.setAttribute(
49 | "src",
50 | "https://storage.ko-fi.com/cdn/scripts/overlay-widget.js"
51 | );
52 | kofiLoaderScript.setAttribute("type", "text/javascript");
53 | document.head.appendChild(kofiLoaderScript);
54 |
55 | kofiLoaderScript.onload = () => {
56 | var kofiButtonScript = document.createElement("script");
57 | kofiButtonScript.setAttribute("type", "text/javascript");
58 | kofiButtonScript.innerHTML = `
59 | window.kofiWidgetOverlay = kofiWidgetOverlay
60 | kofiWidgetOverlay.draw('${id}', {
61 | 'type': 'floating-chat',
62 | 'floating-chat.donateButton.text': '${label}',
63 | 'floating-chat.donateButton.background-color': '${color}',
64 | 'floating-chat.donateButton.text-color': '${textColor}'
65 | });
66 | `;
67 | document.head.appendChild(kofiButtonScript);
68 | };
69 | }
70 | /**
71 | * closeKofi
72 | * Removes the Ko-fi widget scripts from the document
73 | */
74 | function closeKofi() {
75 | // Remove the overlay widget script
76 | const overlayScript = document.querySelector(
77 | 'script[src="https://storage.ko-fi.com/cdn/scripts/overlay-widget.js"]'
78 | );
79 | if (overlayScript) {
80 | overlayScript.remove();
81 | }
82 |
83 | // Remove the button script
84 | const buttonScript = document.querySelector("script:not([src])");
85 | if (
86 | buttonScript &&
87 | buttonScript.innerHTML.includes("kofiWidgetOverlay.draw")
88 | ) {
89 | buttonScript.remove();
90 | }
91 |
92 | // Remove any Ko-fi elements from the DOM
93 | const kofiElements = document.querySelectorAll(
94 | '[class^="floatingchat-container-"]'
95 | );
96 | kofiElements.forEach(element => element.remove());
97 |
98 | // Clean up any global variables or event listeners if necessary
99 | if (window.kofiWidgetOverlay) {
100 | delete window.kofiWidgetOverlay;
101 | }
102 | }
103 |
104 | return null;
105 | }
106 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import './styles.css';
2 | import './kofi.css';
3 |
4 | import KoFiDialog from "./KoFiDialog";
5 | import KoFiButton from "./KoFiButton";
6 | import KoFiWidget from "./KoFiWidget";
7 | import KoFiPanel from './KoFiPanel';
8 |
9 | export { KoFiDialog, KoFiButton, KoFiWidget, KoFiPanel };
--------------------------------------------------------------------------------
/src/kofi.css:
--------------------------------------------------------------------------------
1 | img.kofiimg {
2 | display: initial ;
3 | vertical-align: middle;
4 | height: 13px ;
5 | width: 20px ;
6 | padding-top: 0;
7 | padding-bottom: 0;
8 | border: none;
9 | margin-top: 0;
10 | margin-right: 2px;
11 | margin-left: 0;
12 | margin-bottom: 0px;
13 | content: url("https://ko-fi.com/img/cup-border.png");
14 | }
15 | .kofiimg:after {
16 | vertical-align: middle;
17 | height: 25px;
18 | padding-top: 0;
19 | padding-bottom: 0;
20 | border: none;
21 | margin-top: 0;
22 | margin-right: 6px;
23 | margin-left: 0;
24 | margin-bottom: 4px ;
25 | content: url("https://ko-fi.com/img/whitelogo.svg");
26 | }
27 | .btn-container {
28 | display: inline-block ;
29 | white-space: nowrap;
30 | /* min-width: 160px; */
31 | }
32 | span.kofitext {
33 | color: #fff ;
34 | letter-spacing: -0.15px ;
35 | vertical-align: middle;
36 | line-height: 33px ;
37 | padding: 0;
38 | text-align: center;
39 | text-decoration: none ;
40 | text-shadow: 0 1px 1px rgba(34, 34, 34, 0.05);
41 | }
42 | .kofitext a {
43 | color: #fff ;
44 | text-decoration: none ;
45 | }
46 | .kofitext a:hover {
47 | color: #fff ;
48 | text-decoration: none;
49 | }
50 | a.kofi-button {
51 | box-shadow: 1px 1px 0px rgba(0, 0, 0, 0.2);
52 | line-height: unset ;
53 | display: inline-block ;
54 | background-color: #29abe0;
55 | padding: 2px 12px ;
56 | text-align: center ;
57 | border-radius: 12px;
58 | color: #fff;
59 | cursor: pointer;
60 | overflow-wrap: break-word;
61 | vertical-align: middle;
62 | border: 0 none #fff ;
63 | font-family: "Quicksand", Helvetica, Century Gothic, sans-serif ;
64 | text-decoration: none;
65 | text-shadow: none;
66 | font-weight: 700 ;
67 | font-size: 14px ;
68 | }
69 | a.kofi-button:visited {
70 | color: #fff ;
71 | text-decoration: none ;
72 | }
73 | a.kofi-button:hover {
74 | opacity: 0.85;
75 | color: #f5f5f5 ;
76 | text-decoration: none ;
77 | }
78 | a.kofi-button:active {
79 | color: #f5f5f5 ;
80 | text-decoration: none ;
81 | }
82 | .kofitext img.kofiimg {
83 | height: auto ;
84 | width: 22px ;
85 | margin-right: 6px;
86 | margin-bottom: 2px;
87 | display: initial;
88 | animation: kofi-wiggle 3s infinite;
89 | }
90 | @keyframes kofi-wiggle {
91 | 0% {
92 | transform: rotate(0) scale(1);
93 | }
94 | 60% {
95 | transform: rotate(0) scale(1);
96 | }
97 | 75% {
98 | transform: rotate(0) scale(1.12);
99 | }
100 | 80% {
101 | transform: rotate(0) scale(1.1);
102 | }
103 | 84% {
104 | transform: rotate(-10deg) scale(1.1);
105 | }
106 | 88% {
107 | transform: rotate(10deg) scale(1.1);
108 | }
109 | 92% {
110 | transform: rotate(-10deg) scale(1.1);
111 | }
112 | 96% {
113 | transform: rotate(10deg) scale(1.1);
114 | }
115 | 100% {
116 | transform: rotate(0) scale(1);
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | /* @tailwind base; */
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: [
3 | './src/**/*.{js,jsx,ts,tsx}',
4 | ],
5 | theme: {
6 | extend: {},
7 | },
8 | plugins: [],
9 | }
--------------------------------------------------------------------------------