├── .npmrc ├── src ├── lib │ ├── utils │ │ ├── generateUid.d.ts │ │ ├── cx.d.ts │ │ ├── cx.ts │ │ ├── calculateTooltipPosition.d.ts │ │ ├── generateUid.ts │ │ └── calculateTooltipPosition.ts │ ├── index.ts │ └── components │ │ └── Tooltip.svelte ├── types │ ├── index.ts │ └── index.d.ts ├── app.d.ts ├── app.html └── routes │ └── +page.svelte ├── .prettierignore ├── vite.config.ts ├── .prettierrc ├── svelte.config.js ├── .gitignore ├── tsconfig.json ├── LICENSE ├── eslint.config.js ├── static └── favicon.svg ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /src/lib/utils/generateUid.d.ts: -------------------------------------------------------------------------------- 1 | export declare const generateUid: () => string; 2 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type Position = 'top' | 'right' | 'left' | 'bottom'; 2 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export type Position = 'top' | 'right' | 'left' | 'bottom'; 2 | -------------------------------------------------------------------------------- /src/lib/utils/cx.d.ts: -------------------------------------------------------------------------------- 1 | export declare const cx: (classes: Record) => string[]; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | bun.lock 6 | bun.lockb 7 | 8 | # Miscellaneous 9 | /static/ 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /src/lib/utils/cx.ts: -------------------------------------------------------------------------------- 1 | export const cx = (classes: Record) => { 2 | const classNames = []; 3 | for (const className in classes) { 4 | if (classes[className]) classNames.push(className); 5 | } 6 | return classNames; 7 | }; 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "overrides": [ 8 | { 9 | "files": "*.svelte", 10 | "options": { 11 | "parser": "svelte" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/utils/calculateTooltipPosition.d.ts: -------------------------------------------------------------------------------- 1 | import { type Writable } from 'svelte/store'; 2 | import type { Position } from '../types/index.js'; 3 | export declare const calculateTooltipPosition: (element: HTMLElement, positionWritable: Writable, offset: number, id?: string) => { 4 | x: number; 5 | y: number; 6 | }; 7 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | preprocess: vitePreprocess(), 7 | 8 | kit: { 9 | adapter: adapter() 10 | } 11 | }; 12 | 13 | export default config; 14 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | /dist 11 | 12 | # OS 13 | .DS_Store 14 | Thumbs.db 15 | 16 | # Env 17 | .env 18 | .env.* 19 | !.env.example 20 | !.env.test 21 | 22 | # Vite 23 | vite.config.js.timestamp-* 24 | vite.config.ts.timestamp-* 25 | .vscode 26 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "module": "NodeNext", 13 | "moduleResolution": "NodeNext" 14 | }, 15 | "include": ["src"] 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/utils/generateUid.ts: -------------------------------------------------------------------------------- 1 | export const generateUid = () => { 2 | const getRandomValues = 3 | typeof crypto !== 'undefined' && crypto.getRandomValues 4 | ? crypto.getRandomValues.bind(crypto) 5 | : null; 6 | 7 | if (getRandomValues) { 8 | const bytes = new Uint8Array(16); 9 | getRandomValues(bytes); 10 | bytes[6] = (bytes[6] & 0x0f) | 0x40; 11 | bytes[8] = (bytes[8] & 0x3f) | 0x80; 12 | 13 | const hex = [...bytes].map((b) => b.toString(16).padStart(2, '0')); 14 | return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10).join('')}`; 15 | } 16 | 17 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { 18 | const r = (Math.random() * 16) | 0; 19 | const v = c === 'x' ? r : (r & 0x3) | 0x8; 20 | return v.toString(16); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |

Tipster Demo

17 |

18 | Visit 19 | 24 | svelte.dev/docs/kit 25 | 26 | to read the documentation 27 |

28 | 29 |

30 | Or try a quick inline tooltip: 31 | 36 | hover me 37 | 38 |

39 |
40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Mostafa Kheibary 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 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-config-prettier'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { includeIgnoreFile } from '@eslint/compat'; 4 | import js from '@eslint/js'; 5 | import svelte from 'eslint-plugin-svelte'; 6 | import { defineConfig } from 'eslint/config'; 7 | import globals from 'globals'; 8 | import ts from 'typescript-eslint'; 9 | import svelteConfig from './svelte.config.js'; 10 | 11 | const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); 12 | 13 | export default defineConfig( 14 | includeIgnoreFile(gitignorePath), 15 | js.configs.recommended, 16 | ...ts.configs.recommended, 17 | ...svelte.configs.recommended, 18 | prettier, 19 | ...svelte.configs.prettier, 20 | { 21 | languageOptions: { 22 | globals: { ...globals.browser, ...globals.node } 23 | }, 24 | rules: { 25 | // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects. 26 | // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors 27 | 'no-undef': 'off' 28 | } 29 | }, 30 | { 31 | files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], 32 | languageOptions: { 33 | parserOptions: { 34 | projectService: true, 35 | extraFileExtensions: ['.svelte'], 36 | parser: ts.parser, 37 | svelteConfig 38 | } 39 | } 40 | } 41 | ); 42 | -------------------------------------------------------------------------------- /static/favicon.svg: -------------------------------------------------------------------------------- 1 | svelte-logo -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tipsterjs", 3 | "version": "0.0.6", 4 | "scripts": { 5 | "dev": "vite dev", 6 | "build": "vite build && npm run prepack", 7 | "preview": "vite preview", 8 | "prepare": "svelte-kit sync || echo ''", 9 | "prepack": "svelte-kit sync && svelte-package && publint", 10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 12 | "format": "prettier --write .", 13 | "lint": "prettier --check . && eslint ." 14 | }, 15 | "repository": { 16 | "url": "https://github.com/mostafa-kheibary/Tipster", 17 | "type": "github" 18 | }, 19 | "files": [ 20 | "dist", 21 | "!dist/**/*.test.*", 22 | "!dist/**/*.spec.*" 23 | ], 24 | "sideEffects": [ 25 | "**/*.css" 26 | ], 27 | "svelte": "./dist/index.js", 28 | "types": "./dist/index.d.ts", 29 | "type": "module", 30 | "exports": { 31 | ".": { 32 | "types": "./dist/index.d.ts", 33 | "svelte": "./dist/index.js" 34 | } 35 | }, 36 | "peerDependencies": { 37 | "svelte": "^5.0.0" 38 | }, 39 | "devDependencies": { 40 | "@eslint/compat": "^1.4.0", 41 | "@eslint/js": "^9.36.0", 42 | "@sveltejs/adapter-auto": "^6.1.0", 43 | "@sveltejs/kit": "^2.43.2", 44 | "@sveltejs/package": "^2.5.4", 45 | "@sveltejs/vite-plugin-svelte": "^6.2.0", 46 | "@types/node": "^22", 47 | "eslint": "^9.36.0", 48 | "eslint-config-prettier": "^10.1.8", 49 | "eslint-plugin-svelte": "^3.12.4", 50 | "globals": "^16.4.0", 51 | "prettier": "^3.6.2", 52 | "prettier-plugin-svelte": "^3.4.0", 53 | "publint": "^0.3.13", 54 | "svelte": "^5.39.5", 55 | "svelte-check": "^4.3.2", 56 | "typescript": "^5.9.2", 57 | "typescript-eslint": "^8.44.1", 58 | "vite": "^7.1.7" 59 | }, 60 | "keywords": [ 61 | "svelte" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /src/lib/utils/calculateTooltipPosition.ts: -------------------------------------------------------------------------------- 1 | import { get, type Writable } from 'svelte/store'; 2 | import type { Position } from '../types/index.js'; 3 | 4 | export const calculateTooltipPosition = ( 5 | element: HTMLElement, 6 | positionWritable: Writable, 7 | offset: number, 8 | id?: string 9 | ) => { 10 | let position = get(positionWritable); 11 | const tooltip = document.getElementById(`tipster-${id}`); 12 | if (!tooltip) return { x: 0, y: 0 }; 13 | const { width: tw, height: th } = tooltip.getBoundingClientRect(); 14 | const { top, left, width: ew, height: eh } = element.getBoundingClientRect(); 15 | 16 | let x = 0; 17 | let y = 0; 18 | 19 | const overflowsRight = left + ew + tw > window.innerWidth; 20 | const overflowsLeft = left - tw < 0; 21 | const overflowsTop = top - th < 0; 22 | const overflowsBottom = top + eh + th > window.innerHeight; 23 | 24 | if (['top', 'bottom'].includes(position) && overflowsTop && overflowsBottom) 25 | position = overflowsRight ? 'left' : 'right'; 26 | else if (['left', 'right'].includes(position) && overflowsLeft && overflowsRight) 27 | position = 'bottom'; 28 | else { 29 | if (overflowsTop && position === 'top') position = 'bottom'; 30 | else if (overflowsBottom && position == 'bottom') position = 'top'; 31 | else if (overflowsRight && position === 'right') position = 'left'; 32 | else if (overflowsLeft && position === 'left') position = 'right'; 33 | } 34 | 35 | positionWritable.set(position); 36 | switch (position) { 37 | case 'top': 38 | x = left + ew / 2 - tw / 2; 39 | y = top - th - offset; 40 | break; 41 | 42 | case 'bottom': 43 | x = left + ew / 2 - tw / 2; 44 | y = top + eh + offset; 45 | break; 46 | 47 | case 'left': 48 | x = left - tw - offset; 49 | y = top + eh / 2 - th / 2; 50 | break; 51 | 52 | case 'right': 53 | x = left + ew + offset; 54 | y = top + eh / 2 - th / 2; 55 | break; 56 | } 57 | 58 | return { x, y }; 59 | }; 60 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | import Tooltip from './components/Tooltip.svelte'; 2 | import { writable } from 'svelte/store'; 3 | import { mount, unmount } from 'svelte'; 4 | import { generateUid } from './utils/generateUid.js'; 5 | import { calculateTooltipPosition } from './utils/calculateTooltipPosition.js'; 6 | import type { Position } from '../types/index.js'; 7 | 8 | interface Props { 9 | position?: Position; 10 | delay?: number; 11 | backgroundColor?: string; 12 | color?: string; 13 | offset?: number; 14 | borderWidth?: string; 15 | borderColor?: string; 16 | fontSize?: string; 17 | } 18 | 19 | export const tooltip = (element: HTMLElement, props?: Props) => { 20 | const title = element.getAttribute('title'); 21 | if (!title) return; 22 | const id = generateUid(); 23 | const isTooltipShowing = writable(false); 24 | const position = writable(props?.position || 'bottom'); 25 | const targetPosition = writable<{ x: number; y: number }>( 26 | calculateTooltipPosition(element, position, props?.offset || 12, id) 27 | ); 28 | 29 | const component = mount(Tooltip, { 30 | target: document.getElementById('tipster') as HTMLElement, 31 | props: { 32 | id, 33 | color: props?.color, 34 | backgroundColor: props?.backgroundColor, 35 | borderColor: props?.borderColor, 36 | borderWidth: props?.borderWidth, 37 | fontSize: props?.fontSize, 38 | text: title, 39 | isTooltipShowing, 40 | position, 41 | targetPosition 42 | } 43 | }); 44 | 45 | element.style.position = 'relative'; 46 | element.removeAttribute('title'); 47 | let timeout: ReturnType | null = null; 48 | window.addEventListener('mousemove', (e) => { 49 | const target = e.target as HTMLElement; 50 | if (timeout) clearTimeout(timeout); 51 | 52 | if (element.contains(target)) { 53 | timeout = setTimeout(() => { 54 | targetPosition.set(calculateTooltipPosition(element, position, props?.offset || 12, id)); 55 | isTooltipShowing.set(true); 56 | }, props?.delay || 100); 57 | } else { 58 | isTooltipShowing.set(false); 59 | } 60 | }); 61 | 62 | const onWindowResize = () => { 63 | targetPosition.set(calculateTooltipPosition(element, position, props?.offset || 12, id)); 64 | }; 65 | window.addEventListener('resize', onWindowResize); 66 | return { 67 | destroy() { 68 | unmount(component); 69 | window.removeEventListener('resize', onWindowResize); 70 | } 71 | }; 72 | }; 73 | 74 | export const createTipster = (defaults: Partial) => { 75 | return (element: HTMLElement, props?: Props) => { 76 | return tooltip(element, { ...defaults, ...props }); 77 | }; 78 | }; 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🧠 Tipster 2 | 3 | Beautiful, flexible, and easy-to-use **tooltips for Svelte**. 4 | Create reusable tooltip styles, customize colors, borders, and positions — all with a tiny footprint ✨ 5 | 6 | --- 7 | 8 | ### 🚀 Features 9 | 10 | - 🎨 Customizable background, color, border, font, and offset 11 | - 🪄 `createTipster()` to define reusable tooltip presets 12 | - ⚡ Simple `use:tooltip` directive for quick usage 13 | - 📍 Smart positioning (`top`, `bottom`, `left`, `right`) 14 | - 🧩 Built with Svelte’s reactivity and clean architecture 15 | 16 | --- 17 | 18 | ### 📦 Installation 19 | 20 | ```bash 21 | npm install tipsterjs 22 | # or 23 | yarn add tipsterjs 24 | # or 25 | pnpm add tipsterjs 26 | ``` 27 | 28 | --- 29 | 30 | ### 💡 Quick Example 31 | 32 | ```svelte 33 | 46 | 47 |

Tipster Demo

48 |

49 | Visit 50 | 55 | svelte.dev/docs/kit 56 | 57 | to read the documentation 58 |

59 | 60 |

61 | Or try a quick inline tooltip: 62 | 66 | hover me 67 | 68 |

69 | ``` 70 | 71 | --- 72 | 73 | ### ⚙️ API 74 | 75 | #### `use:tooltip` 76 | 77 | Attach a tooltip directly to any element. 78 | 79 | **Props:** 80 | 81 | | Prop | Type | Default | Description | 82 | | ----------------- | ---------------------------------------- | ------------- | ---------------------------- | 83 | | `backgroundColor` | `string` | `#333` | Tooltip background color | 84 | | `color` | `string` | `#fff` | Text color | 85 | | `borderColor` | `string` | `transparent` | Border color | 86 | | `borderWidth` | `string` | `0` | Border thickness | 87 | | `fontSize` | `string` | `12px` | Text size | 88 | | `position` | `'top' \| 'bottom' \| 'left' \| 'right'` | `bottom` | Tooltip position | 89 | | `offset` | `number` | `12` | Distance from target element | 90 | | `delay` | `number` | `100` | Delay before showing tooltip | 91 | 92 | --- 93 | 94 | #### `createTipster(defaults)` 95 | 96 | Create a reusable tooltip directive with shared defaults. 97 | 98 | ```js 99 | const myTooltip = createTipster({ 100 | backgroundColor: '#222', 101 | color: '#fff', 102 | position: 'top' 103 | }); 104 | ``` 105 | 106 | Then use it anywhere: 107 | 108 | ```svelte 109 | Hover me 110 | ``` 111 | 112 | --- 113 | 114 | ### 🧠 How It Works 115 | 116 | Tipster: 117 | 118 | 1. Reads the element’s `title` attribute 119 | 2. Creates a Svelte tooltip component dynamically 120 | 3. Tracks mouse movement & viewport resizing 121 | 4. Cleans up everything on unmount 122 | 123 | Lightweight, no dependencies, and works seamlessly in any Svelte project 💪 124 | 125 | ### 🤝 Contributing 126 | 127 | Got an idea? Found a bug? 128 | PRs and issues are always welcome 💙 129 | 130 | ```bash 131 | git clone https://github.com/mostafa-kheibary/Tipster 132 | cd Tipster 133 | pnpm install 134 | pnpm dev 135 | ``` 136 | 137 | --- 138 | 139 | ### 📜 License 140 | 141 | **MIT** — do whatever you want, just don’t remove the credit 🙃 142 | -------------------------------------------------------------------------------- /src/lib/components/Tooltip.svelte: -------------------------------------------------------------------------------- 1 | 41 | 42 |
54 | 69 | 70 |
{text}
71 |
72 | 73 | 162 | --------------------------------------------------------------------------------