Articles
Articles from Chunk Creations — our expertise and insights on the latest trends in the industry put to words. All articles available also on Medium
Lessons Learned: NPM packages publishing and API authorization
While refactoring @creation-ui/react to version 15, I tried to publish a new version to npm locally and couldn’t due to NPM auth errors.
Here is how it went:
$ npm publish
npm WARN ignoring workspace config at /Users/username/GitHub/creation-ui-react/packages/ui/.npmrc
npm notice
npm notice 📦 @creation-ui/react@15.0.0
npm notice === Tarball Contents ===
npm notice 2.1kB CHANGELOG.md
npm notice 1.1kB LICENSE
npm notice 1.8kB README.md
npm notice 918B dist/index.css
npm notice 50.0kB dist/index.d.mts
npm notice 50.0kB dist/index.d.ts
npm notice 355.1kB dist/index.js
npm notice 1.0MB dist/index.js.map
npm notice 347.2kB dist/index.mjs
npm notice 1.0MB dist/index.mjs.map
npm notice 1.3kB dist/theme.css
npm notice 2.2kB package.json
npm notice === Tarball Details ===
npm notice name: @creation-ui/react
npm notice version: 15.0.1
npm notice filename: creation-ui-react-15.0.0.tgz
npm notice package size: 547.9 kB
npm notice unpacked size: 2.9 MB
npm notice shasum: 769ad59ba78310274a8a34c0933cad161f2ad483
npm notice integrity: sha512-rxDCX60g0xg5Z[…]dQiQwVoyqPIOg==
npm notice total files: 12
npm notice
npm notice Publishing to https://registry.npmjs.org with tag latest and public access
npm ERR! code E404
npm ERR! 404 Not Found - PUT https://registry.npmjs.org/@creation-ui%2freact - Not found
npm ERR! 404
npm ERR! 404 '@creation-ui/react@15.0.0' is not in this registry.
npm ERR! 404
npm ERR! 404 Note that you can also install from a
npm ERR! 404 tarball, folder, http url, or git url.
npm ERR! A complete log of this run can be found in: /Users/username/.npm/_logs/2025–03–27T14_55_04_665Z-debug-0.log
First things first
When I started working with the NPM API a few years back, I learned that 404 is a bit misleading, as in fact it is 401 Unauthorized.
The whole 401 as 404 thing in systems is really a design decision where any resource that hasn’t been found with the current privilege level is indeed a not-found resource, resulting in the 404 error code.
It is a common design pattern in the frontend: accessing /user-is-private-route while not logged in?
“Well, we didn’t find such a route, sorry mate ¯\\\_(ツ)\_/¯”
Everywhere else it’s a bit too close to “security through obscurity” for my liking. But I digress.
The important thing is the NPM said, You Shall Not Pass! or as cool kids say 401 Not Authorized. This happened despite that I had a token set up in .env that .npmrc file should catch on.
I knew it could be done with an extra CLI command, but I couldn’t be bothered to check it out.
# .npmrc
auto-install-peers=true
strict-peer-dependencies=false
@creation-ui:registry=https://registry.npmjs.org
//registry.npmjs.org/:_authToken=${NPM_TOKEN_AUTOMATION}
To Auth or Not to Auth
But that wasn’t the only way to get authorized. So I tried thenpm login. After successfully logging in on a browser, I still got the same error message.
Bugged out as I was, it suddenly occurred to me that I can actually check if I’m logged in by running the `npm whoami` command:
username@machine ui % npm whoami
npm WARN ignoring workspace config at /Users/username/GitHub/creation-ui-react/packages/ui/.npmrc
npm ERR! code E401
npm ERR! 401 Unauthorized - GET https://registry.npmjs.org/-/whoami
npm ERR! A complete log of this run can be found in: /Users/username/.npm/_logs/2025–03–27T14_55_31_687Z-debug-0.log
npm ERR! code E401
npm ERR! Unauthorized - please log in to your npm account to publish this package.
Crazy right?
At that point I knew that:
- ✅ I logged in successfully
- ❌ NPM API says I’m not logged in
- ❌ NPM_TOKEN_AUTOMATION is a undefined
It was clear to me that how NPM auth works is not clear at all. It seemed that NPM ignores which user is logged in and takes _authToken from the .npmrc file first.
After closer inspection I figured out that token at time had value set to ”${NPM_TOKEN_AUTOMATION}” because of how I set up my .npmrc file.
Removing this line resolved the issue for me:
//registry.npmjs.org/:_authToken=${NPM_TOKEN_AUTOMATION}
After that I was able to publish my package to NPM.
Conclusion
Where are the docs for that, @npm!? 👀
Originally published at https://www.pawelkrystkiewicz.pl on March 27, 2025.
PK
Paweł Krystkiewicz
March 27, 2025
Tailwind 4 Dark Mode Dynamic Theme
Learn how to implement a dynamic dark mode theme in Tailwind CSS 4 using the new theme in CSS config.
January 25, 2025
So the Tailwind 4 was released. The biggest breaking change was the introduction of new theme configuration method—you can only do it in CSS now. Here is an official example:
@import 'tailwindcss';
@theme {
--font-display: 'Satoshi', 'sans-serif';
--breakpoint-3xl: 1920px;
--color-avocado-100: oklch(0.99 0 0);
--color-avocado-200: oklch(0.98 0.04 113.22);
--color-avocado-300: oklch(0.94 0.11 115.03);
--color-avocado-400: oklch(0.92 0.19 114.08);
--color-avocado-500: oklch(0.84 0.18 117.33);
--color-avocado-600: oklch(0.53 0.12 118.34);
--ease-fluid: cubic-bezier(0.3, 0, 0, 1);
--ease-snappy: cubic-bezier(0.2, 0, 0, 1);
/* ... */
}
The problem with this approach is that there is no easy way to define the dark mode theme here, as complained on their GitHub.
Solution
This is how I solved this issue in my React components system, @creation-ui/react using @variant directive:
@import 'tailwindcss';
@variant dark (&:where(.dark, .dark *));
:root {
--text-primary: oklch(0 0 0);
--text-secondary: oklch(0.556 0 0);
--background-primary: oklch(0.99 0 0);
--background-secondary: oklch(0.985 0 0);
--border: oklch(0.87 0 0);
@variant dark {
--text-primary: oklch(0.97 0 0);
--text-secondary: oklch(0.87 0 0);
--background-primary: oklch(0.269 0 0);
--background-secondary: oklch(0.371 0 0);
--border: oklch(0.35 0 0);
}
}@theme {
--color-primary: oklch(60.48% 0.2165 257.21);
--color-warning: oklch(77.97% 0.1665 72.45);
--color-error: oklch(66.16% 0.2249 25.88);
--color-success: oklch(75.14% 0.1514 166.5);
--color-text-primary: var(--text-primary);
--color-text-secondary: var(--text-secondary);
--color-background-primary: var(--background-primary);
--color-background-secondary: var(--background-secondary);
--color-border: var(--border);
}
Basically in :root we define both our light and dark color variables, where @variant dark decides of the variable values.
Because Tailwind generates color CSS classes (text-[color]-value, bg-[color]-value, border-[color]-value, etc.) from --color- variables, we can use them in our React components like so:
const Button = ({ children, ...props }) => {
return (
)
}
And that’s it! Now you can use a dynamic light/dark theme in your app, and it will automatically switch to dark mode when you set dark variant (here: a .dark class on, e.g. body element).
Originally published at pawelkrystkiewicz.pl 25, 2025.
PK
Paweł Krystkiewicz
January 25, 2025
Building a Smart Truncation Detection Hook for React
Learn how to create a reusable React hook that detects text truncation in UI elements, enabling intelligent tooltips and responsive design adjustments
The Truncation Detection Problem
In modern UIs, we often truncate text with CSS when it exceeds container bounds:
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
This is cool and all, but sometimes we need to keep those truncated strings helpful and communicative for our users. To that end, it would be helpful to know whether our string has been truncated or not. This knowledge could open such opportunities as
This knowledge could open such opurtunites as:
Showing tooltips only when content is truncated
Dynamically adjusting layouts
Providing expand/collapse functionality
Can we actually detect that?
Yes, yes we can!
A very rudimentary attempt could be made by checking element dimensions:
const isTruncated = element.scrollWidth > element.clientWidth
While this works OK, it has several limitations:
Doesn’t respond to window resizing
Requires “manual” DOM access
Definitely lacks React lifecycle awareness
Doesn’t handle edge cases (like flex containers)
To make this work the best with React, we definitely could use a hook.
Solution
For this to work, we need a hook with:
Type safety with generics
ResizeObserver for responsiveness
Simple API
import { RefObject, useEffect, useRef, useState } from 'react'
interface UseDetectedTruncation {
ref: RefObject
isTruncated: boolean
}export const useDetectedTruncation = <
RefType extends HTMLElement,
>(): UseDetectedTruncation => {
const [isTruncated, setIsTruncated] = useState(false)
const elementRef = useRef(null)
const checkTruncation = () => {
const element = elementRef.current
if (!element) return
// Check both width and height for multi-line truncation
const isWidthTruncated = element.scrollWidth > element.clientWidth
const isHeightTruncated = element.scrollHeight > element.clientHeight
setIsTruncated(isWidthTruncated || isHeightTruncated)
}
useEffect(() => {
const element = elementRef.current
if (!element) return
// Initial check
checkTruncation()
// Set up observation
const resizeObserver = new ResizeObserver(checkTruncation)
resizeObserver.observe(element)
// MutationObserver for content changes
const mutationObserver = new MutationObserver(checkTruncation)
mutationObserver.observe(element, {
childList: true,
subtree: true,
characterData: true,
})
return () => {
resizeObserver.disconnect()
mutationObserver.disconnect()
}
}, [])
return { ref: elementRef, isTruncated }
}
Practical Usage
Here’s how to create a smart tooltip component using our hook:
import { Tooltip, type TooltipProps } from '@your-ui-library'
import { twMerge } from 'tailwind-merge'
interface SmartTooltipProps extends React.HTMLAttributes {
tooltipProps: Omit
content: string
}
export const SmartTooltip = ({
tooltipProps,
content,
children,
className,
...props
}: SmartTooltipProps) => {
const { isTruncated, ref } = useDetectedTruncation()
return (
{children || content}
)
}
Performance Considerations
Debounce Observations: For frequently resizing elements, consider debouncing the checks:
const debouncedCheck = useDebounce(checkTruncation, 100)
2. Selective Observation: Only observe necessary attributes:
resizeObserver.observe(element, { box: 'content-box' })
3. Cleanup: Properly disconnect observers in the cleanup function to prevent memory leaks.
Testing Strategies
Verify the hook works in different scenarios:
Static truncated text
Dynamically loaded content
Responsive layout changes
Multi-line truncation (line-clamp)
Nested scrolling containers
describe('useDetectedTruncation', () => {
it('detects horizontal truncation', () => {
const { result } = renderHook(() => useDetectedTruncation())
render(
Long text that should truncate
,
)
expect(result.current.isTruncated).toBe(true)
})
it('ignores non-truncated content', () => {
const { result } = renderHook(() => useDetectedTruncation())
render(
Short text
,
)
expect(result.current.isTruncated).toBe(false)
})
})
Going forward
To make it even sexier, we could consider adding the following features:
https://medium.com/media/d37d1c060816395a578aba437cc12a89/href
Conclusion
The useDetectedTruncation hook provides a clean, reusable solution for a common UI challenge. By encapsulating the detection logic, we can:
Create more accessible interfaces
Build smarter components
Reduce unnecessary tooltip clutter
Make our UIs more responsive to content changes.
Originally published at https://www.pawelkrystkiewicz.pl on November 20, 2024.PK
Paweł Krystkiewicz
November 20, 2024
Managing UI Decision Complexity—From Boolean Soup to Business Rules
In software development, we often need to check multiple conditions in one step, be it a frontend component or a backend function. In frontend development, it will mostly decide about UI state. Let’s say there are multiple conditions coming from different parts of the system: user permission levels, user subscription plans, entity states, etc.
Usually frontend code grows with the complexity of product’s business logic. This often starts as a simple conditional statement like so:
import React from 'react'
export const App = () => {
// authentication logic here
const authenticated = useIsAuth()
// decide which view to show
return authenticated ? :
}
This is very basic conditional logic for displaying private routes only to authenticated (logged in) users.
When things get complicated
The previous example was very rudimentary. Complexity can grow very quickly as business requirements grow and evolve in a Scrum environment. It’s highly likely more conditions will be added to the app’s business logic and be required to check for proper UI display. For this let’s use example of Meta’s Messenger app and the single message entity.
First, we define a simple Message entity with all its relevant properties and context:
type Message = {
id: string
senderId: string
recipientId: string
sentAt: Date
deliveredAt?: Date
seenAt?: Date
deleted: boolean
failed: boolean
attachmentsCount: number
isPinned: boolean
isEdited: boolean
isReply: boolean
isGroupMessage: boolean
currentUserId: string
}
Below is a table of conditions that control how the UI behaves for each message:
Visual mapping
If isFailedToSend → show red error icon and retry option.
If isDelivered but not isSeen → show double-check icon in gray.
If isSeen → show double-check icon in blue.
If canEdit → show edit button.
If showPinnedIcon → display pin in corner.
If showReplyPreview → show small reply preview UI.
Now let’s see how this could be implemented without any clever refactors—just growing the codebase overtime.
import React from 'react'
import {
Check,
DoubleCheck,
Pin,
WarningTriangle,
Attachment,
EditPencil,
Reply,
} from 'iconoir-react'
import { Message } from './types'
type MessageProps = {
message: Message
userIsGroupAdmin: boolean
}export const OldFlagsMessage: React.FC = ({
message,
userIsGroupAdmin,
}) => {
const isSent = !!message.sentAt && !message.failed
const isDelivered = !!message.deliveredAt && !message.seenAt
const isSeen = !!message.seenAt
const isFailedToSend = message.failed
const canEdit =
message.senderId === message.currentUserId &&
!message.deleted &&
!message.failed
return // render
From terrible mess to terribly clever
The code above is less than ideal, especially around readability. As a first step, one could certainly refactor all those checks into hooks. But let’s go deeper into this refactor, as business requirements will certainly grow, as the only constant in life (and especially in software development) is change. Death and taxes are just a derivative of that.
So we want to create a future-proof refactor that could possibly be a more robust solution that, if necessary, could be used in other places of the system. The latter is an especially good requirement because making this a bit more top-level and detached from the very business logic allows sharing responsibility for this code among many team members. Easing code maintenance and benefiting from sharing ideas around this solution.
There are few things those conditions have in common:
result is a boolean flag
they can be isolated—they are not interdependent.
they depend on a limited source of truth—deciding data passed to the component is a limited set.
All this tells us there is a solution to this problem that could be expressed as
function a => (payload: LimitedDataSet) => boolean
If we could define all boolean flags as such functions, we could include them in a data set that could run each function with the same payload and collect results for each flag. If this is not clever, then I hate React.
So, inspired by eslint rules and cva props evaluation, I created a simple engine that takes an array of rules (not nested) and runs each function with provided props. Each Rule is represented with its name, and the function that returns boolean. The function evaluates the incoming props and returns a boolean value: a Flag.
export type Rules<
A = any,
K extends string | number | symbol = string,
> = Record boolean>
export type Flags = Record<
K,
boolean
>
Modeling these conditions as a rules object
With the above pattern, we can create a set of functions—in this case in an object:
//basically component props in our case
export type MessageRulesArgs = {
message: Message
userIsGroupAdmin: boolean
}
// this is definiton of our required flags
export type MessageRuleSet = {
isSent: boolean
isDelivered: boolean
isSeen: boolean
canEdit: boolean
showPinnedIcon: boolean
showEditedIndicator: boolean
showReplyPreview: boolean
showAttachmentIcon: boolean
showFailedIcon: boolean
showDeleteButton: boolean
}
export type MessageRules = Rules
export const messageRules: MessageRules = {
isSent: ({ message: m }) => !!m.sentAt && !m.failed,
isDelivered: ({ message: m }) => !!m.deliveredAt && !m.seenAt,
isSeen: ({ message: m }) => !!m.seenAt,
canEdit: ({ message: m }) =>
m.senderId === m.currentUserId && !m.deleted && !m.failed,
showPinnedIcon: ({ message: m }) => m.isPinned,
showEditedIndicator: ({ message: m }) => m.isEdited,
showReplyPreview: ({ message: m }) => m.isReply,
showAttachmentIcon: ({ message: m }) => m.attachmentsCount > 0,
showFailedIcon: ({ message: m }) => m.failed,
showDeleteButton: ({ message: m }) =>
m.senderId === m.currentUserId && !m.deleted,
}
With rules defined, the remaining thing to do is run and evaluate the result with our data. For this I came up with this little helper:
import type { Flags, Rules } from './types'
export const applyRules = <
A = any,
K extends string | number | symbol = string,
>(
rule: Rules,
args: A,
): Flags => {
return Object.keys(rule).reduce((flags: Flags, key: string) => {
const ruleFn = rule[key as K]
if (ruleFn) {
flags[key] = ruleFn(args)
}
return flags
}, {}) as Flags
}
Then, to use it inside a component, we can create a hook:
import { useDeepCompareMemo } from 'use-deep-compare'
import { applyRules } from './apply-rules'
import { Rules } from '../types'
export const useRules = <
RuleSet extends Rules,
Args extends Record,
>(
rules: RuleSet,
args: Args,
): Record => {
// use a deep compare memo to memoize the result regardless of depth
return useDeepCompareMemo(
() => applyRules(rules, args),
[rules, args],
)
}
Final usage in React component:
import {
Attachment,
Check,
DoubleCheck,
EditPencil,
Pin,
Reply,
WarningTriangle,
} from 'iconoir-react'
import React from 'react'
import { messageRules } from './rules/rules'
import { useRules } from './rules/rules-hook'
import { Message, MessageRules, MessageRulesArgs } from './types'
type MessageProps = {
message: Message
userIsGroupAdmin: boolean
}
export const WithRulesMessage: React.FC = ({
message,
userIsGroupAdmin,
}) => {
const {
isSent,
isDelivered,
isSeen,
canEdit,
showPinnedIcon,
showEditedIndicator,
showReplyPreview,
showAttachmentIcon,
showFailedIcon,
showDeleteButton,
} = useRules(messageRules, {
message,
userIsGroupAdmin,
})
return //render
}
Benefits of this approach
Clarity: Each condition is named and easy to understand.
Reusability: Components can import and use conditions without duplication.
Testability: Each rule can be unit-tested independently.
Extensibility: Adding new rules is simple and low-risk.
Conclusion
If you find yourself with growing UI complexity and a web of conditions, consider abstracting those checks into a rules object or lightweight engine. It brings structure, clarity, and scalability to what would otherwise become a nightmare of conditionals and spaghetti code.
Originally published at https://www.pawelkrystkiewicz.pl on October 3, 2024.
PK
Paweł Krystkiewicz
October 3, 2024
Framer Motion: Animating Height Transitions in React
How to create smooth height animations for collapsible content using Framer Motion and dynamic duration calculations based on content size.
The Height Animation Problem
Animating height transitions is notoriously tricky in CSS. Unlike opacity or transforms, you can’t simply transition from height: 0 to height: auto. The browser needs concrete values to interpolate between:
/* This doesn't work */
.collapsible {
height: 0;
transition: height 0.3s;
}.collapsible.open {
height: auto; /* Can't animate to 'auto' */
}
Common workarounds involve JavaScript to calculate heights, max-height hacks that create awkward timing, or fixed heights that break with dynamic content. None of these are ideal.
For expandable sections, accordions, dropdowns, or any collapsible UI, we need smooth height transitions that work with dynamic content of any size.
Solution
Framer Motion combined with the react-use library's useMeasure hook gives us a clean solution. We measure the content's actual height and animate to that specific value, with smart duration scaling based on content size.
import type { FC, ReactNode } from 'react'
import { useMeasure } from 'react-use'
import { motion } from 'framer-motion'
interface AnimateHeightProps {
isVisible: boolean
ease?: string
duration?: number
className?: string
variants?: {
open: object
collapsed: object
}
children: ReactNode
}export const AnimateHeight: FC = ({
duration,
ease,
variants,
isVisible,
children,
...other
}) => {
const [ref, { height }] = useMeasure()
return (
)
}/**
* Get the duration of the animation depending upon
* the height provided.
* @param {number} height of container
*/
const getAutoHeightDuration = (height: number) => {
if (!height) return 0
const constant = height / 36
return Math.round((4 + 15 * constant ** 0.25 + constant / 5) * 10)
}AnimateHeight.defaultProps = {
ease: 'easeInOut',
variants: {
open: {
opacity: 1,
height: 'auto',
},
collapsed: { opacity: 0, height: 0 },
},
}
How It Works
useMeasure Hook: Tracks the actual rendered height of the content in real-time. When content changes, the height updates automatically.
Motion Variants: Define two states:
open - Full height with opacity 1
collapsed - Zero height with opacity 0
Dynamic Duration: The getAutoHeightDuration function calculates animation timing based on content height. Taller content gets longer animations, preventing jarring fast transitions for large sections.
overflow-hidden: Critical for the effect - hides content as the container shrinks to zero height.
inherit=false: Prevents inheriting animation variants from parent motion components, keeping this animation independent.
The Duration Formula
The getAutoHeightDuration function deserves attention:
const getAutoHeightDuration = (height: number) => {
if (!height) return 0
const constant = height / 36
return Math.round((4 + 15 * constant ** 0.25 + constant / 5) * 10)
}
This formula creates a non-linear relationship between height and duration:
Small heights (50px): ~150ms
Medium heights (200px): ~250ms
Large heights (500px): ~400ms
Extra large (1000px): ~550ms
Breaking Down the Math
Let’s dissect that return statement: Math.round((4 + 15 * constant ** 0.25 + constant / 5) * 10)
Step 1: Normalize the height
const constant = height / 36
This converts pixels to a normalized scale. For example, 360px becomes 10, making the math more manageable. This is totally arbitrary; pick what works best for your use case.
Step 2: Three components create the curve
The formula has three parts that add together:
Base duration: 4
Ensures even tiny elements get at least 40ms (after × 10)
2. Diminishing growth: 15 * constant ** 0.25
The ** 0.25 is a fourth root creates sublinear growth
This is the key: doubling height doesn’t double duration
Prevents massive elements from having sluggish animations
3. Linear component: constant / 5
Adds some proportional scaling
Balances out the diminishing returns from component 2
Step 3: Scale to milliseconds
Math.round((...) * 10)
Multiply by 10 to convert to milliseconds and round for clean values.
Why This Curve?
The fourth root (** 0.25) is the secret sauce. Compare linear vs fourth-root scaling:
https://medium.com/media/370d5ec67cac76057bbfef0fc0a9dee3/href
Without the fourth root, large collapsible sections would take almost a second to animate, feeling slow and unresponsive. The formula keeps animations snappy regardless of content size while still giving taller content enough time to feel smooth. For an extra crispy effect, you could clamp this value to keep it within the desired range.
Math.max(Math.min(calculated, 40, 350))
At the end of day, you can override this by passing a fixed duration prop when you need consistent timing across all heights.
Practical Usage
Simple accordion:
const Accordion = ({ title, content }) => {
const [isOpen, setIsOpen] = useState(false)
return (
)
}
Custom animation variants:
Fixed duration for consistent timing:
{children}
Why This Approach Works
Automatic measurement: No manual height calculations needed. The component adapts to content changes automatically.
Smooth animations: Unlike max-height hacks, the animation duration matches the actual height transition.
Flexible: Override defaults when needed while keeping sensible behavior out of the box.
Dynamic content friendly: If content height changes while open, the animation adjusts seamlessly.
Performance Considerations
While this approach is generally performant, be aware:
Height animations trigger layout recalculation: Unlike transforms, animating height is not GPU-accelerated. For numerous simultaneous animations, this can impact performance.
Measurement overhead: useMeasure uses ResizeObserver, which is efficient but adds overhead. For hundreds of collapsible items, consider virtualization.
Alternative for performance-critical cases: If you need many height animations, consider animating scaleY transform instead:
// More performant but content gets squished
The tradeoff is that scaleY squishes content during animation, while height animations maintain readable content throughout.
Comparison with AnimatePresence
This component differs from the AnimateAppearance pattern:
https://medium.com/media/4984edf8d13dab938e97f362dc07fb73/href
Use AnimateHeight when content should remain in the DOM (for SEO, form state, etc.) and you want vertical expand/collapse. Use AnimatePresence when components are truly conditional and should be unmounted.
Common Use Cases
FAQ accordions:
{
faqs.map(faq => (
{faq.answer}
))
}
Filter panels:
Form sections:
Conclusion
The AnimateHeight component solves one of CSS's most annoying limitations by combining Framer Motion's animation capabilities with real-time height measurement. By automating the measurement and providing smart duration scaling, we get:
Smooth height transitions without CSS hacks
Automatic adaptation to dynamic content
Customizable timing when needed
Clean, reusable animation logic
Next time you need a collapsible section, skip the CSS gymnastics and reach for this component. Your users will appreciate the smooth, professional transitions.
Originally published at https://www.pawelkrystkiewicz.pl on February 10, 2024.
{children}
{content}
PK
Paweł Krystkiewicz
February 10, 2024
The Magic of Bookmarklets
A deep dive into bookmarklets—how they work, why they work, and how to create your own JavaScript-powered browser shortcuts.
How Bookmarklets Work: Injecting Code into Any Webpage
In the previous article, I showed you this mysterious one-liner:
javascript:(function(){var s=document.createElement('style');s.innerHTML='*{background:#000!important;color:#0f0!important;outline:solid #f00 1px!important}';document.head.appendChild(s);})();
Save it as a bookmark, click it, and suddenly your page has debug outlines everywhere. But how does this actually work? And why does JavaScript code live in a bookmark?
Let’s break it down.
What is a Bookmarklet?
A bookmarklet is executable JavaScript code stored as a browser bookmark. Instead of navigating to a URL, clicking the bookmark executes code on the current page.
The secret is the javascript: protocol prefix. According to MDN, these are "fake navigation targets that execute JavaScript when the browser attempts to navigate."
// Normal bookmark
https://example.com
// Bookmarklet
javascript:alert('Hello from a bookmark!')
That’s it. Any valid JavaScript after javascript: will run in the context of the current page.
How the Browser Processes javascript: URLs
When you execute a javascript: URL, the browser:
Parses and executes the script
Evaluates its completion value (similar to eval())
If the completion value is a string, treats it as HTML and navigates to it
If it’s not a string, just execute the code without navigation.
This is why simple expressions can replace your page content, while IIFEs (which return undefined) don't.
Breaking Down the Debug Bookmarklet
Let’s dissect the CSS debug bookmarklet piece by piece:
1. The Protocol Prefix
javascript:
Tells the browser, “Execute this as JavaScript; don’t navigate anywhere.”
2. The IIFE Wrapper
(function(){
// code here
})();
An Immediately Invoked Function Expression (IIFE). This pattern:
Keeps variables scoped (doesn’t pollute the global namespace)
Executes immediately
Returns nothing (important—we don’t want to navigate)
Without the IIFE, any return value would replace the page content. Try this:
javascript:'Hello'
Your page gets replaced with the text “Hello.” The IIFE prevents this.
3. Creating a Style Element
var s = document.createElement('style');
Creates a new