Skip to content

Instantly share code, notes, and snippets.

@johnd0e
Last active April 8, 2026 19:32
Show Gist options
  • Select an option

  • Save johnd0e/f03d90c320b5300ad47de90cbbcaea26 to your computer and use it in GitHub Desktop.

Select an option

Save johnd0e/f03d90c320b5300ad47de90cbbcaea26 to your computer and use it in GitHub Desktop.
This is LinkedIn userscript automatically expands tiny textarea fields to fit their full content,
so you can see and edit your text without constant scrolling.

LinkedIn Textarea Auto-Resize

LinkedIn's profile editing interface forces you to write long texts (up to 2000 characters length!) — such as job, education, projects descriptions — inside tiny textarea fields that are only 2–4 lines tall. The text scrolls inside the box and you can never see more than a few lines at once, making editing uncomfortable and error-prone.

This userscript automatically expands those fields to fit their full content, so you can see and edit your text without constant scrolling.


What is a userscript?

A userscript is a small JavaScript program that runs in your browser on specific websites, modifying the page to suit your needs. They are managed by browser extensions and are a well-established way to customize web experiences.

To use userscripts you need one of these popular extensions:

Extension Chrome / Edge Firefox
Tampermonkey
Violentmonkey
Greasemonkey

Installation

  1. Install one of the extensions above.
  2. Open the raw script file: linkedin-textarea-autoresize.user.js
  3. The extension will automatically detect it and show an install dialog.
  4. Click Install.

The script will now activate automatically when you open any LinkedIn profile edit form.


What this script does

When you open a profile editing form on LinkedIn (https://linkedin.com/in/your-name/.../edit/forms/...), the script:

  1. Finds the edit dialog on the page.
  2. Detects all <textarea> fields inside it.
  3. Expands each one to show its full content by adjusting its height based on scrollHeight.
  4. Keeps adjusting the height as you type.
  5. Watches for new textareas that appear while the dialog is open.

The script activates only on LinkedIn edit form URLs and only inside the active edit dialog. It does nothing on any other page.


Is this safe to use?

You can verify the script's behavior by reading its source — it is short and straightforward.

Key facts:

  • Runs locally in your browser only. No servers, no backend, no network requests.
  • Does not read or store your data. It only adjusts the visual size of input fields.
  • Does not send anything anywhere. There are no fetch, XMLHttpRequest, or any other network calls in the code.
  • Does not modify your LinkedIn profile. It only changes how the edit form looks in your browser session.
  • Scoped to a specific URL pattern. The @match directive limits execution to https://www.linkedin.com/*, and internally the script only activates on /edit/forms/ paths.

Reading the source code is the best way to confirm all of the above.

DEVNOTES

Technical notes on implementation decisions, failed approaches, and reasoning behind the final solution.


Problem statement

LinkedIn edit forms render inside a <dialog> component. Multiline text fields (<textarea>) inside these forms are fixed at a small height (typically 2–4 visible lines) with internal scroll, and do not grow with their content.

The goal: make those textareas expand vertically to show the full text, and keep them expanded as the user types.


Key findings during investigation

1. The real blocker is the flex layout, not just CSS resize

Removing resize: none from the textarea in DevTools restores horizontal resizing only. Vertical resizing remains blocked because the textarea lives inside a deeply nested chain of display: flex containers.

The flex cross-axis layout absorbs vertical space in a way that prevents the browser's built-in resize handle from working in the vertical direction.

2. The correct minimal fix is local to the textarea itself

The key insight came from testing height: 500px !important directly on the textarea: it worked immediately without any changes to parent containers.

This means the flex ancestors are compatible with explicit height values set via inline styles with !important. The issue is specifically about the browser's native resize UI (drag handle), not about whether the element can have a different height.

Fix applied to each textarea:

textarea.style.setProperty('flex', '0 0 auto', 'important');
textarea.style.setProperty('min-height', '0', 'important');
textarea.style.setProperty('max-height', 'none', 'important');
textarea.style.setProperty('overflow-y', 'hidden', 'important');
textarea.style.setProperty('height', '0px', 'important');
textarea.style.setProperty('height', `${textarea.scrollHeight}px`, 'important');

3. The dialog is the right scope

Searching document globally for textareas would be fragile and noisy. LinkedIn's DOM contains many elements outside the active dialog. The active edit form is always rendered inside dialog[data-testid="dialog"][open], which provides a clean, stable scope for observation.


Dead ends (do not revisit without new information)

Changing flex properties on parent containers

Tried modifying align-items, overflow, height, flex on various ancestors of the textarea, walking up the DOM tree.

Results: no desirable effect, some changes broke the page layout significantly.

Pure CSS height: fit-content

height: fit-content does not work for <textarea> elements in this context.

<textarea> is a replaced form control. Its intrinsic height as seen by the CSS engine does not reflect the amount of text inside it. scrollHeight is the only reliable way to get the actual content height, and it requires JavaScript to read and apply.


SPA navigation handling

LinkedIn does not do full page reloads when navigating between sections. The script handles this by wrapping history.pushState and history.replaceState, and listening to popstate, to detect URL changes and re-evaluate whether the current page matches the target pattern.


Selector for the dialog

dialog[data-testid="dialog"][open]

This targets the first open dialog with LinkedIn's known data-testid attribute. If LinkedIn renders multiple dialogs simultaneously, this will process only the first one. In practice, the edit form is a single dialog, so this is sufficient.

If future LinkedIn updates change data-testid, this selector will need updating.

// ==UserScript==
// @name LinkedIn dialog textarea auto-resize
// @namespace https://gist.github.com/johnd0e/
// @version 0.1.1
// @description Auto-resize textarea inside LinkedIn edit dialogs so long text is comfortable to edit
// @author johnd0e
// @match https://www.linkedin.com/*
// @run-at document-idle
// @homepageURL https://gist.github.com/johnd0e/f03d90c320b5300ad47de90cbbcaea26
// @downloadURL https://gist.githubusercontent.com/johnd0e/f03d90c320b5300ad47de90cbbcaea26/raw/linkedin-textarea-autoresize.user.js
// @updateURL https://gist.githubusercontent.com/johnd0e/f03d90c320b5300ad47de90cbbcaea26/raw/linkedin-textarea-autoresize.user.js
// ==/UserScript==
(function () {
'use strict';
const processed = new WeakSet();
let dialogObserver = null;
let currentDialog = null;
function isTargetPage() {
return location.href.includes('/edit/forms/');
}
function resize(textarea) {
if (!(textarea instanceof HTMLTextAreaElement)) return;
textarea.style.setProperty('flex', '0 0 auto', 'important');
textarea.style.setProperty('min-height', '0', 'important');
textarea.style.setProperty('max-height', 'none', 'important');
textarea.style.setProperty('overflow-y', 'hidden', 'important');
textarea.style.setProperty('height', '0px', 'important');
textarea.style.setProperty('height', `${textarea.scrollHeight}px`, 'important');
}
function setup(textarea) {
if (!(textarea instanceof HTMLTextAreaElement)) return;
const handler = () => resize(textarea);
setTimeout(handler);
if (processed.has(textarea)) return;
processed.add(textarea);
textarea.addEventListener('input', handler);
}
function scanDialog(dialog) {
dialog.querySelectorAll('textarea').forEach(setup);
}
function observeDialog(dialog) {
if (currentDialog === dialog) return;
dialogObserver?.disconnect();
currentDialog = dialog;
if (!dialog) return;
scanDialog(dialog);
dialogObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof HTMLTextAreaElement) {
setup(node);
} else if (node instanceof Element) {
node.querySelectorAll?.('textarea').forEach(setup);
}
}
}
});
dialogObserver.observe(dialog, {
childList: true,
subtree: true,
});
}
function update() {
if (!isTargetPage()) {
observeDialog(null);
return;
}
const dialog = document.querySelector('dialog[data-testid="dialog"][open]');
observeDialog(dialog);
}
let scheduled = false;
function scheduleUpdate() {
if (scheduled) return;
scheduled = true;
queueMicrotask(() => {
scheduled = false;
update();
});
}
const originalPushState = history.pushState;
history.pushState = function () {
const result = originalPushState.apply(this, arguments);
scheduleUpdate();
return result;
};
const originalReplaceState = history.replaceState;
history.replaceState = function () {
const result = originalReplaceState.apply(this, arguments);
scheduleUpdate();
return result;
};
window.addEventListener('popstate', scheduleUpdate);
const pageObserver = new MutationObserver(() => {
if (isTargetPage() && !currentDialog) {
scheduleUpdate();
}
});
pageObserver.observe(document.documentElement, {
childList: true,
subtree: true,
});
update();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment