Skip to content

Commit 89161d6

Browse files
feat(first-commit): init
0 parents  commit 89161d6

File tree

13 files changed

+6396
-0
lines changed

13 files changed

+6396
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# node_modules
2+
node_modules
3+
4+
# npmrc
5+
.npmrc

.husky/pre-commit

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
npx lint-staged

.npmignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
demo
2+
.husky
3+
.vscode

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Active Theory
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.

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Split Text
2+
3+
GSAP-like text splitting class.
4+
5+
It supports:
6+
7+
- splitting text into lines, words, and chars
8+
- CJK locales
9+
- nested HTML elements (with all the types of splits 😉)
10+
- text balancing
11+
- emoji
12+
13+
## Installation
14+
15+
```bash
16+
npm install @activetheory/split-text
17+
```
18+
19+
## Usage
20+
21+
```js
22+
import SplitText from '@activetheory/split-text';
23+
24+
const el = document.querySelector('.el');
25+
26+
const splitTextInstance = new SplitText(el);
27+
```
28+
29+
## Options
30+
31+
- `el`: The element to split.
32+
- `type`: The type of split to perform. Can be `lines`, `words`, or `chars`. Defaults to `lines`.
33+
- `minLines`: The minimum number of lines to split. Defaults to `1`.
34+
- `lineThreshold`: The threshold for splitting lines. Defaults to `0.2`.
35+
- `noAriaLabel`: Whether to not add .sr-only content. Defaults to `false`.
36+
- `noBalance`: Whether to not balance the text using @activetheory/balance-text. Defaults to `false`.
37+
- `balanceRatio`: The ratio of the width of the element to the width of the parent. Defaults to `1`.
38+
- `handleCJK`: Whether to handle CJK characters. Defaults to `false`.
39+
40+
## Properties
41+
42+
- `isSplit`: Whether the text has been split.
43+
- `chars`: The characters of the text.
44+
- `words`: The words of the text.
45+
- `lines`: The lines of the text.
46+
- `originals`: The original elements of the text.
47+
48+
## Methods
49+
50+
- `split()`: Split the text.
51+
- `revert()`: Revert the text to the original.
52+
53+
## Demo
54+
55+
See the [demo](./demo) folder for examples.
56+
To run the demo, run `npm run dev`.
57+
58+
## Notes
59+
60+
### CJK locales
61+
62+
The `handleCJK` option will leverage `​` to split the text properly.
63+
We suggest to have a look at https://github.com/google/budoux/ for more information about how to place `​` in your text.

demo/index.html

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Document</title>
7+
</head>
8+
9+
<body>
10+
<main>
11+
<h1>Split Text</h1>
12+
<div>
13+
<div class="col">
14+
<textarea id="input-text" rows="10" placeholder="Enter text to split"></textarea>
15+
<fieldset>
16+
<legend>Options</legend>
17+
<div>
18+
<input type="checkbox" id="input-type-lines" checked />
19+
<label for="input-type-lines">Lines</label>
20+
</div>
21+
<div>
22+
<input type="checkbox" id="input-type-words" />
23+
<label for="input-type-words">Words</label>
24+
</div>
25+
<div>
26+
<input type="checkbox" id="input-type-chars" />
27+
<label for="input-type-chars">Chars</label>
28+
</div>
29+
<div>
30+
<input type="checkbox" id="input-no-aria-label" />
31+
<label for="input-no-aria-label">No aria-label</label>
32+
</div>
33+
<div>
34+
<input type="checkbox" id="input-no-balance" />
35+
<label for="input-no-balance">No balance<br /><small>turn of the balance-text library</small></label>
36+
</div>
37+
<div>
38+
<input type="checkbox" id="input-handle-cjk" />
39+
<label for="input-handle-cjk">Handle CJK</label>
40+
</div>
41+
</fieldset>
42+
<fieldset>
43+
<legend>Styles</legend>
44+
<div>
45+
<input type="range" id="input-font-size" min="10" max="100" value="16" />
46+
<label for="input-font-size">Font size</label>
47+
</div>
48+
<div>
49+
<input type="checkbox" id="input-balance" value="balance" checked />
50+
<label for="input-balance">text-wrap: balance<br /><small>preferred way on modern browsers</small></label>
51+
</div>
52+
</fieldset>
53+
</div>
54+
<div class="col">
55+
<div class="output-container">
56+
<div id="output-text"></div>
57+
</div>
58+
<button id="output-revert">Revert</button>
59+
<div id="original-text"></div>
60+
</div>
61+
</div>
62+
</main>
63+
<script src="./index.js" type="module"></script>
64+
</body>
65+
</html>

demo/index.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import './styles.css';
2+
import SplitText from '../src/index.js';
3+
4+
document.addEventListener('DOMContentLoaded', () => {
5+
// Get DOM elements
6+
const inputText = document.getElementById('input-text');
7+
const outputText = document.getElementById('output-text');
8+
const originalText = document.getElementById('original-text');
9+
const revertButton = document.getElementById('output-revert');
10+
11+
// Get option checkboxes
12+
const typeLines = document.getElementById('input-type-lines');
13+
const typeWords = document.getElementById('input-type-words');
14+
const typeChars = document.getElementById('input-type-chars');
15+
const noAriaLabel = document.getElementById('input-no-aria-label');
16+
const noBalance = document.getElementById('input-no-balance');
17+
const handleCJK = document.getElementById('input-handle-cjk');
18+
const cssBalance = document.getElementById('input-balance');
19+
// Get font size input
20+
const fontSize = document.getElementById('input-font-size');
21+
22+
let splitInstance = null;
23+
24+
// Helper to get current options
25+
const getOptions = () => {
26+
const types = [];
27+
if (typeLines.checked) types.push('lines');
28+
if (typeWords.checked) types.push('words');
29+
if (typeChars.checked) types.push('chars');
30+
31+
return {
32+
type: types.join(','),
33+
noAriaLabel: noAriaLabel.checked,
34+
noBalance: noBalance.checked,
35+
handleCJK: handleCJK.checked,
36+
};
37+
};
38+
39+
// Update split text
40+
const updateSplit = () => {
41+
outputText.style.removeProperty('max-width');
42+
43+
if (cssBalance.checked) {
44+
outputText.style.setProperty('text-wrap', 'balance');
45+
} else {
46+
outputText.style.removeProperty('text-wrap');
47+
}
48+
49+
const text = inputText.value.trim();
50+
if (!text) return;
51+
52+
// Store original text
53+
originalText.innerHTML = text;
54+
55+
// Update output
56+
outputText.innerHTML = text;
57+
58+
// Create new split instance
59+
splitInstance = new SplitText(outputText, getOptions());
60+
};
61+
62+
const updateFontSize = () => {
63+
outputText.style.fontSize = `${fontSize.value}px`;
64+
updateSplit();
65+
};
66+
67+
// Event listeners
68+
inputText.addEventListener('input', updateSplit);
69+
fontSize.addEventListener('input', updateFontSize);
70+
[typeLines, typeWords, typeChars, noAriaLabel, noBalance, handleCJK, cssBalance].forEach((checkbox) => {
71+
checkbox.addEventListener('change', updateSplit);
72+
});
73+
74+
// Revert button
75+
revertButton.addEventListener('click', () => {
76+
outputText.style.removeProperty('max-width');
77+
78+
if (splitInstance) {
79+
splitInstance.revert();
80+
splitInstance = null;
81+
}
82+
});
83+
84+
window.addEventListener('resize', () => {
85+
updateSplit();
86+
});
87+
88+
// Initial text
89+
inputText.value = [/*html*/ `<h1>split anything 🐳 🍔 🍕 into words, chars, lines</h1>`, /*html*/ `<p>Try typing some text here to see it split into lines, words, and characters!</p>`].join('\n');
90+
91+
updateSplit();
92+
});

demo/styles.css

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
body {
2+
font-family:
3+
system-ui,
4+
-apple-system,
5+
BlinkMacSystemFont,
6+
'Segoe UI',
7+
Roboto,
8+
Oxygen,
9+
Ubuntu,
10+
Cantarell,
11+
'Open Sans',
12+
'Helvetica Neue',
13+
sans-serif;
14+
font-kerning: none;
15+
-webkit-text-rendering: optimizeSpeed;
16+
text-rendering: optimizeSpeed;
17+
}
18+
19+
main {
20+
display: flex;
21+
justify-content: center;
22+
align-items: center;
23+
flex-direction: column;
24+
padding: 2rem;
25+
max-width: 1200px;
26+
margin: 0 auto;
27+
}
28+
29+
h1 {
30+
margin-bottom: 2rem;
31+
align-self: start;
32+
}
33+
34+
main > div {
35+
display: flex;
36+
gap: 2rem;
37+
width: 100%;
38+
}
39+
40+
.col {
41+
flex: 1;
42+
display: flex;
43+
flex-direction: column;
44+
gap: 1rem;
45+
}
46+
47+
textarea {
48+
display: block;
49+
width: auto;
50+
max-width: 100%;
51+
padding: 1rem;
52+
font-size: 1rem;
53+
border: 1px solid #ccc;
54+
border-radius: 4px;
55+
resize: vertical;
56+
min-height: 200px;
57+
}
58+
59+
fieldset {
60+
border: 1px solid #ccc;
61+
border-radius: 4px;
62+
padding: 1rem;
63+
}
64+
65+
fieldset > div {
66+
margin: 0.5rem 0;
67+
display: flex;
68+
gap: 0.5rem;
69+
}
70+
71+
.output-container {
72+
min-height: 200px;
73+
padding: 1rem;
74+
border: 1px solid #ccc;
75+
border-radius: 4px;
76+
margin-bottom: 1rem;
77+
}
78+
79+
button {
80+
padding: 0.5rem 1rem;
81+
font-size: 1rem;
82+
background: #007bff;
83+
color: white;
84+
border: none;
85+
border-radius: 4px;
86+
cursor: pointer;
87+
}
88+
89+
button:hover {
90+
background: #0056b3;
91+
}
92+
93+
#original-text {
94+
margin-top: 1rem;
95+
padding: 1rem;
96+
border: 1px solid #ccc;
97+
border-radius: 4px;
98+
color: #666;
99+
}
100+
101+
/* Split text styles */
102+
.line {
103+
background-color: #f0f0f0;
104+
}
105+
106+
.word {
107+
position: relative;
108+
display: inline-block;
109+
}
110+
111+
.char {
112+
position: relative;
113+
display: inline-block;
114+
}
115+
116+
.sr-only {
117+
position: absolute;
118+
width: 1px;
119+
height: 1px;
120+
padding: 0;
121+
margin: -1px;
122+
overflow: hidden;
123+
clip: rect(0, 0, 0, 0);
124+
white-space: nowrap;
125+
border: 0;
126+
}

eslint.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import globals from 'globals';
2+
import pluginJs from '@eslint/js';
3+
4+
/** @type {import('eslint').Linter.Config[]} */
5+
export default [{ languageOptions: { globals: globals.browser } }, pluginJs.configs.recommended];

0 commit comments

Comments
 (0)