# TheoSoti — Full Content
Theosoti is a frontend blog sharing CSS tutorials, practical guides, and bite-sized tips on modern web development and UI design.
Author: Theo Soti — Frontend Developer & Content Creator.
Site: https://theosoti.com
================================================================
BLOG POSTS
================================================================
# CSS if(): Conditional Styling Without JavaScript
URL: https://theosoti.com/blog/css-if-function-conditional-styling-without-javascript/
Published: 2026-06-23
Tags: CSS, modern CSS, JavaScript alternatives, frontend development, frontend tutorial
> Use the CSS if() function to choose property values from style, media, and support queries without writing JavaScript state glue.
## CSS can now choose a value
Most CSS conditions work at the rule level.
If the viewport is narrow, run this block:
```css
@media (width < 700px) {
.card {
padding: 1rem;
}
}
```
If the browser supports a feature, run this block:
```css
@supports (color: oklch(60% 0.2 30)) {
.card {
color: oklch(60% 0.2 30);
}
}
```
That model works well when a whole set of declarations needs to change. It feels heavier when only one value is conditional.
The new CSS `if()` function works inside a property value:
```css
.card {
padding: if(
media(width < 700px): 1rem;
else: 1.5rem;
);
}
```
That is the mental model. `if()` does not choose a selector. It does not create a new block in the cascade. It chooses a value for the property you are already writing.
That makes it useful for small branches: one color, one padding value, one part of a border shorthand, one token inside a component.
It is also early. MDN marks `if()` as experimental and not Baseline. Can I Use shows support in Chrome and Edge from version 137, but not Safari or Firefox on June 23, 2026. Treat it as progressive enhancement unless your browser target is Chromium-only.
## The mental model
An `if()` value is a list of branches.
Each branch has a condition, a colon, and the value to use when that condition is true:
```css
.alert {
background: if(
style(--tone: danger): oklch(96% 0.04 28);
else: white;
);
}
```
The browser reads the branches from top to bottom. The first true condition wins. If nothing matches, the `else` branch gives you a default.
Write the `else` branch. Without it, a supported browser can end up with an invalid or initial value when no condition matches. That is rarely what you meant.
There is also a tiny syntax detail that matters: there must be no space between `if` and `(`.
```css
/* valid */
color: if(media(print): black; else: white);
/* invalid */
color: if (media(print): black; else: white);
```
That is annoying, but easy to catch once you know it.
## Start with style queries
The most useful form for components is probably `style()`.
It lets one declaration read a custom property on the same element and choose a value from it.
```css
.button {
--tone: neutral;
background: if(
style(--tone: danger): oklch(60% 0.22 28);
style(--tone: success): oklch(62% 0.18 150);
else: oklch(92% 0 0);
);
color: if(
style(--tone: neutral): black;
else: white;
);
}
```
Then your component can set one custom property:
```css
.button[data-tone='danger'] {
--tone: danger;
}
```
The interesting part is that you do not need to repeat the full button rule for every tone. You can keep the decision next to the value that changes.
That is where `if()` feels different from a class-based pattern. Classes still decide what state the component is in. `if()` decides what each value should become for that state.
The demo uses native radio inputs, `:has()`, custom properties, and `if()`. There is no JavaScript fallback.
## Use media queries when one value changes
`if()` can also run a media query inside a value.
```css
.layout {
gap: if(
media(width < 700px): 0.75rem;
else: 1.5rem;
);
}
```
This is useful when a single value changes and the rule would otherwise exist only for that one line.
I would not replace every `@media` block with this. If the layout changes several declarations, a normal media query is clearer:
```css
@media (width < 700px) {
.layout {
grid-template-columns: 1fr;
gap: 0.75rem;
padding: 1rem;
}
}
```
That code is easier to scan because the whole layout mode is in one place.
Use `if(media(...))` when the branch is small. Use `@media` when the branch is the layout.
## Use supports queries for value fallbacks
Feature queries work too.
```css
.badge {
color: if(
supports(color: oklch(70% 0.2 30)): oklch(70% 0.2 30);
else: hotpink;
);
}
```
That can be handy when a modern value has a fallback and you want to keep the fallback inside the same declaration.
For unsupported browsers, you still need the normal cascade fallback because old browsers do not understand `if()` at all:
```css
.badge {
color: hotpink;
color: if(
supports(color: oklch(70% 0.2 30)): oklch(70% 0.2 30);
else: hotpink;
);
}
```
The first declaration is for browsers without `if()`. The second declaration is for browsers with `if()`.
That pattern is boring, but it is the right kind of boring.
## You can use it inside shorthands
`if()` can return a whole property value:
```css
.callout {
border: if(
style(--tone: danger): 2px solid oklch(60% 0.22 28);
else: 2px solid var(--border);
);
}
```
It can also return only part of a value:
```css
.callout {
border: 2px solid
if(
style(--tone: danger): oklch(60% 0.22 28);
else: var(--border);
);
}
```
That second version is often nicer. The border width and style stay fixed. Only the color branches.
The same idea works inside other values:
```css
.panel {
width: calc(
if(style(--wide: true): 70%; else: 50%) - 2rem
);
}
```
Do not get carried away. Nested conditional values can become harder to read than repeated CSS. If you need to stop and mentally parse three branches inside a `calc()`, a separate rule might be better.
## Browser support
This is the part that should stop you from using `if()` casually.
On June 23, 2026, [Can I Use reports 65.31% global support for CSS `if()`](https://caniuse.com/css-if).
MDN also marks `if()` as limited availability and experimental. That does not make it useless, but it does mean you should treat it as progressive enhancement.
So the safe pattern is:
```css
.card {
padding: 1rem;
}
@supports (padding: if(media(width > 0px): 1rem; else: 2rem)) {
.card {
padding: if(
style(--density: compact): 0.75rem;
else: 1.25rem;
);
}
}
```
The fallback comes first. The `if()` version lives inside `@supports`.
For a tutorial, that is also a good teaching pattern. The browser either shows the live version or it shows a plain message explaining why the demo is unavailable. No fake polyfill. No JavaScript fallback hiding the real support story.
## Final thoughts
CSS `if()` is not ready to be a default tool for every production site. Safari and Firefox support are still missing, and the syntax is new enough that a lot of developers will need to look twice.
But the feature itself is useful.
It fills a small gap in CSS: sometimes you do not want a conditional block. You want one conditional value.
That is what `if()` gives you. Use it behind `@supports`, keep the fallback boring, and reserve it for the places where it makes the CSS easier to read.
## Enjoyed this article?
I write about modern CSS, HTML, and simpler ways to build for the web.
Join my newsletter below if you want more tutorials like this.
## References
- [MDN: `if()` CSS function](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/if)
- [CSS Values and Units Module Level 5: `if()` notation](https://drafts.csswg.org/css-values-5/#if-notation)
- [Chrome 137 release notes](https://developer.chrome.com/blog/new-in-chrome-137/)
- [Can I Use: CSS `if()` function](https://caniuse.com/css-if)
---
# CSS Anchor Positioning: Tooltips and Dropdowns Without JavaScript Math
URL: https://theosoti.com/blog/css-anchor-positioning-without-javascript/
Published: 2026-06-16
Tags: CSS, modern CSS, JavaScript alternatives, frontend development, CSS layout, accessibility
> A modern CSS guide to Anchor Positioning for popovers, tooltips, and dropdowns without getBoundingClientRect, scroll listeners, or layout JavaScript.
## Why this matters
Positioning a dropdown should not require a small geometry engine.
You have a button. You have a menu. The menu should open next to the button, keep that relationship when the page scrolls, and avoid falling outside the viewport when there is not enough space. For years, that usually meant JavaScript:
```js
const button = document.querySelector('.menu-button');
const menu = document.querySelector('.menu');
const rect = button.getBoundingClientRect();
menu.style.top = `${rect.bottom + 8}px`;
menu.style.left = `${rect.left}px`;
```
That first version is simple, then real UI gets involved. The page scrolls. The viewport resizes. The button moves because a parent layout changed. The menu needs to flip above the button near the bottom of the screen. None of that is hard in isolation, but it is a lot of boring code for "put this thing next to that thing".
CSS Anchor Positioning gives CSS the relationship it was missing. One element can become an anchor, and another element can position itself from that anchor. The browser already knows both boxes, so it can do the geometry without a `getBoundingClientRect()` loop.
If you want to learn modern CSS through actual UI problems, this is a good feature to study. It moves a common floating UI pattern back into CSS, where the browser already understands layout, scrolling, and edges.
## The mental model
Anchor positioning has two parts: you name the anchor, then you position another element from it.
```css
.menu-button {
anchor-name: --menu-button;
}
.menu {
position: absolute;
position-anchor: --menu-button;
position-area: bottom;
}
```
The button still lives in the normal layout. The menu is positioned, but it now has a reference point that is not just the viewport or its containing block. It can say "place me around that button".
`position-area` is the friendliest syntax when you want a common placement like `top`, `bottom`, `left`, `right`, or `center`. It reads close to the design decision you would make in a component spec:
```css
.menu {
position-area: bottom;
}
```
That is useful, but it is not the whole feature. Anchor positioning is not only `position-area`.
## You can position with anchor()
The `anchor()` function lets you use the edges of the anchor inside inset properties like `top`, `right`, `bottom`, `left`, and their logical equivalents. This gives you more control than `position-area`.
For example, this places the top edge of the menu 12px below the bottom edge of the button:
```css
.menu-button {
anchor-name: --menu-button;
}
.menu {
position: absolute;
position-anchor: --menu-button;
inset: auto;
top: calc(anchor(bottom) + 12px);
left: anchor(left);
}
```
You can also align the right edge of a dropdown with the right edge of the button:
```css
.menu {
position: absolute;
position-anchor: --menu-button;
inset: auto;
top: calc(anchor(bottom) + 12px);
right: anchor(right);
}
```
Or you can place something above the trigger:
```css
.tooltip {
position: absolute;
position-anchor: --help-button;
inset: auto;
bottom: calc(anchor(top) + 20px);
left: anchor(center);
translate: -50% 0;
}
```
That last example is the point worth remembering. You are not locked into predefined areas. `anchor()` returns a length, so you can put it inside `calc()` and add spacing, offsets, or alignment tweaks.
MDN's `anchor()` reference shows the same idea with examples like `top: calc(anchor(bottom) + 10px)` and `left: calc(anchor(right) + 10px)`. The important caveat is that `anchor()` works inside inset properties. You use it for positioning edges, not as a general value you can drop anywhere in CSS.
## `position-area` vs `anchor()`
I would start with `position-area` when the design is simple. A popover under a button, a callout above an icon, or a small note to the side does not need custom math.
```css
.popover {
position: fixed;
position-anchor: --trigger;
position-area: bottom;
margin: 0.5rem;
}
```
Reach for `anchor()` when you need a specific edge relationship:
```css
.popover {
position: fixed;
position-anchor: --trigger;
inset: auto;
top: calc(anchor(bottom) + 8px);
right: anchor(right);
}
```
Those two approaches can live in the same mental model:
- `position-area` chooses a general zone around the anchor.
- `anchor()` lets you wire individual inset properties to anchor edges.
That distinction makes the feature much easier to use. You do not have to force everything through one syntax.
## Use it with popovers
Anchor positioning is especially nice with the Popover API. Popover handles the opening behavior and the top layer. Anchor positioning handles placement.
```html
Account
```
```css
.profile-button {
anchor-name: --profile-button;
}
.profile-menu {
position: fixed;
position-anchor: --profile-button;
inset: auto;
top: calc(anchor(bottom) + 8px);
left: anchor(left);
}
```
The HTML opens the popover. The CSS places it. That is a clean split.
When you position popovers this way, reset the browser's default popover positioning first. Popovers come with default `inset` and margin styles, and those can fight your anchor rules.
```css
.profile-menu {
margin: 0;
inset: auto;
}
```
If you want more detail on the opening part, I wrote about [opening dialogs and popovers with `commandfor`](/blog/html-command-commandfor-without-javascript/). This article is about the placement part. Once both pieces work, [`@starting-style` can animate the popover as it enters the top layer](/blog/starting-style-css/) without changing either one.
## Let the browser try another side
The hard part of dropdown positioning is not the happy path. It is what happens near the edge of the viewport.
This is where `position-try-fallbacks` helps. You can tell the browser to try a preferred placement first, then flip if that placement does not fit.
```css
.profile-menu {
position: fixed;
position-anchor: --profile-button;
position-area: bottom;
position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
}
```
That version says: place the menu below the button, but try another side if there is not enough room. This is not as powerful as a full tooltip library with custom collision rules, but it covers a lot of common UI.
You can also use fallbacks with a more manual `anchor()` setup. Start with a usable default, then enhance where anchor positioning is supported:
```css
.actions-menu {
inset: 50% auto auto 50%;
transform: translate(-50%, -50%);
}
@supports (anchor-name: --actions-menu-trigger) {
.actions-menu {
position: fixed;
position-anchor: --actions-menu-trigger;
inset: auto;
transform: none;
top: calc(anchor(bottom) + 8px);
left: anchor(left);
position-try-fallbacks: flip-block;
}
}
```
The fallback is not beautiful, but it is usable. In older browsers, the popover appears in the middle of the viewport. In browsers with anchor positioning, it attaches to the trigger.
## A copyable dropdown pattern
For a small dropdown, I would start from this:
```html
```
```css
.menu-trigger {
anchor-name: --actions-menu-trigger;
}
.actions-menu {
margin: 0;
inset: 50% auto auto 50%;
transform: translate(-50%, -50%);
}
@supports (anchor-name: --actions-menu-trigger) {
.actions-menu {
position: fixed;
position-anchor: --actions-menu-trigger;
inset: auto;
transform: none;
top: calc(anchor(bottom) + 0.5rem);
right: anchor(right);
position-try-fallbacks: flip-block, flip-inline;
}
}
```
This keeps the fallback simple and makes the enhanced version precise. The menu opens below the button, lines up with the button's right edge, and can flip when space gets tight.
The simpler `position-area` version is still fine when exact edge alignment does not matter:
```css
@supports (anchor-name: --actions-menu-trigger) {
.actions-menu {
position: fixed;
position-anchor: --actions-menu-trigger;
position-area: bottom;
margin: 0.5rem;
position-try-fallbacks: flip-block, flip-inline;
}
}
```
I like showing both versions because they solve slightly different problems. `position-area` is quick. `anchor()` is exact.
## Browser support and caveats
Support is much better than it was in early anchor positioning demos. MDN now marks `anchor()` as Baseline 2026, and current support is broad enough to consider it for progressive enhancement. You should still use `@supports`, because not every user is on the latest browser and not every part of the spec lands at the same time.
There are a few details worth testing before shipping:
- `anchor()` is only valid in inset properties like `top`, `left`, `inset-block-start`, and `inset-inline-end`.
- The anchor side has to match the axis. For example, `top: anchor(bottom)` makes sense, but `top: anchor(left)` does not.
- Popovers need their default `inset` and margin reset if you want your anchor placement to win.
- `position-try-fallbacks` handles common flipping, but it is not a complete replacement for every custom collision system.
For current syntax and compatibility, use [MDN's anchor positioning guide](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Anchor_positioning), the [`anchor()` reference](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/anchor), [Can I Use](https://caniuse.com/css-anchor-positioning), and the [CSS Anchor Positioning specification](https://drafts.csswg.org/css-anchor-position-1/).
## When not to use it
Anchor positioning places things. It does not design the whole interaction.
You still need JavaScript if the menu content depends on app state, if opening the menu fetches data, if you are building a complex keyboard model, or if you need custom collision rules that the browser cannot express yet. You also still need to think about accessibility. A tooltip that only appears on hover is still a weak way to expose important content, even if the placement is now pure CSS.
Use anchor positioning for the layout work. Use HTML for the native behavior where it fits. Keep JavaScript for the parts that are actually application logic.
## The short version
CSS Anchor Positioning gives CSS a real relationship between a trigger and a floating element.
Use `position-area` when you want a quick placement around the anchor:
```css
.popover {
position-anchor: --button;
position-area: bottom;
}
```
Use `anchor()` when you need exact edges:
```css
.popover {
position-anchor: --button;
top: calc(anchor(bottom) + 8px);
right: anchor(right);
}
```
Add `position-try-fallbacks` when the element might run out of space:
```css
.popover {
position-try-fallbacks: flip-block, flip-inline;
}
```
That combination replaces a surprising amount of small layout JavaScript. Not all of it, and not every tooltip library, but definitely the boring part where you measure a button just to put a menu next to it.
---
# Open Dialogs and Popovers Without JavaScript Using commandfor
URL: https://theosoti.com/blog/html-command-commandfor-without-javascript/
Published: 2026-06-08
Tags: HTML, modern HTML, JavaScript alternatives, accessibility, frontend development, frontend tutorial
> Use HTML command and commandfor to control dialogs and popovers declaratively, with less JavaScript and native browser behavior.
## The tiny JavaScript we keep writing
A lot of frontend JavaScript is not really application logic.
Sometimes it is just glue.
You have a button.
You have a dialog.
When the button is clicked, the dialog should open.
So you write this:
```js
const button = document.querySelector('.open-dialog');
const dialog = document.querySelector('#confirm-dialog');
button.addEventListener('click', () => {
dialog.showModal();
});
```
There is nothing wrong with this code. It is clear, small, and easy to understand.
But it also feels strange.
The browser already knows what a button is.
The browser already knows what a dialog is.
The browser already knows how to open a modal dialog.
The only missing piece is the connection between the two.
That is the gap `command` and `commandfor` fill.
They let you describe the relationship directly in HTML:
```html
Delete project
Delete this project?
This action cannot be undone.
Cancel
```
No click listener.
No `querySelector`.
No tiny state wrapper just to call a native method.
The button says what it controls, and what action it wants to perform.
## The mental model: a button can invoke a native action
`commandfor` is the connection.
It points to the `id` of the element you want to control:
```html
```
`command` is the action:
```html
```
Put them together and the button becomes an invoker. It can ask a supported element to do something the browser already understands.
For dialogs, that can be:
```html
Open dialog
Close dialog
```
For popovers, that can be:
```html
Toggle menu
```
This is not a replacement for all JavaScript.
It is a replacement for the boring JavaScript that only exists to connect a button to a native browser behavior.
That distinction matters. If your modal needs to fetch data, update app state, track events, or coordinate a complex workflow, JavaScript still belongs there. But if your code only says "open this dialog" or "toggle this popover", HTML can now carry that intent by itself.
## Open a dialog without JavaScript
Native `` already gives you a lot:
- modal behavior with `showModal()`
- focus handling
- Escape key behavior
- a `::backdrop`
- form-friendly close actions
Before `commandfor`, opening the dialog still usually needed a small script:
```js
openButton.addEventListener('click', () => {
dialog.showModal();
});
```
Now the open button can call the native action by itself:
```html
Open newsletter settings
Newsletter settings
Choose how often you want to hear from us.
Close
```
The important part is this:
```html
command="show-modal" commandfor="newsletter-dialog"
```
`show-modal` is the declarative equivalent of calling `dialog.showModal()`.
`close` is the declarative equivalent of calling `dialog.close()`.
There is also `request-close`, which is closer to asking the dialog to close. It fires the cancel flow first, so code can prevent the close if needed. For simple demos, `close` is easier to understand. For serious confirmation flows, `request-close` can be a better fit.
## Control a popover without JavaScript
Popover already had a declarative trigger:
```html
Toggle menu
```
That still works.
The difference is that `command` and `commandfor` are more general. They are not only for popovers.
Here is the same menu with the new pattern:
```html
Account
```
You can use:
- `show-popover`
- `hide-popover`
- `toggle-popover`
So the button can be explicit about what it does. A trigger button can toggle. A close button inside the popover can hide. A separate help button could only show.
The nice part is that the browser still handles the native popover behavior. Auto popovers can light-dismiss. They can close with Escape. They live in the top layer instead of fighting the rest of your stacking context.
If you want a deeper intro to the Popover API, I already wrote about [native HTML popovers](/short/html-popover-api/). This article is about the newer control layer on top.
`commandfor` only handles the action. If the popover also needs to stay attached to its trigger, [CSS Anchor Positioning handles the placement](/blog/css-anchor-positioning-without-javascript/). To animate a dialog or popover as it enters the top layer, you can add an [`@starting-style` entry transition](/blog/starting-style-css/) without changing the HTML that opens it.
You do not need to rewrite every popover just because `commandfor` exists.
The older `popovertarget` syntax is still short and readable:
```html
Menu
```
For a simple popover trigger, that is perfectly fine.
`commandfor` becomes more interesting when you want one mental model for different native actions:
```html
Open dialog
Toggle menu
```
If all you need is a popover toggle, use the syntax your team finds clearer. If you are building a design system and want one declarative pattern for dialogs and popovers, `commandfor` is easier to standardize.
## Browser support
This is the part you should not skip.
`command` and `commandfor` are Baseline 2025 features.
Support starts in Chrome and Edge 135, Firefox 144, and Safari/iOS Safari 26.2. Older browsers do not support them.
That makes it reasonable for progressive enhancement, but you still need to think about the importance of the interaction.
If a popover is just extra help text, unsupported browsers can miss the enhancement. If a modal is the only way to complete a critical checkout step, you need a fallback.
The simplest fallback is still a tiny script:
```js
if (!('command' in HTMLButtonElement.prototype)) {
document.querySelectorAll('[command][commandfor]').forEach((button) => {
const target = document.getElementById(button.getAttribute('commandfor'));
const command = button.getAttribute('command');
button.addEventListener('click', () => {
if (command === 'show-modal') target?.showModal?.();
if (command === 'close') target?.close?.();
if (command === 'toggle-popover') target?.togglePopover?.();
if (command === 'show-popover') target?.showPopover?.();
if (command === 'hide-popover') target?.hidePopover?.();
});
});
}
```
You may not need that fallback everywhere. But it is useful to understand the tradeoff: `commandfor` removes the JavaScript for modern browsers, and you can still add a small compatibility layer when the interaction is critical.
## Accessibility notes
The biggest win here is not just fewer lines of code.
It is fewer chances to rebuild native behavior incorrectly.
With ``, the browser already understands modality. With popovers, the browser already understands top-layer placement and dismissal behavior. With buttons, the browser already understands keyboard activation.
Still, you can make this worse if you ignore the basics:
- use real `` elements for commands
- add `type="button"` when the button is not submitting a form
- label dialogs with `aria-labelledby`
- keep visible close actions inside dialogs and popovers
- test keyboard navigation
- test mobile and zoomed layouts
- do not hide focus styles
Also remember that a popover is not a modal dialog. If the user must make a decision before returning to the page, use ``. If the content is lightweight and dismissible, a popover is usually a better fit.
## Useful references
The official docs are worth keeping nearby:
- [MDN: `HTMLButtonElement.command`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLButtonElement/command)
- [MDN: `` command and commandfor attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button)
- [Chrome Developers: Introducing command and commandfor](https://developer.chrome.com/blog/command-and-commandfor)
- [web.dev: Popover and dialog](https://web.dev/learn/css/popover-and-dialog)
- [Can I Use: `button commandfor`](https://caniuse.com/mdn-html_elements_button_commandfor)
- [Web Platform Features Explorer: Invoker commands](https://web-platform-dx.github.io/web-features-explorer/features/invoker-commands/)
## Enjoyed this article?
I write about modern CSS, HTML, and simpler ways to build for the web.
Join my newsletter below if you want more tutorials like this.
---
# How to Use @starting-style in CSS for Entry Transitions
URL: https://theosoti.com/blog/starting-style-css/
Published: 2026-05-10
Tags: CSS, modern CSS, CSS animations, JavaScript alternatives, frontend development, frontend tutorial
> Use @starting-style for modern CSS entry transitions on dialogs, popovers, drawers, and display:none elements without JavaScript timing hacks.
## CSS transitions had a missing piece
If you ever tried to fade in a dialog, a popover, or any element that goes from `display: none` to visible, you probably hit the same wall: the transition just would not run.
That is exactly the problem `@starting-style` solves.
The short version is simple:
- CSS transitions normally need a previous rendered state.
- Elements coming from `display: none` do not really have one.
- `@starting-style` gives the browser that missing starting point.
It only applies to **transitions**, not `@keyframes` animations. If you are already using keyframes, `@starting-style` is not the tool you need.
## Why transitions fail in the first place
CSS transitions interpolate between two rendered states.
That sounds obvious, but it explains the limitation.
When an element is hidden with `display: none`, there is no box to transition from. And when an element appears for the first time, the browser does not have a previous visible state to animate away from.
So code like this looks reasonable, but it won't give you a proper entry transition:
```css
.toast {
display: none;
opacity: 0;
transform: translateY(12px);
transition:
opacity 0.25s ease,
transform 0.25s ease,
display 0.25s allow-discrete;
}
.toast.is-open {
display: block;
opacity: 1;
transform: translateY(0);
}
```
You have the closed state.
You have the open state.
But you still don't have a real **starting** state for the moment the element first becomes visible.
That is the gap `@starting-style` fills.
## The real mental model: three states
This is the part that makes `@starting-style` click.
In practice, you are not managing two states.
You are managing **three**:
1. The closed or default state
2. The open state
3. The starting state used only for the entry transition
Here is the same example with all three states written explicitly:
```css
.toast {
display: none;
opacity: 0;
transform: translateY(12px);
transition:
opacity 0.25s ease,
transform 0.25s ease,
display 0.25s allow-discrete;
}
.toast.is-open {
display: block;
opacity: 1;
transform: translateY(0);
}
@starting-style {
.toast.is-open {
display: block;
opacity: 0;
transform: translateY(12px);
}
}
```
Now the browser knows:
- what the element looks like when it is closed
- what it should look like when it is open
- what values it should transition **from** when it first appears
This repeated `display: block` is the part many developers find weird the first time.
It feels redundant, but it is necessary.
The element must be considered visible for the browser to animate its visible properties.
## Why `@starting-style` feels a bit strange
There are two details that make this feature feel more confusing than it really is.
The first is repetition.
You often repeat some values between the closed state and the starting state, especially `opacity` and transform values.
The second is order.
`@starting-style` does not create any special cascade priority. It has the same specificity as the rule it mirrors, so it needs to come **after** the open-state rule.
This works:
```css
.toast.is-open {
opacity: 1;
transform: translateY(0);
}
@starting-style {
.toast.is-open {
opacity: 0;
transform: translateY(12px);
}
}
```
This does not:
```css
@starting-style {
.toast.is-open {
opacity: 0;
transform: translateY(12px);
}
}
.toast.is-open {
opacity: 1;
transform: translateY(0);
}
```
If you put the starting styles first, the open rule simply overrides them before the transition can use them.
## Where `@starting-style` becomes genuinely useful
The best use cases are not decorative hover effects.
The real value shows up when elements:
- enter the DOM
- change from `display: none`
- move into the top layer
That means things like:
- dialogs
- popovers
- toasts
- drawers
- menus
These are exactly the components that used to need extra JavaScript choreography just to animate in cleanly.
With `@starting-style`, the CSS can own more of that behavior.
This solves the motion part of the component. For the same dialog or popover, [`commandfor` can handle opening and closing](/blog/html-command-commandfor-without-javascript/), while [CSS Anchor Positioning can place a popover beside its trigger](/blog/css-anchor-positioning-without-javascript/). Each feature has a separate job, so you can use them together without tying the interaction to one large script.
## A dialog example
Native `` is a great example because it already has a clear open/closed lifecycle.
```css
dialog {
opacity: 0;
transform: translateY(16px) scale(0.98);
transition:
opacity 0.25s ease,
transform 0.25s ease,
overlay 0.25s allow-discrete,
display 0.25s allow-discrete;
}
dialog[open] {
opacity: 1;
transform: translateY(0) scale(1);
}
@starting-style {
dialog[open] {
opacity: 0;
transform: translateY(16px) scale(0.98);
}
}
```
Here the browser handles the dialog's visibility state for you.
You are mostly defining the visual transition.
If you also want to animate the backdrop, use a standalone block:
```css
dialog::backdrop {
background-color: rgb(0 0 0 / 0);
transition:
background-color 0.25s ease,
overlay 0.25s allow-discrete,
display 0.25s allow-discrete;
}
dialog[open]::backdrop {
background-color: rgb(0 0 0 / 0.35);
}
@starting-style {
dialog[open]::backdrop {
background-color: rgb(0 0 0 / 0);
}
}
```
That gives you a cleaner, fully CSS-driven entry effect for both the panel and its backdrop.
## Entry and exit do not have to match
This is another subtle but powerful part of `@starting-style`.
The starting state is only used for the entry transition.
When the component closes again, the browser transitions back to the normal closed state.
So the in and out motions can be different.
```css
.panel {
display: none;
opacity: 0;
transform: translateX(40px);
transition:
opacity 0.3s ease,
transform 0.3s ease,
display 0.3s allow-discrete;
}
.panel.is-open {
display: block;
opacity: 1;
transform: translateX(0);
}
@starting-style {
.panel.is-open {
display: block;
opacity: 0;
transform: translateX(-40px);
}
}
```
This panel enters from the left, but exits to the right.
That is a nice pattern for drawers, notifications, or stacked UI where the direction helps communicate intent.
## Browser support and production use
According to MDN, `@starting-style` is Baseline 2024 and has worked across the latest browser versions since August 2024.
That said, "Baseline" does not mean "safe for every device your users might still have".
Older browsers will simply skip the entry transition.
That is why I see `@starting-style` as a very good progressive enhancement:
- the component still works
- the UI just appears without the nicer transition
That is a good tradeoff.
If the animation is essential to understanding the interface, do not rely on it alone.
But for dialogs, menus, drawers, and other UI polish, it is a strong modern CSS tool.
## Final thoughts
`@starting-style` is not hard because the syntax is complicated.
It feels hard because it forces you to think in one extra state:
not just closed and open, but also the temporary starting state used for the first transition.
Once that clicks, the feature becomes much more predictable.
And more importantly, it removes one more category of JavaScript that used to exist mostly to patch a CSS limitation.
That is exactly the kind of feature I like seeing in the platform.
## Enjoyed this article?
I write about modern CSS, HTML, and simpler ways to build for the web.
Join my newsletter below if you want to learn modern CSS with practical tutorials like this.
Happy coding!
---
# How to Create Animated Gradient Borders in CSS
URL: https://theosoti.com/blog/animated-gradient-borders/
Published: 2026-04-02
Tags: CSS, modern CSS, CSS gradients, JavaScript alternatives, frontend development, frontend tutorial
> Animated gradient borders are easier with modern CSS: conic gradients, pseudo-elements, and @property replace JavaScript-heavy tricks.
## Custom Border Animations
This article is adapted from a chapter of my book, [You Don’t Need JavaScript](https://theosoti.com/you-dont-need-js/).
Animated borders used to require heavy tricks: JavaScript continuously redrawing gradients, or SVG filters layered behind content. Today, modern CSS gives us all the ingredients natively. With a pseudo-element, a conic gradient, and the new @property rule, we can create glowing, rotating borders that feel alive.
Final result: glowing border moving indefinitely
If you prefer, I also have a video walkthrough of this effect on YouTube:
VIDEO
## The Basic HTML Structure
The markup is minimal:
```html
```
The work all happens in CSS.
## Preparing the card
The card needs to establish itself as a positioning context so that its pseudo-elements can sit exactly behind it. It also needs a solid background to prevent the animated gradient from leaking through.
```css
.card {
position: relative;
z-index: 1; /* keep content above the border */
background-color: white; /* hides gradient behind the face */
}
```
Without the background color, the gradient layer we’ll add would bleed through, competing with the card’s content.
Result if you don’t add a background to the card
## Adding a border layer
We create the “border” not with border itself, but with a pseudo-element stretched slightly larger than the card.
```css
.card::after {
content: '';
position: absolute;
inset: -4px; /* expand 4px beyond every edge */
z-index: -1; /* push it behind the card */
border-radius: inherit;
}
```
The negative inset makes the pseudo-element stick out beyond the card, visually becoming its border. Inheriting the radius ensures corners line up perfectly.
## Declaring an animatable property
We want this border to rotate smoothly. To do that, we need a custom property for the angle — but not all custom properties can animate by default. The @property at-rule tells the browser that --angle should behave like a real angle value, so it can be interpolated in keyframes.
```css
@property --angle {
syntax: '';
initial-value: 0deg;
inherits: false;
}
```
This registers --angle as an angle type, starting at 0deg. The inherits: false ensures each card manages its own angle, rather than inheriting from a parent.
## Drawing the gradient
Now we paint the border using a conic gradient that rotates based on --angle:
```css
.card::after {
/* ...previous declarations... */
background: conic-gradient(
from var(--angle),
#ff4545,
#00ff99,
#006aff,
#ff0095,
#ff4545 /* repeat first stop for a seamless loop */
);
}
```
The repeating first color stop avoids a visible jump where the gradient loops. Even without animation, this already produces a colorful border.
Gradient border result (no glowing)
## Animating the spin
To bring it to life, animate --angle from 0 to 360 degrees in an infinite loop:
```css
.card::after {
/* ...previous declarations... */
animation: spin 3s linear infinite;
}
@keyframes spin {
to {
--angle: 360deg;
}
}
```
smooth, endlessly rotating gradient border
## Adding the glow
The border already rotates, but we can make it feel more alive by giving it a soft halo. We do this by duplicating the gradient in another pseudo-element and applying blur.
```css
/* Shared border setup for both layers */
.card::after,
.card::before {
content: '';
position: absolute;
inset: -4px;
z-index: -1;
border-radius: inherit;
background: conic-gradient(from var(--angle), #ff4545, #00ff99, #006aff, #ff0095, #ff4545);
animation: spin 3s linear infinite;
}
/* Blur and fade the ::before layer for a glow effect */
.card::before {
filter: blur(1.5rem);
opacity: 0.8;
}
```
Here’s what’s happening in layers:
The ::after pseudo-element is our crisp, sharp border.
The ::before pseudo-element is the exact same gradient, but blurred and semi-transparent. The blur makes the bright colors spill outward beyond the edge, softening into a glow.
Because both are positioned in the same place, the sharp version sits on top and the blurred version radiates beneath it.
Together, they produce the illusion of a glowing border without any extra graphics or images.
Border + glow effect result
## Respecting reduced motion
Animations should never be forced on users who prefer static interfaces. We can turn the spin off gracefully with a media query:
```css
@media (prefers-reduced-motion: reduce) {
.card::after {
animation: none;
}
}
```
This leaves a colorful static border for users who opt out of motion.
## Final thoughts
With modern CSS, custom border animations no longer need JavaScript workarounds or SVG tricks. A pseudo-element, a conic gradient, and `@property` are enough to create a border that feels polished, animated, and surprisingly lightweight.
Like any strong visual effect, it works best when used with intention. Keep it for featured surfaces, and keep the reduced-motion fallback in place so the effect stays decorative rather than distracting.
You can check a codepen here: https://codepen.io/editor/theosoti/pen/019d123a-1628-7b30-967f-8ce6e24ddd87.
Or you can check out a live example on my landing page: https://theosoti.com/you-dont-need-js/.
## Enjoyed this article?
I write about modern CSS, HTML, and simpler ways to build for the web.
Join my newsletter below if you want to learn modern CSS with practical tutorials like this.
Happy coding!
---
# CSS Relative Colors: Build Palettes Without Color Pickers
URL: https://theosoti.com/blog/css-relative-colors/
Published: 2026-01-26
Tags: CSS, modern CSS, CSS colors, CSS shadows, frontend development, frontend tutorial
> CSS relative colors turn one base color into hover states, borders, shadows, and accents, a practical modern CSS pattern for UI systems.
## Why relative colors feel like cheating
If you ever built a tiny design system, you already know the loop: pick a brand color, then hand craft a hover state, a border, a shadow, a muted background, and a highlight. Five minutes later you have a handful of unrelated hex values that look correct but do not scale.
The problem gets worse as soon as you add more contexts. Dark mode, marketing pages, seasonal themes, or a new product line all require a new set of tweaks. Manual palettes start to drift, and every refactor becomes a color audit.
Relative colors break that loop. They let you reference a source color and adjust its channels directly in CSS. One base becomes a small palette, and that palette stays consistent as you change the base later.
I like to think of relative colors as relationships, not values. The system says, "hover is 12% darker than the base" instead of "hover is #3d56d6." That mental shift removes a lot of tiny decisions from day to day UI work.
⚠️ **Quick support note:** global support for relative color syntax is [about 89.64%](https://caniuse.com/css-relative-colors) as of February 2026, so it's up to you if you want to use it now.
## The old workflow: manual palette drift
Here is the classic setup. You define a base color, then you create a new value for every variation you need:
```css
:root {
--brand: #4b6bff;
--brand-hover: #3d56d6;
--brand-active: #2f45b2;
--brand-border: rgba(75, 107, 255, 0.2);
--brand-soft: #e9edff;
}
.button {
background-color: var(--brand);
border-color: var(--brand-border);
}
.button:hover {
background-color: var(--brand-hover);
}
.button:active {
background-color: var(--brand-active);
}
```
This works, but each value is hand picked. If you ever change `--brand`, you also have to re-pick the hover, active, border, and soft background. That is why palettes drift over time.
The subtle issue is that every token becomes a frozen decision, not a relationship. Once you lose the relationship, you lose the system: changing the base color means re‑discovering the entire palette from scratch, and a new page or theme multiplies that work.
## Meet relative colors
Relative colors let you extract channels from an existing color and modify them in place. You can do that in `hsl()`, `rgb()`, `lch()`, or `oklch()`.
Here is the core idea:
```css
.button {
--brand: #4b6bff;
--shift: -12;
--hover: hsl(from var(--brand) h s calc(l + var(--shift)));
--border: rgb(from var(--brand) r g b / 0.2);
background-color: var(--brand);
border-color: var(--border);
}
.button:hover {
background-color: var(--hover);
}
```
The hover and border are now derived from the same base value. Update `--brand` and the rest updates automatically.
The syntax looks strange at first, but it follows a simple pattern:
```css
hsl(from h s l / )
```
- `from ` is the source. It can be a hex value, a CSS variable, `currentColor`, or another color function.
- `h`, `s`, and `l` are placeholders for the original channels.
- You can replace any channel with a number or a `calc()` expression.
Examples:
```css
/* Shift hue by 30deg */
--accent: hsl(from var(--brand) calc(h + 30) s l);
/* Desaturate and brighten */
--soft: hsl(from var(--brand) h calc(s - 20) calc(l + 20));
/* Keep RGB but lower alpha */
--shadow: rgb(from var(--brand) r g b / 0.25);
```
The mental model is simple: pick a source, then nudge the channel you care about.
## Choose the right color space
Relative colors work in the space you choose. That choice affects how the results look:
- **RGB** is great for alpha tweaks and direct channel edits. It is not perceptual, so changing channels can feel uneven.
- **HSL** is quick for hue and saturation shifts. It is intuitive, but the lightness channel does not match human perception.
- **OKLCH** is the most consistent for lightness changes. It is a better default when you want palettes that feel balanced across hues.
Relative colors always convert the source into the output color space before doing any math. The result is expressed in that output space, which is why `hsl()` and `oklch()` can yield different shifts even with the same numeric offsets.
A good rule of thumb is to use `rgb()` for transparency and `oklch()` for lightness. Reach for `hsl()` when you want fast adjustments or you need wide support.
## Build a mini palette from one variable
A practical way to start is to define a handful of tokens. You can keep the palette small and still cover most UI needs.
This recipe builds seven tokens from one base color and keeps everything in sync.
OKLCH uses different numeric ranges than HSL. Lightness resolves to 0–1, chroma resolves to 0–0.4 in relative syntax, and hue resolves to 0–360. That is why these offsets are written as small decimals instead of percentages.
Here is the token recipe that powers the palette:
```css
:root {
--brand: #4b6bff;
--strength: 0.12;
--strong-color: oklch(from var(--brand) calc(l - 0.12) c h);
--soft-color: oklch(from var(--brand) calc(l + 0.4) c h);
--accent-color: oklch(from var(--brand) l c calc(h + 20));
--brand-border: rgb(from var(--brand) r g b / 0.18);
--brand-shadow: rgb(from var(--brand) r g b / 0.3);
}
```
This small set is usually enough for a button, a badge, a soft background, and a border. If you need more depth, add one extra step for a darker and lighter variation.
## A component recipe you can copy
Relative colors shine when a component needs multiple accents. Here is a compact pattern for a callout card:
Keep derived tokens in one space so the relationships stay coherent. Mixing spaces is valid, but remember the source is converted to the output space for every calculation, which can subtly change the result.
```css
.callout {
--tone: #4b6bff;
--tone-strong: hsl(from var(--tone) h s calc(l - 12));
--tone-soft: hsl(from var(--tone) h calc(s - 20) calc(l + 28));
--tone-border: rgb(from var(--tone) r g b / 0.2);
--tone-shadow: rgb(from var(--tone) r g b / 0.25);
border: 1px solid var(--tone-border);
background-color: var(--tone-soft);
box-shadow: 0 12px 24px var(--tone-shadow);
}
.callout strong {
color: var(--tone-strong);
}
```
The component only needs one input (`--tone`). Everything else follows.
## Altering the lightness channel
If you want a quick set of tints and shades, you can keep the hue and saturation and only shift the lightness. The slider below changes a single `--step` value, and every swatch is derived from the same formula:
```css
.swatch {
--offset: calc(var(--step) * var(--i) / 100); // i is the swatch index
oklch(from var(--brand) calc(l + var(--offset)) c h);
}
```
Here `--step` is a plain number. Because HSL lightness runs from 0 to 100, a step of 5 means “five lightness points.” Values are clamped to the channel range, so going past the ends just pins at 0 or 100.
## HSL vs OKLCH (why the difference matters)
HSL is fast and familiar, but its lightness channel is not perceptual. The same +10% shift can feel dramatic on one hue and barely visible on another.
OKLCH is much more consistent. It keeps lightness changes feeling even across different hues, which is why it is great for UI palettes.
Also note the unit differences: HSL uses a 0–100 scale for lightness and saturation, while OKLCH lightness is 0–1 and chroma values are much smaller. That means a `+0.1` OKLCH lightness shift is already significant.
Here is a side by side comparison using the same lightness shift in HSL and OKLCH.
If you want predictable results across colors, OKLCH is usually the better space for lightness adjustments.
## Use relative colors with currentColor
A neat trick is using `currentColor` as the source. That lets icons, borders, and highlights follow the text color automatically.
```css
.badge {
color: #1d4ed8;
border: 1px solid rgb(from currentColor r g b / 0.2);
background-color: oklch(from currentColor clamp(0%, calc(l + 40%), 100%) c h);
}
```
`currentColor` resolves to the computed text color of the element. That means any change that affects the actual computed color (inheritance, state styles, media queries) will flow through the derived colors automatically.
One change to `color` updates the border and background without touching any other variables.
## The payoff
Once you start using relative colors, you stop thinking in isolated hex values. You think in relationships: hover is a darker brand, borders are the same brand with lower alpha... and your palette stays coherent by default.
If you need older browser support, consider a fallback palette.
If you are building a design system or cleaning up a few components, this is one of the CSS features that makes everything feel simpler.
## Enjoyed this article?
I write about modern CSS, HTML, and simpler ways to build for the web.
Join my newsletter below if you want to learn modern CSS with practical tutorials like this.
Happy coding!
---
# Designing Better CSS Box Shadows Easily
URL: https://theosoti.com/blog/designing-shadows/
Published: 2025-12-25
Tags: CSS, modern CSS, CSS colors, CSS shadows, frontend development, frontend tutorial
> Better box shadows start with light direction, layering, and color. This modern CSS guide shows how to make UI depth feel less fuzzy.
## Shadows are not decoration
Most shadows on the web look like fuzzy gray borders. They technically separate layers, but they don't feel _real_. If you want a UI to feel tactile, shadows are the cheapest way to fake depth.
This article is inspired by Josh W. Comeau's deep dive on shadows, but I'll focus on a practical recipe you can reuse in your own components.
## Elevation is the real purpose
Shadows are a visual cue for elevation. A higher element looks like it is closer to the user, and its shadow gets larger, softer, and lighter as the distance from the surface increases. That is why elevation also controls focus and attention: your eyes naturally go to the closest thing.
Here's a simple slider that controls the elevation value used to build the shadow (and a light-angle control that rotates the x/y offsets):
## Pick a single light source
In the real world, shadows depend on a light source.
In CSS, the "light source" is expressed through your **x and y offsets**.
Remember: the shadow is cast away from the light. If the light is above and to the left, the shadow moves down and to the right (positive x and y).
Pick a direction (top-left is the usual choice), keep the ratio between x and y consistent, and scale both together:
```css
:root {
--shadow-color: hsl(220deg 20% 20% / 0.25);
--shadow-x: 2px;
--shadow-y: 7px;
--shadow-blur: 18px;
}
.card {
box-shadow: var(--shadow-x) var(--shadow-y) var(--shadow-blur) var(--shadow-color);
}
```
If every component uses a different angle or offset, the page will feel messy, even if each shadow looks nice in isolation.
## Build a tiny recipe
Instead of guessing numbers, build a small formula that scales with elevation. Use a length for `--elevation` so it can be multiplied directly:
```css
.card {
--elevation: 8px;
--shadow-color: 220deg 20% 20%;
--shadow-opacity: 0.25;
--x: calc(var(--elevation) * 0.25);
--y: calc(var(--elevation) * 0.9);
--blur: calc(var(--elevation) * 1.6 + 6px);
--spread: calc(var(--elevation) * -0.15);
box-shadow: var(--x) var(--y) var(--blur) var(--spread) hsl(var(--shadow-color) / var(--shadow-opacity));
}
```
Once the formula feels right, every component can share it.
The idea is simple:
- **Offset** (`--x` and `--y`) keeps a consistent light direction.
- **Blur** grows faster than elevation so higher cards feel softer.
- **Opacity** should usually decrease as elevation increases, otherwise shadows feel too heavy.
- **Negative spread** keeps the shadow from bloating outward and preserves crisp edges.
You can tweak the multipliers, but keep the relationship. That is what makes the shadow system feel coherent across the page.
## Layering makes shadows feel real
A single shadow rarely looks natural.
Real shadows are layered: a tight shadow near the object, and softer ones farther away.
Try toggling the layers to see how they combine:
The simplest pattern is **multiple, tightly-spaced layers** with the same opacity:
```css
.card {
box-shadow:
0 1px 1px hsl(0deg 0% 0% / 0.075),
0 2px 2px hsl(0deg 0% 0% / 0.075),
0 4px 4px hsl(0deg 0% 0% / 0.075),
0 8px 8px hsl(0deg 0% 0% / 0.075),
0 16px 16px hsl(0deg 0% 0% / 0.075);
}
```
Think of these as zones:
- **Contact shadow** (first lines): short blur, higher visual density. This anchors the card to the surface.
- **Ambient shadow** (last lines): large blur, faint opacity. This creates the soft halo that feels natural.
Notice how the blur radius grows at the same rate as the offsets in this example. That even scaling creates a smooth, natural falloff.
If the shadow still feels too puffy, try adding a small negative spread on the tightest layer.
Layered shadows are also more expensive to render. Keep the count low and avoid animating layered shadows on large elements.
## Color-match your shadows
Neutral black shadows can look muddy on colorful surfaces.
To keep the shadow believable, match its hue to the environment and adjust saturation/lightness until it feels right.
```css
body {
--background: hsl(220deg 100% 80%);
background-color: var(--background);
}
.card {
background-color: #fff;
box-shadow: 1px 2px 8px var(--shadow-color);
}
.card--too-gray {
--shadow-color: hsl(0deg 0% 0% / 0.5);
}
.card--too-bright {
--shadow-color: hsl(from var(--background) h s 50%);
}
.card--just-right {
--shadow-color: hsl(from var(--background) h 60% 50%);
}
```
The “just right” version keeps the hue but lowers saturation and lightness, so the shadow belongs to the scene instead of floating on top.
## Bonus: drop-shadow
`box-shadow` always uses the element's box.
`filter: drop-shadow()` follows the _actual rendered shape_, including transparent parts. Under the hood, `drop-shadow()` uses an SVG gaussian blur, so it looks and behaves a little differently than `box-shadow`.
It's perfect for speech bubbles, cutouts, or icons. You can also stack multiple `drop-shadow()` calls to get a richer falloff:
Here is the basic HTML and the tiny CSS that creates the bubble tip:
```html
Bubble text
```
```css
.bubble {
position: relative;
background: white;
padding: 1rem 1.2rem;
border-radius: 10px;
--shadow: hsl(0deg 0% 0% / 0.2);
}
.bubble::after {
content: '';
position: absolute;
left: 26px;
bottom: -20px;
width: 34px;
height: 24px;
background: inherit;
clip-path: polygon(50% 100%, 0 0, 100% 0);
}
```
```css
.bubble {
filter: drop-shadow(1px 2px 3px var(--shadow)) drop-shadow(2px 4px 6px var(--shadow))
drop-shadow(4px 8px 12px var(--shadow));
}
```
In many cases, `drop-shadow()` can be faster because `filter` effects can be GPU-accelerated. That said, Safari can struggle with filtered elements that contain inputs, so test before applying it broadly.
## Final thoughts
Shadows don't need to be complicated.
Pick a light source, scale your numbers, layer your blur, and tint the color.
That's enough to make your UI feel more intentional and more real.
## Enjoyed this article?
I write about modern CSS, HTML, and simpler ways to build for the web.
Join my newsletter below if you want to learn modern CSS with practical tutorials like this.
Happy coding!
---
# My Iterative Process to Design a Book Cover
URL: https://theosoti.com/blog/book-cover-creation/
Published: 2025-10-14
Tags: ebook design, web design, content creation, AI design tools
> A behind the scenes look at how I designed my ebook cover, from AI drafts and Photopea mockups to a clean, minimal design style.
## Introduction
When I started working on the cover for my book [You Don’t Need JavaScript](https://theosoti.com/you-dont-need-js/), I thought it would be quick.
Turns out, I went through a few different experiments before getting something that actually felt right.
Design is rarely linear. It’s more like a loop of small tests, tweaks, and re-thinking. This cover was exactly that kind of loop: a mix of experiments, quick wins, and a few dead ends that eventually shaped the final look.
From a white canvas to a finished book cover
## Trying with ChatGPT
My first idea was to let ChatGPT generate the cover for me.
It was a fun starting point. I described the kind of layout I wanted, the vibe, the green color, and the typography direction. It produced some nice compositions at first glance, especially for the shapes and framing.
But it really struggled with text and positioning.
The titles came out fuzzy, the fonts weren’t consistent, and sometimes letters would morph or overlap. It was clear that AI image generation still has a hard time with precise typography and visual hierarchy.
List of 3 books generated by ChatGPT
So instead of relying on it for the final image, I decided to use it more like a brainstorming partner. It gave me a few directions, but I knew I’d need to design the real thing by hand.
## Starting Fresh in Photopea
With that in mind, I opened [Photopea](https://www.photopea.com/), my favorite free Photoshop alternative.
It is online and does almost everything I need for design.
Photopea interface
I started with a blank canvas. No mockup, no 3D, just the flat cover.
That way, I could focus purely on the basics: layout, typography, and color.
I reused the green from my blog, but made it a bit deeper.
For the text, I picked an off-white instead of pure white. It felt calmer and softer on the eyes.
Then I chose a clean sans-serif font and started playing with letter spacing and sizing until it felt balanced.
I also worked on the central illustration.
I had asked ChatGPT to generate a few icon ideas: panels, sliders, and simple shapes that would hint at user interfaces and CSS.
One of those sketches had the right vibe, so I redrew and simplified it by hand. The result looked technical but friendly, which fit the ebook perfectly.
The first version looked okay, but it was missing something.
Flat colors and clean typography alone made it feel a bit too sterile.
First version of the book cover
## Creating the Background Pattern
To bring some life into it, I decided to create a subtle background pattern.
Again, I started with ChatGPT to generate a base texture: small geometric shapes and lines that echoed a "code-like" rhythm.
After exporting the pattern, I edited it manually in Photopea. I simplified the shapes, adjusted spacing, and reduced opacity until it was barely visible.
The idea was to make it something you feel rather than see, a quiet layer that gives the cover depth without distracting from the text.
That small detail made a huge difference.
It suddenly looked less flat, more tangible, and more polished. For the first time, I felt like I was getting close.
Final background pattern
## Getting Feedback
Once I was happy with this new version, I sent it to a friend for feedback.
He is honest and has a good eye, which is exactly what you need at that stage.
He pointed out a few things I had not noticed:
- The letter spacing in the title was a bit too tight
- The illustration was oversized
- One of the icons in the illustration wasn't representing the theme of the book well
- The subtitle could be more clear and impactful
- Delimitating my name from the subtitle
All small things, but he was right about all of them.
It was subtle tweaks, but the difference was obvious when comparing both versions side by side.
The comparison between the first and second version of the book cover
That feedback round really helped.
When you work on something for too long, you stop seeing it clearly.
Having a fresh set of eyes was what made it finally click.
## Wrapping It in a Mockup
Once the design felt right, I wanted to see it in context, as an actual book.
So I found a mockup on [mockups-design.com](https://mockups-design.com/free-book-mockups/), a clean `.psd` file with nice perspective and lighting.
I opened it in Photopea again, and that is when I really started to appreciate smart objects.
You can double-click the placeholder, paste your flat design, save it, and the mockup automatically updates in 3D.
It is a small thing, but it feels like magic every time.
The first time I saw my flat design wrapped around that 3D book, I smiled.
All the small iterations, the adjustments, and the feedback suddenly came together.
It looked real, and more importantly, it looked like something I had made.
The final version of the book cover
## Final Thoughts
Designing this cover took way longer than I expected.
I probably spent three or four evenings on it, tweaking, testing, and overthinking.
But that process made it better. Each version taught me something new about simplicity, balance, and restraint.
The funny part is that it follows the same philosophy as the ebook itself.
You do not always need the biggest tools or the fanciest effects, just clear thinking, good structure, and a few smart iterations.
I am really happy with how it turned out. It feels connected to my blog, consistent with my work, and simple enough to let the content speak first.
If you have not seen it yet, [You Don’t Need JavaScript](https://theosoti.com/you-dont-need-js/) is available.
It is a practical guide to building modern interfaces using only CSS.
## Enjoyed this article?
I write about modern CSS, HTML, and simpler ways to build for the web.
Join my newsletter below if you want more tutorials like this.
Happy coding!
---
# Learn Modern CSS Container Queries for Adaptive Layouts
URL: https://theosoti.com/blog/container-queries/
Published: 2025-04-04
Tags: CSS, modern CSS, container queries, responsive design, CSS layout, frontend tutorial
> Use container queries to build modern CSS components that respond to their own space, not only the viewport, with layouts you can reuse.
## The next step in responsive design: container queries.
For many years, responsive web design relied on media queries. You define some parameters, and the layout changes depending on the screen size. Ideal for adapting designs for phones, tablets, and desktops. But what if your components need to adapt to the real space they are in, rather than just the screen size?
Enter container queries: a modern CSS feature that lets you style components based on the size of their parent container. It’s useful for reusable layouts that need to work in different parts of a page.
For example:
- A Card component might look great in a wide sidebar but needs adjustments for a narrower one.
- A Profile block might stack its elements vertically in a small space but align them horizontally when there’s more room.
With container queries, your components don’t need to care about the entire screen anymore. They just care about their own box.
## How do container queries work?
To use container queries, the first step is to tell CSS which parent element should act as a "container." You do this with the container-type property. Here's an example:
```css
/* Define a container */
.parent {
container-type: inline-size;
}
```
What this does is let the `.parent` element report its **inline size** (basically, its width) to its child elements. Once that's set up, you can use the `@container` rule inside any child elements to adjust their styles dynamically.
```css
.child {
/* Default styles go here */
}
@container (min-width: 40rem) {
.child {
/* Apply these styles when the container's width is at least 40rem */
}
}
```
It’s kind of like saying, "Hey, if my box is big enough, let’s give this component a different look."
## Real-world example
Imagine you’ve got a ProfileCard component, and it needs to display a profile picture, username, and bio.
In a **small container**, you might want to stack everything inside the card:
But in a larger container, you’d want a more horizontal layout inside the card:
Here's the html I used for the card:
```html
```
With container queries, you can define a flexible design for this. Just mark the ProfileCard’s parent as a container, and then tweak the layout using something like this:
```css
.card {
display: grid;
grid-template-columns: 1fr;
grid-template-areas:
'avatar'
'infos'
'actions';
}
.card__avatar {
grid-area: avatar;
}
.card__info {
grid-area: infos;
}
.card__actions {
grid-area: actions;
}
@container (min-width: 18rem) {
.card {
grid-template-columns: 120px 1fr;
grid-template-areas:
'avatar infos'
'actions actions';
}
}
```
Try removing and adding cards to see how the layout changes. The card will adapt to the size of its parent container, making it a lot more flexible and reusable.
See how that works? The layout adjusts automatically depending on the space the component has to work with.
## Naming containers
By default, container queries target the nearest parent with a container-type set. But what if a component is nested inside multiple containers, and you want to target a specific one? That’s where container names come in.
You can give a container a custom name using the container-name property. Then, when writing your @container rules, you can refer directly to that name.
Here’s how it works:
```css
.sidebar {
container-type: inline-size;
container-name: sidebar;
}
```
Now that this container is named, you can write a query like this:
```css
@container sidebar (min-width: 30rem) {
.widget {
/* Styles specific to when the sidebar is wide enough */
}
}
```
This is super helpful when your layout has multiple containers, like a card inside a sidebar inside a grid. Instead of relying on CSS to guess which container you mean, you're telling it exactly where to look.
Quick tip: You can also set both the type and name in one line like this:
```css
.container {
container: sidebar / inline-size;
}
```
The format is:
container: ` / `;
This makes your CSS more predictable and your components easier to manage, especially in more complex layouts.
## New units for container queries
Container queries also introduce some exciting new units that make it easier to build responsive, context-aware designs. These units are specifically designed to work with the size of a container, not the viewport. Here are the key ones to know:
1. `cqw` (container width) - Represents the container's width.
```css
.child {
width: 50cqw; /* 50% of the container's width */
}
```
2. `cqh` (container height) - Represents the container's height.
```css
.child {
height: 20cqh; /* 20% of the container's height */
}
```
3. `cqi` (container inline size) - Based on the container’s inline size (width in LTR or height in top-to-bottom layouts).
```css
.child {
margin-left: 5cqi; /* 5% of the container’s inline size */
}
```
4. `cqb` (container block size) - Based on the container's block size (height in most layouts).
```css
.child {
padding-bottom: 2cqb; /* 2% of the container's block size */
}
```
5. `cqmin` (minimum container size) - Based on the smaller of the container’s width or height.
```css
.child {
font-size: 2cqmin; /* 2% of the container's smallest dimension */
}
```
6. `cqmax` (maximum container size) - Based on the larger of the container’s width or height.
```css
.child {
font-size: 3cqmax; /* 3% of the container's largest dimension */
}
```
These units give you flexibility to create designs that react to the container’s actual size, making your components more adaptable to different contexts.
## What about browser support?
Good news, container queries are here, and they’re supported by **most modern browsers**! As of today, support is solid for about 93,6% of users. That said, for browsers that don’t support them (looking at you, legacy setups), it’s smart to design with **progressive enhancement** in mind.
Here’s the strategy:
1. Start with a solid set of default styles that look good no matter what.
2. Layer container-query rules on top for enhanced layouts when supported.
## Ready to Learn More?
If you’re curious to dig even deeper, Josh W. Comeau’s article, [A friendly introduction to container queries](https://www.joshwcomeau.com/css/container-queries-introduction/), is an incredible resource with more examples and detailed explanations. Definitely worth a read!
---
# How to Implement Scroll-Driven Animations Using Pure CSS
URL: https://theosoti.com/blog/scroll-driven-animation/
Published: 2025-01-06
Tags: CSS, modern CSS, scroll-driven animations, CSS animations, performance, frontend development, frontend tutorial
> Scroll-driven animations connect CSS animation progress to scroll position, a modern CSS pattern for reveals, timelines, and sticky effects.
## Introduction to scroll-driven animations
Scroll-driven animations are a great way to enhance web experiences by tying animations to scrolling. Think of parallax effects, where backgrounds shift as you scroll, or elements that fade in as they come into view.
In the past, creating these effects meant handling scroll events (with javascript) on [the main thread](https://developer.mozilla.org/en-US/docs/Glossary/Main_thread), which often led to choppy results. But with the [Scroll-driven Animations specification](https://drafts.csswg.org/scroll-animations-1/), you can now create smooth, responsive animations declaratively.
These new APIs work seamlessly with the [Web Animations API](https://drafts.csswg.org/web-animations-1/) and [CSS Animations API](https://drafts.csswg.org/css-animations/), allowing scroll-driven animations to run off the main thread.
The result? Silky-smooth animations that are easy to implement with just a few lines of code.
Here is an example of a component working with the scroll-driven animation:
## Scroll Timeline
By default, animations run on the document’s main timeline, where time determines progress. But with the new Scroll Timeline API, you can make animations scroll-driven. Instead of progressing over a fixed duration, the animation’s progress is tied directly to the scroll position of a scroll container.
Here’s how it works:
- At **0% progress**, the animation corresponds to the start of the scroll.
- At **100% progress**, the animation completes at the end of the scroll.
This approach allows you to control animations dynamically through scrolling, offering a more interactive experience.
To illustrate, here’s a simple animation that runs as soon as the page loads. The element scales horizontally over a fixed two-second duration:
```css
@keyframes progress {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
#progress {
animation: progress 2s linear infinite running;
}
```
Now, let’s make this animation scroll-driven. With the `animation-timeline` property and `scroll()` function, the animation’s progress is tied to the scroll position. We no longer need to define a fixed duration neither an iteration count, as the scroll position determines the timing:
```css
@keyframes progress {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
#progress {
animation: progress linear forwards;
animation-timeline: scroll();
}
```
By default, the `scroll()` function uses the nearest scrollable ancestor as the scroller. But it can also take two optional parameters to customize its behavior:
- **``**: Specifies the axis of scrolling that drives the animation.
- Possible values: **block** (default), **inline**, **x**, or **y**.
- **``**: Specifies the scroll container element whose scroll position drives the animation.
- Possible values: **nearest** (default), **root**, or **self**.
With these options, you can fine-tune the behavior of scroll-driven animations to match your design needs.
VIDEO
## View Timeline
The View Timeline is a specialized type of scroll timeline. While it responds to scrolling, it behaves more like the JavaScript Intersection Observer API. The key difference is that the animation progress starts only when the target element begins entering the viewport and ends when it leaves the viewport.
Just like with scroll-driven animations, we use the `animation-timeline` property, but this time we call the `view()` function instead of `scroll()`.
The `view()` function can take two optional parameters:
- **``**: Determines the axis of scrolling that drives the animation progress.
- Possible values: **block** (default), **inline**, **x**, or **y**.
- **``**: Adjusts the visibility range for the animation progress within the viewport.
- Possible values: **auto** (default) or a custom `` value.
Unlike the `scroll()` function, there’s no scroller parameter here. The `view()` function always uses the nearest scrollable ancestor as its reference.
Here’s an example where images fade & slide in as they become visible in the viewport:
```css
@keyframes fade-in {
to {
opacity: 1;
transform: translateX(0);
}
}
.image {
opacity: 0.2;
transform: translateX(-100px);
animation: fade-in linear forwards;
animation-timeline: view();
}
```
It’s nice but not perfect. We never see the full image at 100% opacity. The image should have full opacity at around 50% of the viewport.
The initial idea might be to adjust the keyframes to end at 50%. While this approach could work, it’s not easy to maintain. For example:
```css
@keyframes fade-in {
0% {
opacity: 0.2;
transform: translateX(0);
}
50% {
opacity: 1;
transform: translateX(1);
}
}
```
Thankfully, there is an easier way to do this. You can adjust your animation attachment with timeline ranges.
## Timelines Ranges
To further customise scroll-driven animations, you can use the `animation-range` property in combination with `animation-timeline`. This property allows you to define the exact range within which your animation should run.
By default, `animation-range` is set to **`normal normal`**, which is shorthand for `animation-range-start: normal;` and `animation-range-end: normal;`. This default configuration corresponds to the animation running from **0%** to **100%** of the timeline. In CSS terms, it can be expressed as: `animation-range: 0% 100%;`
But you’re not limited to percentages! The `animation-range` property lets you specify **lengths** or **percentages** to fine-tune when the animation starts and ends relative to the scroll position.
For instance, if we get back to our previous example, you can define an animation that starts when the scroll offset reaches `100px` and ends at `50%`:
```css
@keyframes fade-in {
to {
opacity: 1;
}
}
.image {
opacity: 0.2;
transform: translateX(-100px);
animation: fade-in linear forwards;
animation-timeline: view();
animation-range: 100px 50%;
}
```
In this example:
- The animation begins when the element’s scroll offset reaches **100px**.
- The animation completes as the scroll offset reaches **50%**.
This flexibility allows you to create more precise, visually engaging animations that align perfectly with your design goals.
With that there are already a lot of scroll animations possible with pure CSS.
## Browser Support
Scroll-driven animations in CSS are gaining attention but aren’t yet universally supported. Currently, they have around 74% global support. While Chrome and Edge support them starting from version 115, Safari and Firefox have yet to implement this feature.
However, this shouldn’t discourage us from using scroll-driven animations. They can be implemented as a progressive enhancement, meaning non-critical animations won’t affect the overall user experience if they aren’t visible in unsupported browsers.
To handle browser compatibility, the `@supports()` rule in CSS allows you to check if a specific property is supported. This makes it easy to add fallbacks or even display warning messages for unsupported browsers. Additionally, [polyfills](https://github.com/flackr/scroll-timeline) are available to bring scroll-driven animations to more browsers, ensuring a wider reach.
## Tools & Resources
To visualise and debug Scroll-Driven Animations on your own site: [Scroll-Driven Animations Debugger extension for Chrome DevTools](https://chromewebstore.google.com/detail/scroll-driven-animations-debugger/ojihehfngalmpghicjgbfdmloiifhoce).
To visualise all the possible outcome for the view timeline ranges: [scroll-driven-animations.style/tools/view-timeline/ranges](https://scroll-driven-animations.style/tools/view-timeline/ranges/)
To deep dive into scroll driven animations: [scroll-driven-animations.style](https://scroll-driven-animations.style/).
Great video from Kevin Powell about scroll driven animations: [youtube.com/watch?v=UmzFk68Bwdk](https://www.youtube.com/watch?v=UmzFk68Bwdk).
## Enjoyed this article?
I write about modern CSS, HTML, and simpler ways to build for the web.
Join my newsletter below if you want to learn modern CSS with practical tutorials like this.
Happy coding!
---
# CSS :focus Hack for Better UX Without JavaScript
URL: https://theosoti.com/blog/css-focus-hack/
Published: 2024-11-18
Tags: CSS, modern CSS, accessibility, performance, JavaScript alternatives, frontend tutorial
> Use :focus-visible for modern CSS focus states so keyboard users keep clear rings and mouse users avoid unwanted click outlines.
## Introduction to focus states
CSS Focus states might seem straightforward, but in reality, there are multiple selectors that control how and when focus is displayed on elements.
There are three primary focus states in CSS, plus a “secret” fourth one.
Understanding how to use these focus states correctly can elevate accessibility, create smoother interfaces, and improve user navigation.
Try playing with these examples below by tabbing or clicking and see the differences:
Now that you have a basic understanding, let's deep dive into this `:focus` universe to see when and where to implement them.
## The basic :focus state
The simplest of all focus states is `:focus`.
```css
a:focus {
outline: 2px solid deeppink;
}
```
When you apply this to an element, it shows a style whenever that element is focused.
The focus can occur from clicking, tabbing, or using other navigation methods.
For example, if you tab through this site, you will see a black or white (depending on dark mode) outline on links and buttons.
But if you try to click on any buttons or links, it won’t show anything.
**Why you ask?**
Because, for this site, I used :focus-visible on almost all the links!
## The :focus-visible state
Unlike `:focus`, which shows styles regardless of how the user interacts with the element, `:focus-visible` only displays focus styles when they’re visually necessary.
```css
a:focus-visible {
outline: 2px solid deeppink;
}
```
For instance, when navigating by keyboard, `:focus-visible` triggers, but with a mouse, it won’t display any outline.
This keeps the interface looking clean, while still offering guidance for keyboard users.
So, how do you decide between `:focus` and `:focus-visible`?
Here’s my rule:
- Use `:focus` if you want a style that shows up with any type of interaction. In general all inputs related to a form.
- Use `:focus-visible` for a cleaner look, where focus only appears when helpful for the user. In general, for all other types of elements (links, buttons, etc.).
If you noticed on my website, the only element that have a simple `:focus` state is the input of my newsletter. Everything else uses `:focus-visible`.
Now that we’ve reviewed the 2 basic focus states, let’s dive into the advanced state `:focus-within`.
## The advanced :focus-within state
Another useful focus state is :focus-within, which allows you to style a parent element based on whether any child element inside it is focused.
```css
.container:focus-within {
outline: 2px solid deeppink;
}
```
This is especially useful for complex components like dropdowns or modal containers.
This will trigger for each user interaction on the selected area (click, tab, or other navigation methods).
For now we reviewed `:focus`, `:focus-visible` and `:focus-within`.
So, the next state to present is logically `:focus-within-visible`, right?
Well, almost, but not excatly. Here is this fourth secret state!
## The “secret” fourth :focus-visible-within state
While CSS doesn’t directly support a `focus-visible-within` selector, you can achieve this effect using the `:has` pseudo-class.
```css
.container:has(a:focus-visible, button:focus-visible) {
outline: 2px solid deeppink;
}
```
This “secret” selector applies the focus only when it’s necessary for the user’s navigation style, similar to `:focus-visible`, but within a container context.
By combining `:has` with `:focus-visible`, you can set up a container element to style itself based on the visibility of focus within its child elements.
This is particularly useful for custom interface elements, where a parent container needs to indicate focus only in visually relevant scenarios.
## Enjoyed this article?
I write about modern CSS, HTML, and simpler ways to build for the web.
Join my newsletter below if you want to learn modern CSS with practical tutorials like this.
Happy coding!
---
# Smooth CSS Height Transitions Without Max-Height
URL: https://theosoti.com/blog/height-transition/
Published: 2024-10-22 | Updated: 2025-04-14
Tags: CSS, modern CSS, CSS animations, frontend development, frontend tutorial
> Animate dynamic height in modern CSS without max-height hacks, using patterns that fit accordions, drawers, and expanding content.
## Introduction to height transition in CSS
The current best approch in term of height transition, is using the grid transitions. But other cool solutions exists or will be existing in the future.
In this article, I'll show you five ways to transition an element's height. We'll start with older, less reliable methods and move on to modern techniques that work best.
All the examples I provide below are interactive, so feel free to experiment with them!
## Common height transitions
We’ve all wanted to transition an element’s height directly. Unfortunately, it’s not that simple.
If you know the final height and set it as a static value, it works perfectly. But in most cases, the element's height is dynamic and unpredictable.
Here is the HTML:
```html
Toggle dropdown
```
Here is the CSS:
```css
.dropdown {
overflow: hidden;
height: 210px; /* example 1 */
height: auto; /* example 2 */
transition: height 0.5s ease;
}
.hide {
height: 0px;
}
```
Then I toggle the .hide class with some JS by clicking on the button.
On the demo below you can see:
- For the first one, the animation works because there is an explicit height.
- For the second one, there are no animation because the height is set to auto.
## Transitioning the max-height in CSS
Since transitioning height directly doesn’t work well, what about using max-height?
Sadly, like the height value, you can't directly animate it's height to an auto value.
This approach is still a bit better since you can set a larger value, and the animation will work.
However, if the value is too high, the transition speeds up too quickly and becomes erratic.
Here is the CSS:
```css
.dropdown {
overflow: hidden;
max-height: 100px; /* | 400px | 1000px */
transition: height 0.5s ease;
}
.hide {
max-height: 0px;
}
```
Try toggling the content by clicking the button and changing the max-height values:
You see that if the max-height has a value too big, the animation is too quick and delayed.
And if the value is too low, the text will be cropped.
## Height transition with flex
The previous examples aren’t ideal, as they come with many limitations.
But did you know you can transition the height to 100% using flex?
The HTML structure changes a bit (there is one more div inside the dropdown):
```html
Toggle dropdown
```
And the CSS looks like that:
```css
.dropdown {
display: flex;
}
.inner {
max-height: 0;
overflow: hidden;
transition: max-height 0.5s linear;
}
.dropdown:not(.hide) .inner {
max-height: 100%;
}
```
See the example below:
It works, but there are still two issues:
- You need to add a wrapper div for it to function properly
- While the content transitions, its container expands instantly
## Height transition with grid
Another more flexible solution is using CSS Grid. Similar to the flex approach, Grid can help us transition an element’s height, but with better control and fewer limitations.
In this case, we’ll utilize the grid-template-rows property to animate the height.
Here’s the updated HTML:
```html
Toggle dropdown
```
And the CSS:
```css
.dropdown {
grid-template-rows: 0fr;
transition: grid-template-rows 0.5s ease;
display: grid;
}
.inner {
overflow: hidden;
}
.dropdown:not(.hide) {
grid-template-rows: 1fr !important;
}
```
In this example:
- grid-template-rows: 0fr hides the content, while 1fr makes the content fully visible.
- The height smoothly transitions as the grid adjusts.
With Grid, there’s no need for extra wrapper divs, and the container height adjusts smoothly along with its content.
It also gives you finer control over the layout compared to flexbox.
## New height transitions in the future?
One promising feature is the upcoming calc-size() function, which will bring even more flexibility to CSS layouts.
What this function does is convert values like auto to specific pixel values which it can then use in calculations with other values.
This is handy on its own, but where it is most useful is with animating elements that are auto sized.
Here is what it looks like:
```css
.dropdown {
height: 0;
overflow: hidden;
transition: height 0.3s;
}
.dropdown:not(.hide) {
height: calc-size(auto);
}
```
The support is almost inexistant as today, but I believe it will spread soon to all major browsers!
## Enjoyed this article?
I write about modern CSS, HTML, and simpler ways to build for the web.
Join my newsletter below if you want to learn modern CSS with practical tutorials like this.
Happy coding!
---
# How to Build a Dark Mode Toggle in Pure CSS
URL: https://theosoti.com/blog/darkmode-css/
Published: 2024-09-30
Tags: CSS, modern CSS, accessibility, performance, CSS theming, JavaScript alternatives, frontend development
> Build an accessible dark mode toggle with modern CSS, system preference support, and no JavaScript for the core theme switch.
## Introduction to darkmode
In this step-by-step tutorial, I'll show you how to create a dark mode for your website using minimal JavaScript. You can follow along and see the results in real time with the interactive elements included.
There are various ways to implement dark mode, ranging from simple to complex. Here, I'll walk you through a straightforward method that relies on three key principles:
- **CSS variables** to easily switch color values between modes
- A **checkbox** (or other input) to capture the user's preference
- **localStorage** to make the selected mode persist across pages
This is the same approach I've used on this site, so feel free to check it out!
## Setting up the HTML
First things first, let's create a basic html template.
We'll need a **checkbox** and **some content**.
```html
```
We have our HTML structure, now let's set the colors.
## Add some CSS variables
Let's declare **3 CSS variable** for now.
Put them in the **:root selector** so you are sure they are accessible everywhere.
We declare 1 for the text, 1 for the background and one for the links.
```css
:root {
--c-text: #493c37;
--c-background: #eddfe0;
--c-primary: #b392f0;
}
```
Now let's link these variables to their elements.
```css
h1,
p {
color: var(--c-text);
}
main {
background-color: var(--c-background);
}
a {
color: var(--c-primary);
}
```
From now on, when you change the value of the CSS variables, it will impact all the elements linked to it.
You can try and change the color values in the interactive example below.
Now we have our HTML and CSS variables set up, but we still miss the main part: the darkmode.
It's pretty simple from here. We have to define the colors we want when we enter the darkmode. We also have to define when we are in a darkmode.
To know when we are in darkmode, we just have to see if the input is checked.
We can do that easily in CSS using the relatively new selector **:has()**.
Here we watch if the **:root** element **has** the **input** (#demo-darkmode) in a **checked** state. If it's the case, we reassign the CSS variable with new values.
```css
/* Default variables values */
:root {
--c-text: #493c37;
--c-background: #eddfe0;
--c-primary: #b392f0;
}
/* Variables values when the input is checked */
:root:has(#demo-darkmode:checked) {
--c-text: #fff;
--c-background: #333;
--c-primary: #98fb98;
}
```
With just that addition, clicking the darkmode checkbox will switch the declared colors.
You can try it out in the interactive demo below.
Our darkmode is working 🎉
Currently, if we check the darkmode checkbox and reload the page, it will reset to its original state.
To fix this, we can make the darkmode persistant
## Add a localStorage
Adding localStorage to maintain darkmode is pretty straightforward.
We watch the checkbox, and when it changes state, we save it in a localStorage item. We give this item a name (here "demo-darkmode"), and the state of the checkbox (if it’s checked or not).
On the page load, we also modify the checkbox state by applying the localStorage item value.
```js
const checkbox = document.querySelector('#demo-darkmode');
// When the checkbox state change, we store its state in the localStorage
checkbox.addEventListener('change', function (event) {
localStorage.setItem('demo-darkmode', event.currentTarget.checked);
});
// Change the checkbox state depending of the value stored in the localStorage
checkbox.checked = localStorage.getItem('demo-darkmode') === 'true';
```
If you check the checkbox below and reload the page, the interactive demo should stay in darkmode.
In less than 5 lines of JavaScript and 20 lines of CSS, we managed to create a darkmode.
It's functional, easy to implement, easy to maintain, and supported by over 88% of browsers.
Want to see all the code in one place ? Check out [the CodePen](https://codepen.io/theosoti/pen/bGPpvZg) I created.
## What's next?
There are still a lot of ways to improve the darkmode from here:
- Implement **prefers-color-scheme**, which will automatically apply the user's preferred mode based on their system settings.
- Add animations to make the transition smoother
- Add more than just a light and dark mode
- Change other values than just colors
It's up to you to explore and decide how you want to enhance your dark mode!
## Enjoyed this article?
I write about modern CSS, HTML, and simpler ways to build for the web.
Join my newsletter below if you want to learn modern CSS with practical tutorials like this.
Happy coding!
================================================================
SHORT POSTS
================================================================
# Add alternative text to CSS generated images
URL: https://theosoti.com/short/accessible-css-generated-images/
Published: 2026-08-09
Tags: CSS, accessibility, generated content, alternative text, screen readers
> Use the slash syntax of the CSS content property to provide replacement text for generated images while keeping meaningful content in HTML.
## CSS generated images can have replacement text.
Most people use `content` to insert icons or decorative images. But what if that generated image carries information?
The `content` property accepts alternative text after a slash:
```css
.element::before {
content: url('/images/status.jpg') / 'Current status: available';
}
```
The value after the slash is the replacement text associated with the generated content.
This syntax is widely available in browsers, but assistive-technology behavior can still vary. For meaningful content, real HTML such as ` ` remains the safer choice.
Use CSS generated content for decoration or progressive enhancement, not for information that must always be announced.
---
If you liked this tip, you might enjoy my guide "You Don't Need JavaScript", which contains more ways to build modern interfaces with HTML and CSS.
You can find it at https://theosoti.com/you-dont-need-js/.
---
# Build accessible accordions with details and summary
URL: https://theosoti.com/short/accessible-details-accordion/
Published: 2026-08-09
Tags: HTML, CSS, accessibility, details, summary, JavaScript alternatives
> Use the native details and summary elements for keyboard-friendly accordions, then add a smooth CSS Grid animation without replacing their semantics.
## Make smooth accordions without JavaScript.
The useful part starts with the `` and `` elements.
They give you native disclosure semantics and keyboard interaction without recreating the component in JavaScript.
But what about smooth animations?
This is where CSS Grid can help. Instead of relying on a guessed `max-height`, place the accordion content inside a grid row and transition it between `0fr` and `1fr`.
The closed state uses a row height of `0fr`. When the `` element opens, the row changes to `1fr`. A child wrapper with `overflow: hidden` keeps the content clipped during the transition.
Keep `` as the trigger. The animation should improve the native component, not replace its keyboard behavior or accessible name.
Also respect reduced-motion preferences when adding the transition.
---
If you liked this tip, you might enjoy my guide "You Don't Need JavaScript", which contains more ways to build modern interfaces with HTML and CSS.
You can find it at https://theosoti.com/you-dont-need-js/.
---
# Create reusable CSS logic with @function
URL: https://theosoti.com/short/css-custom-functions/
Published: 2026-08-09
Tags: CSS, custom CSS functions, experimental CSS, modern CSS, CSS tip
> Define reusable custom CSS functions for calculations, colors, and fluid values without repeating the same expressions across a stylesheet.
## You can now write custom functions in CSS.
You can keep reusable functions in a `functions.css` or `utils.css` file, much like a `utils.js` file alongside your `reset.css`.
Here is what that can look like:
```css
/* Return the negative of any value */
@function --negate(--value) {
result: calc(-1 * var(--value));
}
/* Return a color with a custom alpha value */
@function --opacity(--color, --opacity) {
result: rgb(from var(--color) r g b / var(--opacity));
}
/* Return a fluid font-size value */
@function --fluid-type(--font-min, --font-max, --type: 'header') {
--scalar: if(
style(--type: 'header'): 4vw;
style(--type: 'copy'): 0.5vw
);
result: clamp(
var(--font-min),
var(--scalar) + var(--font-min),
var(--font-max)
);
}
```
The function name starts with `--`, parameters behave like local custom properties, and `result` defines the returned value.
`@function` is still experimental and has limited browser availability. Treat it as progressive enhancement or use it in a controlled browser environment for now.
---
If you liked this tip, you might enjoy my guide "You Don't Need JavaScript", which contains more ways to build modern interfaces with HTML and CSS.
You can find it at https://theosoti.com/you-dont-need-js/.
---
# Replace six typography declarations with the CSS font shorthand
URL: https://theosoti.com/short/css-font-shorthand/
Published: 2026-08-09
Tags: CSS, typography, CSS shorthand, font, CSS tip
> Combine font style, variant, weight, size, line height, and family in one font declaration, while avoiding the shorthand reset trap.
## CSS can be verbose and repetitive.
There are often several ways to declare the same result. Horizontal margins alone can be written in at least four ways:
```css
/* Longhand */
margin-left: 10px;
margin-right: 10px;
/* Four-value shorthand */
margin: 0 10px 0 10px;
/* Two-value shorthand */
margin: 0 10px;
/* Logical shorthand */
margin-inline: 10px;
```
The `font` shorthand can combine six typography declarations in one line.
```css
.title {
font-style: italic;
font-variant: small-caps;
font-weight: 700;
font-size: 1rem;
line-height: 1.5;
font-family: system-ui, sans-serif;
}
```
Becomes:
```css
.title {
font: italic small-caps 700 1rem/1.5 system-ui, sans-serif;
}
```
`font-size` and `font-family` are required. The other values are optional, but there is one catch: omitted font longhands are reset to their initial values.
Use the shorthand when you intend to define the complete font state, not when you only want to change one typography property.
---
If you liked this tip, you might enjoy my guide "You Don't Need JavaScript", which contains more ways to build modern interfaces with HTML and CSS.
You can find it at https://theosoti.com/you-dont-need-js/.
---
# Animate elements along a path with CSS Motion Path
URL: https://theosoti.com/short/css-motion-path/
Published: 2026-08-09
Tags: CSS, CSS animation, motion path, offset-path, performance
> Use offset-path, offset-distance, and offset-rotate to move an element along a curve or custom SVG path without animating left and top.
## Make elements follow a path without JavaScript or GSAP.
CSS Motion Path lets you animate an element along a curve, loop, or custom shape with three properties.
`offset-path` defines the route. You can use a `path()` value or a basic shape.
`offset-distance` places the element between the start and end of that route.
`offset-rotate: auto` keeps the element aligned with the direction of travel.
```css
.traveller {
offset-path: path('M 20 100 C 80 0 220 0 280 100');
offset-rotate: auto;
animation: follow-path 3s linear infinite;
}
@keyframes follow-path {
to {
offset-distance: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
.traveller {
animation: none;
}
}
```
An inline SVG can display the same path as a visual guide, while CSS controls the moving element.
Motion paths work well for loaders, diagrams, timelines, and small interface details. They also avoid animating layout properties such as `left` and `top`.
---
If you liked this tip, you might enjoy my guide "You Don't Need JavaScript", which contains more ways to build modern interfaces with HTML and CSS.
You can find it at https://theosoti.com/you-dont-need-js/.
---
# Build dark mode with CSS only
URL: https://theosoti.com/short/css-only-dark-mode/
Published: 2026-08-09
Tags: CSS, dark mode, prefers-color-scheme, light-dark, JavaScript alternatives
> Create a CSS-only dark mode with a checkbox and :has(), follow the system preference, or let light-dark() select theme-aware colors.
## You can build a dark mode toggle with only CSS.
Combine an ` `, CSS variables, and `:has()` to change a page theme without a JavaScript listener.
```css
:root {
--background: white;
--text: #161616;
}
:root:has(#dark-mode:checked) {
color-scheme: dark;
--background: #161616;
--text: white;
}
body {
background: var(--background);
color: var(--text);
}
```
The checkbox acts as the trigger. When it is checked, `:has()` updates the variables and the whole interface follows them.
CSS also gives you two other options.
Use `prefers-color-scheme` to follow the user's system setting:
```css
@media (prefers-color-scheme: dark) {
body {
background: #161616;
color: white;
}
}
```
Or let `light-dark()` choose between two colors:
```css
:root {
color-scheme: light dark;
}
body {
background: light-dark(white, #161616);
color: light-dark(#161616, white);
}
```
The CSS-only checkbox option does not persist the user's choice after navigation or reload. Use JavaScript only if you need to save that preference.
---
If you liked this tip, you might enjoy my guide "You Don't Need JavaScript", which contains more ways to build modern interfaces with HTML and CSS.
You can find it at https://theosoti.com/you-dont-need-js/.
---
# Apply hover styles only when a device can hover
URL: https://theosoti.com/short/hover-media-query/
Published: 2026-08-09
Tags: CSS, media queries, hover, touch interfaces, accessibility
> Use the CSS hover media feature to avoid mouse-only interactions on touchscreens and provide appropriate behavior for each input type.
## Ever hovered on a touchscreen? Me neither.
But your CSS might still assume that every user can hover.
The `hover` media feature lets you apply styles only when the primary input device can hover conveniently:
```css
@media (hover: hover) {
.card:hover {
transform: translateY(-0.25rem);
}
}
```
You can also target primary input devices that cannot hover:
```css
@media (hover: none) {
.tooltip-trigger {
/* Keep the information available on tap or focus */
}
}
```
This is useful for dropdowns, tooltips, hover transitions, and any interaction that would otherwise disappear on touchscreens.
Use `any-hover` when you care whether any connected input can hover, not just the primary one. A touchscreen laptop with a mouse is a common example.
Hover should remain an enhancement. Important actions and information still need to work with touch and keyboard input.
---
If you liked this tip, you might enjoy my guide "You Don't Need JavaScript", which contains more ways to build modern interfaces with HTML and CSS.
You can find it at https://theosoti.com/you-dont-need-js/.
---
# Understand every CSS object-fit value
URL: https://theosoti.com/short/object-fit-values/
Published: 2026-08-09
Tags: CSS, object-fit, responsive images, video, CSS tip
> Control how images and videos fill a fixed media box with the five object-fit values: fill, contain, cover, none, and scale-down.
## Master object-fit in under one minute.
Images and videos do not always scale the way you want inside a fixed-size box. That is what `object-fit` controls.
```css
.media {
inline-size: 20rem;
block-size: 12rem;
object-fit: cover;
}
```
There are five values:
- `fill` stretches the media to fill the box, even when that changes its aspect ratio.
- `contain` shows the whole media without cropping, which may leave empty space.
- `cover` fills the box while preserving the aspect ratio, so some content may be cropped.
- `none` keeps the media at its natural size.
- `scale-down` chooses whichever result is smaller between `none` and `contain`.
The element needs a constrained content box before the differences become visible. Pair `object-fit` with an explicit inline size, block size, or `aspect-ratio`.
I use `object-fit: cover` most often for cards and thumbnails, where filling the frame matters more than showing every edge of the image.
---
If you liked this tip, you might enjoy my guide "You Don't Need JavaScript", which contains more ways to build modern interfaces with HTML and CSS.
You can find it at https://theosoti.com/you-dont-need-js/.
---
# Transition an element to its natural height in CSS
URL: https://theosoti.com/short/smooth-height-transitions/
Published: 2026-08-09
Tags: CSS, CSS transitions, CSS Grid, interpolate-size, calc-size, modern CSS
> Compare the practical CSS techniques for animating dynamic height, including Grid, interpolate-size, and calc-size(), without guessing max-height.
## Smooth height transitions are possible in CSS.
Animating from `height: 0` to `height: auto` has traditionally been awkward because the browser could not interpolate an intrinsic size.
There are several ways to handle it:
- A fixed `max-height`, which is simple but requires a guessed upper limit.
- A transform, which looks smooth but does not move surrounding content.
- CSS Grid with a row transitioning from `0fr` to `1fr`.
- `interpolate-size: allow-keywords`, which allows transitions to intrinsic sizing keywords such as `auto`.
- `calc-size()`, which lets calculations work with intrinsic sizes.
The Grid method is the most practical fallback when surrounding content needs to move. `interpolate-size` and `calc-size()` are cleaner, but they still need progressive enhancement because browser availability is limited.
I cover all five approaches with interactive examples in the full article:
https://theosoti.com/blog/height-transition/
---
If you liked this tip, you might enjoy my guide "You Don't Need JavaScript", which contains more ways to build modern interfaces with HTML and CSS.
You can find it at https://theosoti.com/you-dont-need-js/.
---
# Build an animated perspective grid in pure CSS.
URL: https://theosoti.com/short/moving-grid/
Published: 2026-05-26
Tags: CSS, CSS animation, CSS gradients, CSS tip
> Layer two linear gradients, tilt with a 3D perspective transform, fade with a mask, and animate the background position. No JS, no images, just CSS.
## You can create an animated perspective grid in CSS.
Here’s the idea:
First, draw the grid with 2 linear gradients.
One for the horizontal lines.
One for the vertical ones.
```css
.el {
background-image: linear-gradient(white 2px, transparent 2px), linear-gradient(90deg, white 2px, transparent 2px);
background-size: 50px 50px;
}
```
That gives you a simple square grid.
Then tilt it in 3D:
transform: perspective(900px) rotateX(50deg);
transform-origin: center;
Now it starts to look like a floor.
To make it fade naturally into the distance,
add a mask:
```css
.el {
mask-image: linear-gradient(to bottom, rgb(0 0 0 / 0), rgb(0 0 0 / 0.3));
-webkit-mask-image: linear-gradient(to bottom, rgb(0 0 0 / 0), rgb(0 0 0 / 0.3));
}
```
And finally, animate the background position:
```css
@keyframes moveGrid {
from {
background-position: center 0;
}
to {
background-position: center 100px;
}
}
```
That’s what creates the movement.
The lines themselves do not move as elements.
You just shift the background pattern.
So the full effect is:
- 2 gradients for the grid
- a perspective transform for depth
- a mask for atmosphere
- and a background animation for motion
A nice example of how far simple layers can go.
---
If you liked this tip, you might enjoy my guide "You don't need JavaScript", which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# You can now style browser find-in-page matches in CSS.
URL: https://theosoti.com/short/css-search-text-pseudo-element/
Published: 2026-04-28
Tags: CSS, modern CSS, CSS typography, browser APIs, experimental CSS, progressive enhancement, CSS tip
> Style browser find-in-page matches with CSS ::search-text and :current. Support is limited, so treat it as progressive enhancement.
## You can now style text that matches a browser find-in-page search, in CSS with `::search-text`.
That means when someone uses the browser search bar and looks for a word on your page, you can control how those matches appear.
And it gets even better.
You can use `::search-text` on its own to style all search matches on the page, or scope it to a specific area like a `section`.
You can also combine it with `:current` to style the currently focused match differently from the others.
```css
/* Style every match on the page */
::search-text {
background: oklch(0.9 0.15 90);
color: black;
}
/* Style only the currently focused match */
::search-text:current {
background: oklch(0.7 0.2 30);
color: white;
}
```
This is one of those small features that feels very "platform-first".
The browser already knows what text is being searched.
Now CSS can react to it too.
It opens the door to nicer reading experiences, better documentation pages, and more polished long-form content without building a custom search UI. That part is more of a design implication, but it is exactly the kind of use case this selector points to.
Browser support is still limited though.
[Can I Use](https://caniuse.com/) shows support in Chrome 144+, Edge 144+, and Opera 122+, while Firefox and Safari do not support it for now.
So this is clearly progressive enhancement for now.
But it is another nice example of CSS getting access to more real browser states.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# You can now create true randomness in CSS.
URL: https://theosoti.com/short/css-random-function/
Published: 2026-04-26
Tags: CSS, modern CSS, experimental CSS, CSS tip
> CSS random() lets you generate values inside a range and share random base values across properties. Powerful idea, but support is still limited.
## You can now create true randomness in CSS.
With `random()` function.
Just CSS picking a value inside a range.
That means you can randomise things like size, position, angle, delay, and more.
And the interesting part is this:
It’s not just chaos.
`random()` can also share a random base value.
So multiple properties can stay connected.
For example:
the same random value for width and height,
or a consistent variation across several elements.
That makes it much more useful than a simple visual gimmick.
It opens the door to more generative UI,
more playful layouts,
and less repetitive styling.
But browser support is still very limited.
MDN marks it as experimental and not Baseline.
Right now, support is mainly in Safari 26.2+ and iOS Safari 26.2+.
No Chrome, no Edge, no Firefox for now.
So this is clearly not something to rely on in production yet.
But it’s a very fun glimpse of where CSS is going.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# HTML in Canvas API Explained: Render HTML Inside Canvas
URL: https://theosoti.com/short/html-in-canvas-api/
Published: 2026-04-18
Tags: accessibility, Canvas API, experimental web APIs, HTML, progressive enhancement, web development tip
> Explore the experimental HTML in Canvas API, which renders real HTML inside canvas while preserving DOM interaction and accessibility.
## The web is getting a bit weird again. And I mean that in a good way.
There’s a new experimental API called HTML in Canvas.
The idea is wild:
You can render real HTML inside a `` surface, while keeping the DOM in sync for things like interaction and accessibility.
It is currently a proposal, and Chromium has an implementation behind a flag.
So instead of canvas being just pixels, it can start behaving more like a place where actual web content lives.
The proposal describes three main pieces:
- an opt-in attribute for ``
- a 2D API to draw DOM content
- a WebGL path to use that HTML as a texture.
It could open the door to things like:
HTML-mapped 3D screens,
interactive UI inside graphics-heavy scenes,
and more experimental interfaces without rebuilding everything manually in canvas.
That is exactly the kind of direction shown in the current demos and explainer.
It’s still very early. But definitely something worth watching.
It kind of feels like the web is becoming playful again.
Here's a few examples of what can be possible with this HTML in Canvas API:
- https://x.com/jaffathecake/status/2039632975831191858
- https://x.com/mattrothenberg/status/2040416074710102300
- https://www.linkedin.com/posts/vittorio-retrivi_hear-me-out-wicg-is-experimenting-ugcPost-7447070535492071424-vygx
- https://www.youtube.com/shorts/7LiJIpf8jD0
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Shrink a sticky header only when it sticks. In CSS.
URL: https://theosoti.com/short/shrink-sticky-header-scroll-state/
Published: 2026-03-16
Tags: CSS, modern CSS, scroll-driven animations, CSS animations, frontend development, CSS tip
> Use CSS scroll-state queries to shrink a sticky header only after it becomes stuck. A cleaner way to build adaptive headers without scroll listeners.
## Sticky headers can now shrink at the right moment.
Only when they are actually stuck.
And CSS can handle it on its own.
The setup starts the same way:
the sticky wrapper becomes a scroll-state container.
```css
.header-container {
position: sticky;
top: 0;
container-type: scroll-state;
}
```
Once that is in place, you can style the \"stuck\" state directly.
```css
@container scroll-state(stuck: top) {
.header {
padding-block: 0.75rem;
box-shadow: 0 8px 20px rgb(0 0 0 / 0.1);
}
.header-title {
font-size: 1.5rem;
}
}
```
This is a small detail, but it changes the feel of the interface.
At first, the header can be roomy and expressive.
Once it sticks, it can become tighter and more compact.
That gives more space back to the content while keeping navigation visible.
This pattern is useful on:
- article pages
- docs layouts
- long landing pages
- dashboards with persistent controls
Before, this usually meant listening to scroll,
calculating thresholds,
and toggling classes at the right time.
Now the browser exposes the exact state you actually care about:
is the sticky element stuck or not?
That makes the rule easier to explain too.
The header stays large in its normal flow state,
then becomes compact only after the sticky state is real.
That makes the intent much clearer.
You are not styling \"some scroll position\".
You are styling a real layout state.
It also means transitions become easier to reason about.
The header can animate its padding, font size, shadow, or background as soon as the sticky state changes.
This is also better for maintenance.
The sticky logic stays in CSS next to the visual change,
instead of being split between layout styles and a scroll script.
That is the kind of improvement modern CSS is getting really good at.
Not flashy.
Just useful.
More behavior in the platform.
Less custom scroll glue.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# You can now detect when sticky is actually stuck. In CSS.
URL: https://theosoti.com/short/sticky-stuck-scroll-state/
Published: 2026-03-15
Tags: CSS, modern CSS, scroll-driven animations, CSS animations, performance, JavaScript alternatives, CSS tip
> Use CSS scroll-state queries to style a sticky element only when it is actually stuck to the top. No scroll listener, no class toggle, no JavaScript.
## You can now detect when a sticky element is actually... stuck.
In CSS.
No scroll listener.
No class toggle.
No JavaScript.
Here is the idea:
you turn the sticky wrapper into a scroll-state container.
```css
.header-container {
position: sticky;
top: 0;
container-type: scroll-state;
}
```
Then CSS can ask a very useful question:
\"Is this element currently stuck to the top?\"
```css
@container scroll-state(stuck: top) {
.header {
box-shadow: 0 8px 30px rgb(0 0 0 / 0.12);
}
.title {
font-size: 1.5rem;
}
}
```
So when the header reaches the top and actually becomes sticky,
its style changes automatically.
In this kind of example:
- the header gets a shadow
- the title becomes smaller
- transitions can smooth the whole change
That means the header can feel more alive without any scroll logic in JavaScript.
This solves a very common UI need.
Designers often want one visual state before the header sticks,
and another one after it locks to the top.
Before `scroll-state`, that usually meant watching scroll position,
toggling a class, and keeping logic in sync with layout.
Now the browser can expose that state directly to CSS.
That means the visual change happens for the real sticky moment,
not for a rough threshold you had to maintain in JS.
That is why this feature is so interesting.
It removes the glue code around a behavior the platform already understands.
It is especially useful for documentation layouts, long-form articles, dashboards, and sticky section bars.
Anywhere a header changes role once it reaches the top, this becomes a clean native hook.
Support is still experimental, so treat it as progressive enhancement.
But as a platform feature, it is a very strong step forward.
Less code.
Less coordination between CSS and JS.
More behavior described where the styling already lives.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Your slider can now know the active card. In CSS.
URL: https://theosoti.com/short/scroll-state-active-slider/
Published: 2026-03-14
Tags: CSS, modern CSS, scroll-driven animations, CSS animations, JavaScript alternatives, CSS tip
> Use CSS scroll-state queries to detect the snapped slide in a carousel and style the active card without Intersection Observer, scroll listeners, or JavaScript.
## Your slider can now know which card is active.
In CSS.
No Intersection Observer.
No scroll listener.
No JavaScript state tracking.
Just `scroll-state`.
In this pattern, each slide becomes its own scroll-state container.
```css
.horizontal-track li {
container-type: scroll-state;
}
```
Then CSS can detect when one slide is the snapped one in the inline direction.
```css
@container scroll-state(snapped: inline) {
.card-content img {
filter: sepia(0) brightness(1);
transform: scale(1.1);
}
.card-footer {
background-color: var(--bg-color);
color: var(--color-orange);
}
.num {
opacity: 1;
color: var(--color-orange);
}
}
```
So when a card becomes the snapped item:
- the image gets highlighted
- the footer changes style
- the number becomes visible
All automatically.
That is the interesting part.
The browser already knows which slide is snapped.
Now CSS can react to that state directly.
This makes sliders feel much smarter without adding JavaScript just to figure out which card is active.
You can keep the behavior in the platform and keep your component logic simpler.
It is a very good fit for carousels, product galleries, onboarding steppers, and horizontal story-like interfaces.
Anywhere snap points already exist, the styling can follow naturally.
It also makes the styling more trustworthy.
You are reacting to the exact card the browser considers snapped,
not to an approximation you had to detect yourself.
What makes `scroll-state` exciting is that it reacts to real interaction states:
- stuck
- scrollable
- snapped
That is a big shift.
CSS is not only styling static elements anymore.
It is starting to respond to what the browser already knows about scrolling behavior.
Support is still emerging, so this is best treated as a progressive enhancement for now.
But the direction is very clear.
Less glue code.
Less bookkeeping.
More UI behavior handled directly in CSS.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Reset Form Inputs Natively with button type="reset"
URL: https://theosoti.com/short/reset-form-natively/
Published: 2026-03-07
Tags: HTML forms, performance, HTML, JavaScript alternatives, web development tip
> Use the native reset button to restore inputs, selects, checkboxes, and validation state without JavaScript. A simple HTML feature that is still easy to forget.
## You can reset a form natively.
No helper function.
No state reset logic.
No JavaScript at all.
HTML already gives you a reset button.
```html
```
When the reset button is clicked, the browser restores every field to its initial value.
That includes:
- text inputs
- textareas
- selects
- checkboxes
- radios
It also resets states tied to the original values.
That is why it is useful in demos, filter panels, and small utility forms.
If your inputs start empty, reset brings them back to empty.
If a checkbox starts checked in the HTML, reset restores that checked state.
The browser simply goes back to the form's original snapshot.
Another nice detail:
validation states follow that reset too.
If a field became invalid after typing, resetting the form brings the field back to its starting value and clears that temporary state.
This is where the feature becomes practical.
You do not need custom code to clear multiple fields one by one.
The browser already knows what the initial state was.
There is also a small mental shift here.
Reset does not mean "empty everything".
It means "go back to the original HTML values".
That distinction matters when a form starts with defaults, preselected options, or checked filters.
That said, use it with intent.
A reset button can be frustrating in long forms if users click it by mistake.
It works best when clearing the form is a real, expected action.
Good examples:
- search forms
- filter sidebars
- playground demos
- short admin tools
Less good examples:
- long checkout flows
- multi-step forms
- anything where accidental clearing would be costly
So the feature is not new.
But it is still underrated.
Sometimes the fastest solution is already built into HTML.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Fix z-index leaks with CSS isolation: isolate
URL: https://theosoti.com/short/isolation-isolate-property/
Published: 2026-03-03
Tags: CSS, modern CSS, CSS typography, CSS tip
> Use isolation: isolate to create a new stacking context and keep pseudo-elements, blend effects, and negative z-index layers inside their component.
## `isolation: isolate` can save your layering.
Especially when decorative elements start escaping their card.
This property creates a new stacking context.
```css
.card {
position: relative;
isolation: isolate;
}
```
That sounds technical.
But the effect is simple.
It tells the browser:
\"everything inside this component should stack inside this component.\"
This is very useful when you add:
- pseudo-elements
- blend effects
- negative z-index layers
- decorative backgrounds
Without isolation, a child with `z-index: -1` can slip behind other elements outside the card.
Sometimes it disappears behind the page.
Sometimes it overlaps in places you did not expect.
With `isolation: isolate`, the parent becomes its own stacking world.
Your decorative layer can sit behind the content, while still staying inside the component.
That is why this property pairs so well with `::before` and `::after`.
```css
.card {
position: relative;
isolation: isolate;
}
.card::after {
content: '';
position: absolute;
inset: 0;
z-index: -1;
}
```
This is also useful with `mix-blend-mode`.
Blend effects can interact with everything behind them.
Isolation limits that interaction to the component itself, which makes the result much easier to control.
That is why the property feels so useful in modern UI work.
Cards, badges, highlights, blurred layers, and oversized quote marks often rely on decorative elements behind the content.
`isolation: isolate` gives those effects a safe boundary.
The key idea is this:
`isolation` does not move anything.
It does not replace `position`.
It does not replace `z-index`.
It simply gives the component a boundary for stacking.
So if one of your fancy background shapes keeps leaking outside its card, this is probably the missing piece.
Small property.
Very practical fix.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# React to Scroll with CSS Scroll State Queries
URL: https://theosoti.com/short/scroll-state-query/
Published: 2026-02-11
Tags: CSS, modern CSS, scroll-driven animations, CSS animations, performance, JavaScript alternatives, CSS tip
> Use CSS scroll state queries to style elements based on scroll position. Detect top, bottom, or scrolling states without JavaScript or scroll listeners.
## Let CSS know when you scroll. No JavaScript needed.
Scroll-state queries let you style elements based on how a container is scrolled.
You can detect when it’s at the top, bottom, or in motion, and trigger animations directly from CSS.
Here’s a simple example:
```css
html {
container-type: scroll-state;
container-name: scroller;
}
.to-top {
position: fixed;
bottom: 20px;
right: 20px;
translate: 80px 0;
transition: translate 0.4s ease;
}
@container scroller scroll-state(scrollable: top) {
.to-top {
translate: 0 0;
}
}
```
The key is `container-type: scroll-state`.
It makes the page a scroll container that CSS can observe.
The `@container` rule reacts to its scroll state.
When the container is at the top, bottom, or being scrolled, styles update automatically.
No scroll listeners. No intersection observers.
Just CSS responding to motion.
You can use it to fade headers, slide elements, or trigger transitions as users scroll.
Browser support is about 68.5%, so it’s still experimental, use with caution (or don't use it yet).
This is a glimpse of a more reactive CSS.
Less scripting, more design freedom.
Scroll state queries are practical for sticky headers, section indicators, and context-aware controls. Because the state is declarative in CSS, you can remove a lot of JS scroll bookkeeping and keep behavior easier to maintain.
Scroll state queries are practical for sticky headers and section-aware controls. Since state logic stays in CSS, you can reduce JS scroll bookkeeping and keep behavior easier to audit.
Always include reduced-motion behavior. A strong animation pattern is one that degrades cleanly while preserving meaning.
For teams, `container-type: scroll-state` is easiest to maintain when it starts in one documented example before broader reuse. This keeps implementation predictable and prevents style drift as the codebase grows.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Align Numbers Perfectly with CSS tabular-nums
URL: https://theosoti.com/short/tabular-nums/
Published: 2026-01-26
Tags: CSS, modern CSS, CSS layout, responsive design, CSS typography, frontend development, CSS tip
> Use CSS tabular-nums to align numbers in tables, dashboards, prices, and stats for cleaner, more readable interfaces and layouts.
## Ever noticed how numbers often look misaligned?
That's because most fonts use proportional numbers by default.
Each digit takes up a different amount of space.
But there's a fix for that.
In CSS, you can opt in to tabular numbers, where all digits have the same width.
That makes columns of numbers line up perfectly.
Here's how to enable it:
```css
.after {
font-variant-numeric: tabular-nums;
font-feature-settings: 'tnum';
}
```
This works best with fonts that support OpenType features.
But most system fonts and many web fonts do.
Clearer layouts, cleaner data tables, and a subtle polish your UI users will feel.
Use tabular figures only where alignment matters: prices, analytics, timers, and table columns. For body copy, proportional digits usually look better. A small utility class like `.tnum { font-variant-numeric: tabular-nums; }` keeps the pattern reusable.
Also check decimal alignment and negative values like `-12.40`, so spacing remains stable when data updates in real time.
Enable `tabular-nums` where alignment matters most, like prices, stats, and timers. Keep proportional figures in body text, and reserve fixed-width digits for data views where quick comparison is important.
Enable `tabular-nums` where numeric comparison matters, like prices, stats, and timers. Keep proportional figures in body copy, then switch to fixed-width digits in data views for faster visual scanning.
Test with multilingual strings and varied word lengths. Typographic CSS should remain graceful when content is unpredictable.
For teams, `-12.40` is easiest to maintain when it starts in one documented example before broader reuse. That rollout sequence preserves clarity and reduces regressions during future refactors.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Create a Mirror Reflection Effect with CSS box-reflect
URL: https://theosoti.com/short/reflexion-effect/
Published: 2026-01-20
Tags: CSS, modern CSS, progressive enhancement, CSS tip
> Create a mirror reflection effect in CSS with -webkit-box-reflect and add glossy image reflections with a single property and graceful fallback.
## You can create a mirror reflection effect in CSS.
With one property.
No pseudo-elements.
No duplicated images.
No gradients hacks.
```css
img {
-webkit-box-reflect: below -5px linear-gradient(transparent, rgba(0, 0, 0, 0.5));
}
```
Here’s what’s happening:
`-webkit-box-reflect` does 3 things:
- below: puts the reflection under the element (you can also use above, left, right)
- -5px: the gap between the image and its reflection
- linear-gradient(…): fades the reflection out nicely
It looks great for:
- product shots
- hero images
- galleries
- “glass” / glossy UI vibes
⚠️ Small catch: it’s a WebKit-only feature.
Works in Safari (and usually Chromium), but not Firefox.
Still, it’s a fun trick when you can use progressive enhancement.
`box-reflect` works best as a subtle accent instead of a full mirror. Keep the reflection short and softly faded, so the visual effect supports hierarchy without competing with the main content.
Here’s what’s happening:
` is most useful when you apply it with a narrow scope and a clear component-level objective. For this article, the strongest use case is straightforward: the technique is small, but the maintenance benefit grows when the team applies it with clear intent. Before shipping, keep this checklist in mind: keep fallback behavior explicit so unsupported environments remain stable. As always, test with real content and realistic viewport ranges; that is the fastest way to confirm the pattern holds up outside sandbox demos.
Here’s what’s happening:
` is intentional usage: one focused rule can often replace several brittle overrides. In this exact topic, the implementation payoff is clear: this is a good example of modern css reducing complexity while keeping behavior declarative. One thing worth validating early: document a short usage rule to keep future edits predictable. If you document this as a team convention, future refactors become easier because everyone understands when the technique should, and should not, be used.
`box-reflect` works best as a subtle visual accent on hero media or decorative cards. Keep reflections short and faded, so the effect supports hierarchy without stealing attention from primary content.
Validate it with real content and realistic viewport ranges so implementation details hold up outside demos.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# CSS corner-shape: Shape Corners Beyond border-radius
URL: https://theosoti.com/short/corner-shape/
Published: 2026-01-19
Tags: CSS, modern CSS, CSS shapes, frontend development, progressive enhancement, CSS tip
> Learn the CSS corner-shape property to create scoop, bevel, squircle, and custom corners beyond border-radius with progressive enhancement.
## You can now shape corners in CSS. Not just round them.
Meet corner-shape.
border-radius only changes how much rounding you get.
corner-shape changes the geometry of the corner itself.
Same element.
Same radius.
Different corner style.
Square.
Rounded.
Scoop.
Bevel.
And more.
This matters because corners carry tone.
They can feel softer.
Or sharper.
More designed.
More intentional.
Great for buttons, cards, and UI chrome.
Browser support is the catch.
Around 68.65% today.
No Safari.
No Firefox.
So it’s not for critical UI yet.
But it works well as progressive enhancement.
Unsupported browsers simply fall back to normal corners.
No breakage.
No hacks.
CSS is slowly giving us more control over geometry.
Less SVG.
Fewer pseudo-elements.
More design system power.
If you want predictable results, pair `corner-shape` with a regular `border-radius` fallback and gate advanced shapes with `@supports (corner-shape: bevel)`. That way, older browsers keep the rounded version while capable browsers get the sculpted corners.
Try it on small components first: buttons, tags, and cards. Big radii with aggressive shapes can reduce readable surface area, so test with long labels and tighter mobile widths.
Use `corner-shape` as a brand accent, not as a global default. Keep `border-radius` as your base token, then apply custom corner geometry to decorative surfaces like cards and badges where browser fallback remains graceful.
Use `corner-shape` as a brand accent rather than a default everywhere. Keep `border-radius` as baseline, then apply custom geometry on selected components where fallback to rounded corners still looks intentional.
Media-related CSS needs testing across different asset ratios and resolutions. Real image variance is where composition rules are truly validated.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Position Elements with CSS sibling-index()
URL: https://theosoti.com/short/sibling-index/
Published: 2026-01-13
Tags: CSS, modern CSS, CSS layout, responsive design, HTML forms, CSS selectors, performance
> Learn how CSS sibling-index() lets elements position themselves based on sibling order. Use pure CSS math for layouts without JavaScript or nth-child selectors.
## Place elements relative to their siblings.
With one CSS function.
Meet sibling-index().
It gives each element a number.
Based on its position among siblings.
That number can drive layout.
No extra classes.
No :nth-child() chains.
No JavaScript.
Here’s the trick I used to place dots in a circle.
Each dot computes its own angle.
```css
.dot {
--angle: calc(sibling-index() * 9deg);
--radius: 240px;
position: absolute;
width: 25px;
height: 25px;
transform: translate(-50%, -50%);
top: calc(50% + sin(var(--angle)) * var(--radius));
left: calc(50% + cos(var(--angle)) * var(--radius));
}
```
Here’s what happens.
sibling-index() returns the position of the dot.
You multiply it by 9deg.
That spreads 40 dots across a full circle.
Then sin() and cos() convert the angle into coordinates.
top uses sin().
left uses cos().
The circle is centered with 50%.
The dot is centered with translate(-50%, -50%).
Simple math.
Pure CSS.
Very little markup.
Browser support is still limited.
About 69.3% today.
And there is no Firefox support yet.
But the idea is powerful.
CSS is getting real logic now.
What would you build with sibling-index()?
`sibling-index()` is excellent for staggered offsets and sequence-based styling without extra classes. Combined with `calc()`, it keeps patterns adaptive when list order changes or items are inserted dynamically.
`sibling-index()` is great for sequence-aware styling without adding helper classes. It becomes especially useful for staggered layouts where item order can change and manual nth rules become fragile.
A quick edge-case audit with inserted elements helps confirm that selector logic stays predictable over time.
To roll this out safely, start by applying `sibling-index()` in a single UI surface where the benefit is obvious. After validation, expand gradually to matching components and avoid ad-hoc one-off overrides.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# When Relative Units Behave Differently: EM vs REM in CSS
URL: https://theosoti.com/short/em-or-rem/
Published: 2026-01-10
Tags: CSS, modern CSS, CSS layout, responsive design, CSS typography, CSS tip
> Learn the difference between CSS em and rem units, when to use each one, and how they affect scalable typography, spacing, and layouts.
## EM and REM look the same. Until they don’t.
Both are relative units.
But they reference different things.
rem is always based on the root font size.
Usually the html element.
em is based on the computed font size of the element.
Or its parent, depending on what you are sizing.
Here’s the part that confuses people.
em can compound.
If you set font-size in em, the element’s font size changes.
Then every em value inside that element scales from this new size.
Padding. Margin. Gaps. Everything.
rem does not compound.
It always goes back to the root.
A quick mental model.
Use rem when you want consistency across the page.
Typography scale. Spacing scale. Layout rhythm.
Use em when you want a component to scale with its own text.
Buttons. Chips. Badges. Cards with internal spacing tied to font size.
Rule of thumb.
rem for global rules.
em for local rules.
If you ever got a button that grows too much inside a container,
it was probably em compounding.
Do you default to rem everywhere?
A reliable rule is simple: use `rem` when values should follow the root font size, and use `em` when values should scale with the component itself. That split keeps accessibility strong while nested components preserve their own rhythm.
A practical rule is simple: use `rem` for global rhythm and accessibility scaling, and use `em` when sizing should follow the local component context. This prevents nesting surprises in reusable UI blocks.
Keep fallback behavior explicit and straightforward. Stable defaults make advanced enhancements much safer to maintain.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Scroll-Driven Animations in CSS with @starting-style
URL: https://theosoti.com/short/scroll-animation/
Published: 2025-12-20
Tags: CSS, modern CSS, scroll-driven animations, CSS animations, CSS tip
> Learn how to combine entry animations and scroll-driven motion using modern CSS. Use @starting-style, custom properties, and scroll timelines.
## You can combine scroll-driven animations with `@starting-style` in CSS.
The goal is simple.
Fade elements in on page load.
Then animate them based on scroll.
At first glance, it sounds easy.
In practice, scroll timelines behave differently from classic animations.
So you need a small setup.
### Register a custom property
Instead of animating opacity directly,
animate a numeric value you control.
```css
@property --progress {
syntax: '';
inherits: false;
initial-value: 0;
}
```
This gives you a stable value to work with.
### Define the entry state
Use `@starting-style` to handle the initial fade-in.
```css
@starting-style {
opacity: 0;
}
```
Now the browser knows the element’s starting point,
even if it appears dynamically.
### Drive the animation with scroll
Animate the custom property using a scroll timeline.
```css
animation: progress linear both;
animation-timeline: view();
```
Then map it back to real styles.
```css
opacity: var(--progress);
```
That’s the pattern.
Smooth entry animation.
Scroll-linked motion.
No hacks.
No JavaScript.
Just modern CSS doing modern UI work.
This kind of setup shows how far CSS has evolved.
And how much interaction it can handle on its own.
Have you started experimenting with scroll-driven animations yet?!
Scroll-driven animations should reinforce context, not distract from content. Keep progress-linked motion restrained and provide comfortable fallbacks for reduced-motion users.
Run interaction tests with rapid clicks and navigation changes. Input should stay responsive even while transitions are active.
A practical way to adopt `@starting-style` is to scope it to one high-impact component first. This keeps implementation predictable and prevents style drift as the codebase grows.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Build a Masonry Layout with Pure CSS column-width
URL: https://theosoti.com/short/masonry-layout/
Published: 2025-10-04
Tags: CSS, modern CSS, CSS Grid, CSS layout, responsive design, performance, JavaScript alternatives
> Create responsive Masonry layouts using the CSS column-width property. Fit flexible columns automatically, no JavaScript or grid hacks—just clean, native CSS.
## You only need a few CSS lines to build a Masonry layout.
Here’s how it works:
The magic happens with the column-width property.
```css
.columns {
width: 100%;
column-width: 200px;
gap: 1em;
}
```
This tells the browser: “Fit as many 200px-wide columns as you can into this container.”
If the screen is wider, more columns fit. On smaller screens, fewer columns show.
The gap sets the horizontal spacing between them.
The rest is just good styling:
- Images set to width: 100% so they fill the column
- object-fit: cover to keep them visually balanced
- A bit of padding, rounded corners, and borders for aesthetics
No position hacks. No flex/grid juggling.
Just native CSS doing the heavy lifting.
And it adapts instantly when you resize the container.
Link to the codepen: https://codepen.io/theosoti/pen/ogXedrz
Column-based masonry layouts are fast to implement for feeds and galleries. Always test reading flow and item ordering, because visual columns can differ from the underlying DOM sequence users navigate.
Column-based masonry is quick for feeds, but always verify reading order and keyboard navigation. Visual placement should not conflict with logical DOM sequence.
Before rollout, verify long content, dense cards, and extreme widths. Stable behavior under stress is what turns a neat trick into production CSS.
When introducing `column-width`, begin with one reference component and treat it as the canonical pattern. Then reuse that same pattern in similar contexts so behavior stays consistent and review time stays low.
If reading order matters, double-check keyboard and screen-reader flow so visual columns do not confuse content sequence.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Responsive Components with CSS Container Queries
URL: https://theosoti.com/short/emoji-toggle-cq/
Published: 2025-09-20
Tags: CSS, modern CSS, container queries, responsive design, CSS layout, CSS tip
> Use CSS container queries to make components respond to their parent’s size, not the viewport. Create adaptive layouts and icons with clean, modern CSS.
## Make components responsive to their parent. Not the viewport.
In the demo, the icon changes based on the content's height.
All with CSS container queries.
Step 1 - declare a container.
Set `container-type: size;` on the wrapper (optionally `container-name: box;`).
Step 2 - define a default state.
Use `.content:after { content: "default"; }` or your base icon.
Step 3 - react to the container, not the page.
Write `@container (height > 60px) { .content:after { content: "state-1"; } }`.
Add another for a larger breakpoint, e.g. `(height > 240px)`.
As the content grows or shrinks, the pseudo-element swaps automatically.
No JavaScript. No viewport media queries.
Works inside grids, flex layouts, and nested components.
Support is solid in modern browsers with almost 93%.
Ship a sensible default so it still looks fine where `@container` isn’t supported.
Container-query-driven components are ideal for small interactive widgets that appear in many parent layouts. Defining behavior from local width keeps them responsive without page-level media-query coupling.
Container-query-driven widgets stay reusable because they respond to local width, not page assumptions. This is ideal for components embedded in many parent contexts.
Before rollout, verify long content, dense cards, and extreme widths. Stable behavior under stress is what turns a neat trick into production CSS.
For teams, `container-type: size;` is easiest to maintain when it starts in one documented example before broader reuse. That rollout sequence preserves clarity and reduces regressions during future refactors.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Style Scrollbars with CSS scrollbar-color & scrollbar-width
URL: https://theosoti.com/short/scrollbar-customisation/
Published: 2025-09-17
Tags: CSS, modern CSS, scroll-driven animations, CSS animations, CSS colors, accessibility, frontend development
> Customize scrollbars to match your UI using scrollbar-color and scrollbar-width. Control colors, size, and accessibility. All with just two CSS properties.
## Make your scrollbars match your UI. Do it with only two CSS properties.
`scrollbar-color `sets the colors.
First value is the thumb. Second is the track.
Example: `scrollbar-color: hashtag#8aaa8a hashtag#294b29;`
`scrollbar-width` controls size.
Use auto, thin, or none to hide it.
Example: `scrollbar-width: thin;`
Apply these on any scrollable container.
Or put them on `:root to style the page scrollbars.
It respects user settings like overlay scrollbars.
So on macOS they appear when scrolling and keep your colors.
Accessibility tip.
Pick a thumb color with strong contrast against the track.
For the browser support, it's not great for now.
~73% for the `scrollbar-color` property.
~83% for the `scrollbar-width` property.
But they graciously fallback to default style if not supported.
Custom scrollbars can improve visual consistency, but usability must stay first. Keep enough contrast and width for discoverability, and avoid styling choices that hide scroll affordance.
Custom scrollbars can support visual consistency, but discoverability and contrast come first. Keep width and color choices readable across themes.
Run interaction tests with rapid clicks and navigation changes. Input should stay responsive even while transitions are active.
For teams, `scrollbar-color` is easiest to maintain when it starts in one documented example before broader reuse. That rollout sequence preserves clarity and reduces regressions during future refactors.
Aim for subtle customization that keeps scrollbar track and thumb clearly distinguishable at a glance.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# CSS image-rendering: pixelated. Scale Pixel Art Without Blur
URL: https://theosoti.com/short/crispy-images/
Published: 2025-09-15
Tags: CSS, modern CSS, CSS tip
> Preserve sharp edges when scaling pixel art, icons, retro screenshots, or QR codes using one line of CSS supported by modern browsers.
## Scale pixel art without blur. Keep it crisp with one line.
By default, browsers smooth images when you scale them, which makes sprites and icons look muddy.
image-rendering: pixelated; forces nearest-neighbor scaling so every pixel stays sharp.
Use it when you upscale low-res assets: pixel art, icons, retro screenshots, QR codes.
Skip it for photos or illustrations that should stay smooth.
Apply it to the element you’re resizing (img, canvas).
It takes effect when the rendered size differs from the image’s intrinsic size.
If you’re using background images, test in your target browsers since support can vary.
No extra assets. Smaller downloads. Clean, crunchy results.
Modern browser support is broad with ~95%.
Don't hesitate to share projects where you used this property!
`image-rendering: pixelated` is perfect for pixel art and retro assets that need hard edges when scaled. Apply it selectively, because photographs and gradients usually look degraded with this mode.
`image-rendering: pixelated` preserves hard edges for pixel art and retro sprites. Use it selectively, since photos and gradients usually degrade with this mode.
Keep fallback rendering in mind so unsupported effects still produce clear, readable visuals.
When introducing `image-rendering: pixelated`, begin with one reference component and treat it as the canonical pattern. That rollout sequence preserves clarity and reduces regressions during future refactors.
Reserve the pixelated mode for true pixel art assets, and keep other imagery on normal rendering to avoid accidental degradation.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Create Multi-Column Layouts with CSS Columns
URL: https://theosoti.com/short/simple-columns/
Published: 2025-08-30
Tags: CSS, modern CSS, CSS layout, responsive design, frontend development, CSS tip
> Build column layouts with the CSS columns property. Add spacing, separators, and spanning elements in just two lines of code.
## Did you know you can create a column layout without grid or flex?
It only takes 2 lines of CSS and can be useful!
One key advantage is the ability to easily add separators between columns.
So how to implement it?
Apply the `columns` property to your container. This property accepts up to two values:
- The minimum width of a column (not mandatory)
- The number of desired columns (mandatory)
And just with that it works!
But you can go further by:
- Using the `gap` property to create spacing between columns.
- Adding separator with the “column-rule” property (it works just like `border`).
- Make an element span across all columns with the `column-span` property.
Don’t hesitate to use it in your next project!
CSS columns are effective for flowing long-form text with minimal setup. Tune column width and gaps carefully, because readability depends on line length as much as layout density.
CSS columns are quick for flowing long-form text and lightweight galleries. Tune width and gaps carefully, because readability depends strongly on line length.
Use this pattern to keep responsive behavior local to the component instead of scattering viewport overrides. That makes scaling and refactoring much safer.
To roll this out safely, start by applying `column-span` in a single UI surface where the benefit is obvious. Then reuse that same pattern in similar contexts so behavior stays consistent and review time stays low.
Before shipping, test `column-span` with both short and long content, then verify behavior in narrow and wide containers.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# CSS backdrop-filter: Frosted Glass Effect Made Easy
URL: https://theosoti.com/short/glassomorphism-backdrop-filter/
Published: 2025-08-29
Tags: CSS, modern CSS, CSS effects, accessibility, frontend development, CSS tip
> Enhance your UI with the CSS backdrop-filter property. Apply blur, brightness, or contrast for sleek frosted-glass effects.
## Want to give your UI a sleek, modern touch?
Try the CSS backdrop-filter property.
It lets you apply visual effects behind an element, like blur, brightness, or contrast.
It's what creates that soft, frosted-glass look you see in modern UIs.
Here’s how simple it is:
```css
.card {
backdrop-filter: blur(5px);
}
```
That’s it.
One line of CSS, and your element blends beautifully into whatever is behind it.
You can combine multiple effects: blur(5px) brightness(1.2).
Bonus: It’s widely supported in modern browsers, with more than 96%
Have you tried backdrop-filter yet?
Share your favorite use case or a creative twist below.
For best results, pair `backdrop-filter` with a semi-transparent background (for example `rgb(255 255 255 / 0.2)`). The filter needs translucency to create that frosted effect visibly.
Add a graceful fallback for unsupported environments: keep a solid or lightly transparent background so contrast stays readable even without blur. Decorative effects should never reduce legibility.
Backdrop blur should support legibility, not reduce it. Combine `backdrop-filter` with translucent fills and subtle borders, then verify text contrast on busy backgrounds before shipping.
Backdrop blur should support clarity, not reduce it. Combine it with translucent fills and borders, then verify text contrast on busy backgrounds.
Test with multilingual strings and varied word lengths. Typographic CSS should remain graceful when content is unpredictable.
A practical way to adopt `backdrop-filter` is to scope it to one high-impact component first. This keeps implementation predictable and prevents style drift as the codebase grows.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Crisp Background Images with CSS image-set()
URL: https://theosoti.com/short/sharp-background-images/
Published: 2025-08-28
Tags: CSS, modern CSS, responsive design, CSS layout, CSS tip
> Serve sharp, responsive backgrounds with CSS image-set(). Let browsers choose the right asset for retina or standard screens efficiently.
## Serve sharp backgrounds without extra code.
Let the browser pick the right asset.
`image-set()` is like `srcset`, but for CSS.
You give multiple sources. The browser chooses the best one for the screen.
Why use it:
- Retina screens get crisp images.
- Regular screens download smaller files.
- No JavaScript. No hacks.
It also works in other image-accepting properties like content, list-style-image, and border-image(but with less browser support).
Using `image-set()` in a background-image property has almost 96% browser support!
Practical tips:
- Add a plain fallback first for older browsers:
`background-image: url("evee.png");` then the `image-set()`.
- Keep file names consistent (@1x, @2x) to simplify builds.
- Pair with background-size: cover/contain as needed.
- Test AVIF/WEBP plus JPEG/PNG as a safe fallback.
`image-set()` helps serve crisp backgrounds on high-density screens without always downloading the heaviest asset. Add sensible fallbacks so lower-density devices still render quickly and clearly.
`image-set()` helps deliver sharper backgrounds on high-density displays without always forcing large assets. Keep sensible fallbacks to protect load performance.
Keep fallback rendering in mind so unsupported effects still produce clear, readable visuals.
A practical way to adopt `image-set()` is to scope it to one high-impact component first. After validation, expand gradually to matching components and avoid ad-hoc one-off overrides.
Pair `image-set()` with explicit `background-size` rules so high-density assets remain sharp without unexpected cropping.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Pure CSS Typing Animation Effect without JavaScript
URL: https://theosoti.com/short/typewrite-effect/
Published: 2025-08-27
Tags: CSS, modern CSS, CSS animations, CSS typography, performance, JavaScript alternatives, frontend development
> Create a smooth typing text animation using pure CSS with keyframes, steps(), overflow, and a blinking caret. No JavaScript required.
## Make your text type itself. With pure CSS.
How it works
- overflow: hidden hides the unrevealed text.
- white-space: nowrap keeps everything on one line.
- Animate width from 0 to 100% with @keyframes typing.
- Use steps(n) to reveal characters one by one. Set n close to the character count for a crisp tick.
- A 1px border-right acts as the caret. A second animation toggles its color to “blink”.
- Add animation-fill-mode: forwards so the text stays visible at the end.
Practical tips
- Prefer a monospace font for perfectly even steps, or tune the steps() value if letters have different widths.
- If the string changes, update the steps() count.
- Respect users with @media (prefers-reduced-motion: reduce) to disable or simplify the effect.
The best part? It's supported by almost 96% of browsers!
Would you use this in a hero, headline, or code sample?
A CSS typing effect works best for short phrases and compact hero copy. Keep timing consistent and provide immediate readable text when motion is reduced or unsupported.
Typing effects are strongest on short, high-impact lines. Keep timing tight and provide immediate readable fallback when motion preferences disable animation.
Typography refinements are most visible with real content length and mixed wording. Validate rhythm, spacing, and line breaks on mobile where small issues become obvious.
When introducing `typing animation`, begin with one reference component and treat it as the canonical pattern. This keeps implementation predictable and prevents style drift as the codebase grows.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# CSS Moving Background Text Effect without JavaScript
URL: https://theosoti.com/short/text-background-animation/
Published: 2025-08-19
Tags: CSS, modern CSS, CSS animations, CSS typography, performance, JavaScript alternatives, CSS tip
> Learn how to animate text with a moving background using only CSS. A lightweight effect with background-clip, keyframes, and zero JavaScript.
## Text with a moving background?
No JavaScript needed.
This effect is pure CSS, and it's clean, fast, and surprisingly easy to pull off.
We use:
- background-image 👉 display the image
- background-clip: text 👉 attach the image to the text
- color: transparent 👉 make the text invisible (to see the image behind it)
- A simple `@keyframes` animation
That’s it.
Just a few lines of code and your typography comes alive.
- No libraries.
- No DOM hacks.
- No bloat.
Perfect for hero sections, portfolios, landing pages, or even just showing off what CSS can do.
It’s part of a growing mindset:
Use less JS. Push CSS further.
There’s a lot you can build without JavaScript, and it’s more fun than you think.
Moving background text effects should prioritize readability over spectacle. Keep motion slow enough to avoid visual fatigue, and ensure the non-animated state still communicates the message clearly.
Moving text backgrounds should never compromise legibility. Keep motion slow and contrast strong, and ensure a clear static state when animation is reduced.
Use this as a readability tool, not only a visual effect. The best result is when style improves scanning speed without adding cognitive load.
A practical way to adopt `@keyframes` is to scope it to one high-impact component first. After validation, expand gradually to matching components and avoid ad-hoc one-off overrides.
Before final rollout, validate `@keyframes` against real content and real interaction states, not only demo text.
Always verify readability when animation is paused, because the static state is what many users will spend most time seeing.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# CSS clamp(): Responsive Design Without Media Queries
URL: https://theosoti.com/short/fluid-css-clamp/
Published: 2025-08-18
Tags: CSS, modern CSS, CSS layout, responsive design, frontend development, CSS tip
> Learn how CSS clamp() creates fluid, responsive layouts by defining min, preferred, and max values—reducing media queries with 95% browser support.
## Media queries aren't the only solution for responsive design.
With clamp(), you can reduce their use significantly!
But wait, what is clamp()?
Clamp is a native css function that allow you to specify:
- a minimum value
- a preferred value (usually with % or viewport units)
- a maximum value
Its syntax looks like this: clamp(5em, 30%, 10em)
In short, it makes elements scale smoothly without relying on media queries.
Clamp is often used for padding, margins, width and font-size.
But it can handle anything with numerical values, such as:
aspect-ratio, gap, positioning, …
With ~95.2% browser support, it’s safe to use.
A useful workflow is to start from two breakpoints you already trust, then derive the middle value so interpolation feels natural between them. This keeps fluid scaling intentional, not random.
Clamp also pairs well with design tokens: expose min, ideal, and max values as variables so typography and spacing stay consistent across components while still adapting to viewport changes.
`clamp()` is most reliable when min and max values come from clear design targets. Tune the middle value carefully, so scaling feels smooth across viewports instead of overreacting on very wide screens.
`clamp()` performs best when min and max values are grounded in real design limits. Tuning the middle value carefully prevents exaggerated scaling on extreme viewport widths.
Use this pattern to keep responsive behavior local to the component instead of scattering viewport overrides. That makes scaling and refactoring much safer.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# CSS @property: Animate and Validate Custom Variables
URL: https://theosoti.com/short/new-css-variables/
Published: 2025-08-17
Tags: CSS, modern CSS, CSS animations, HTML forms, CSS tip
> Learn how CSS @property upgrades custom variables with validation, debugging, and animation support. A must-have feature for cleaner, more powerful CSS.
## Unlock powerful animations with `@property` !
Defining all your CSS variables in :root?
It works, but it has some limitations:
- You can't tell if a variable is valid or not.
- Impossible to animate certain values.
- Debugging is messy.
- Browsers give you zero feedback.
That's where `@property` comes in.
It doesn’t replace CSS variables, it upgrades them.
With `@property`, you get automatic validation, better debugging, and the ability to animate certain types of variables that were never animatable before.
If a value is invalid, it falls back cleanly instead of silently breaking your design.
Plus, you get warnings when something’s wrong, no more guessing.
If you want more powerful variables in your CSS, it’s time to make `@property` part of your toolbox.
`@property` makes custom properties typed, animatable, and easier to validate. Defining syntax and initial values early prevents silent errors and enables smoother state transitions.
`@property` gives custom properties explicit syntax, inheritance rules, and animatability. That makes advanced variable-driven UIs safer and easier to debug.
Validate it with real content and realistic viewport ranges so implementation details hold up outside demos.
When introducing `@property`, begin with one reference component and treat it as the canonical pattern. That rollout sequence preserves clarity and reduces regressions during future refactors.
Before final rollout, validate `@property` against real content and real interaction states, not only demo text.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Native CSS Nesting vs Preprocessors, no more excuses
URL: https://theosoti.com/short/native-css-nesting/
Published: 2025-08-16
Tags: CSS, modern CSS, CSS tip
> CSS nesting is now natively supported with 91% browser coverage. Learn why you may not need Sass preprocessors anymore and how to switch smoothly.
## Stop using CSS preprocessor!
CSS nesting is now natively supported.
If you were using a preprocessor only for this feature,
It might be time to go back to plain CSS.
(Otherwise you can stick with it).
1/ No build tools required
- It makes the development setup easier.
- It also reduces the project’s build time.
2/ Reduced file size
- Sass expands rules during compilation, increasing the final CSS size.
- CSS nesting doesn't expand rules, resulting in smaller CSS.
Browser support is already strong at 91.3%.
But keep in mind that unsupported browsers will fail to parse nested rules.
If you need full compatibility, consider using a build step until support is universal.
If nesting was the only thing keeping you tied to a preprocessor… maybe it’s time to go vanilla.
Native nesting improves readability when selector depth remains shallow. Keep nesting focused on component structure, so maintainability stays high and specificity does not creep upward.
Native nesting improves readability when kept shallow and component-scoped. Avoid deep chains, so specificity and maintainability stay under control.
Validate it with real content and realistic viewport ranges so implementation details hold up outside demos.
When introducing `CSS nesting`, begin with one reference component and treat it as the canonical pattern. That rollout sequence preserves clarity and reduces regressions during future refactors.
As a final check, run `CSS nesting` through edge cases like dense layouts, long labels, and constrained mobile widths.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Dynamic Layouts with CSS :has() without JavaScript
URL: https://theosoti.com/short/switching-layout-in-CSS/
Published: 2025-08-15
Tags: CSS, modern CSS, CSS layout, responsive design, CSS selectors, performance, JavaScript alternatives
> Learn how to build interactive layouts using CSS :has() and :checked. Create dynamic grids, galleries, and dashboards without JavaScript.
## Stop using Javascript to change your layout dynamically.
You can do it entirely with CSS.
Here’s how it works:
- 1. Use input elements (checkbox or radio) to mimic click actions.
- 2. The :has() pseudo-class lets a parent element respond to a child’s state.
--> Example: `.container:has(input) .child {}`
- 3. Use :checked to detect if an input is selected.
--> Example: `input[type="radio"]:checked {}`
- 4. Combine :has() and :checked to target specific states and apply styles dynamically.
--> Example: `.container:has(input:checked) .child {}`
In the example below, we just change the grid-template-columns values when a specific checkbox has been checked.
You can use this for multiple things like e-commerce product, portfolio galleries, dashboards, blog layout, ...
Modern CSS makes it easier than ever to create interactive layouts.
Using `:has()` to switch layouts can remove a lot of small JS toggles. Keep the condition explicit and local, so layout changes happen only where users expect them.
Using `:has()` for layout switching can remove small JS toggles while keeping logic declarative. Keep conditions explicit so layout changes remain predictable.
This pattern has the best payoff when documented with one clear usage rule. Consistent adoption turns small CSS features into reliable system behavior.
When introducing `.container:has(input) .child {}`, begin with one reference component and treat it as the canonical pattern. This keeps implementation predictable and prevents style drift as the codebase grows.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# CSS @scope: Encapsulated Styles Without CSS-in-JS
URL: https://theosoti.com/short/scope-property/
Published: 2025-08-14
Tags: CSS, modern CSS, HTML, JavaScript alternatives, CSS tip
> Discover how CSS @scope lets you limit style leakage, write cleaner HTML, and gain true encapsulation—no BEM, utility classes, or CSS-in-JS needed.
## Ever felt like your CSS is leaking all over the place?
You’re not alone, it’s a familiar pain.
But now, we have `@scope`.
It lets you define where your styles begin, and where they end.
Think of it like local styles, without the need for CSS-in-JS, BEM, or tons of utility classes.
```css
@scope (.container) to (.post) {
p {
color: blue;
}
}
```
That `p` won’t be styled if it’s inside `.post`. Simple and clean.
Here’s why this matters:
- Styles don’t leak outside components
- Fewer class names, cleaner HTML
- Control over specificity and proximity
- Donut scopes (yes, really)
Even better, you can nest scopes.
You can keep styles modular without naming gymnastics.
This is real encapsulation, built into CSS itself.
If you’ve ever wished CSS had better boundaries, this is it.
The only downside is its browser support with only 86.56%.
`@scope` helps contain styles close to their component boundary without extra wrapper classes. It is especially useful in content-rich pages where broad selectors might leak into unrelated sections.
`@scope` helps isolate styles without additional wrappers. It is useful in content-heavy pages where broad selectors can accidentally leak into unrelated sections.
Layout features like this provide real value when components move between narrow and wide containers. Testing nested contexts early prevents brittle breakpoint rules.
When introducing `@scope`, begin with one reference component and treat it as the canonical pattern. This keeps implementation predictable and prevents style drift as the codebase grows.
Document each scoped root clearly so future contributors can trace where styles start and where they are intentionally blocked.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# HTML inert: Disable Interactions and Improve Accessibility
URL: https://theosoti.com/short/html-inert-attribute/
Published: 2025-08-13
Tags: HTML, accessibility, web development tip
> Learn how the HTML inert attribute blocks interactions, hides content from screen readers, and improves accessibility for modals and pop-ups.
## Heard about the new HTML "inert" attribute?
It’s a game-changer and it’s as simple as it is powerful.
When you add inert to an element:
- The element and everything inside it becomes inactive.
- Users can’t click or interact with it.
- Screen readers completely ignore it.
It’s perfect for modals and pop-ups!
With inert, you can ensure users stay focused on the modal without interacting with the rest of the page.
It’s super handy for keeping users focused and making sure screen readers skip over stuff they don’t need to see.
Bonus: better accessibility often means better SEO too!
How does it work?
Just add inert to any element you want to deactivate.
While it’s there, users can’t click, tab, or interact with it.
Remove it, and everything’s back to normal.
It’s simple, effective, and takes no effort to set up.
The best part ?
It’s available on every major browser with a support of 94%!
`inert` is a clean way to disable background interaction during modals and drawers. It blocks both pointer and focus access at once, which is safer than manually toggling many individual elements.
`inert` is a clean way to disable background interaction during overlays. It blocks both pointer and focus access, reducing the risk of partially disabled UI states.
Validate it with real content and realistic viewport ranges so implementation details hold up outside demos.
When introducing `inert`, begin with one reference component and treat it as the canonical pattern. That rollout sequence preserves clarity and reduces regressions during future refactors.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Custom List Bullets in CSS with @counter-style
URL: https://theosoti.com/short/customise-each-li-bullets/
Published: 2025-08-12
Tags: CSS, modern CSS, CSS tip
> Discover how to customize each list bullet in CSS using @counter-style and list-style-type. Unique symbols, emojis, or characters in just a few lines.
## Ever wanted to customise each `` bullets individually?
There is a way to do it in less than 5 lines of CSS!
We only need 2 things to make it work:
- The function “@counter-style” with some setup
- The property “list-style-type”
"list-style-type" is a pretty common property.
It allows you to change the style of the bullet or to remove it completely.
But here we will combine it with the @counter-style function.
Here how it works:
First, name your counter style (in this case, “cat-list”).
Then add some key properties:
- “symbols”: a list of elements (numbers, letters, emojis, special characters, etc.) that replace standard bullets.
- “system”: defines the algorithm to convert a counter’s value into a character string.
- “prefix”: adds characters before the symbol.
- “suffix”: adds characters after the symbol. By default, this is a dot.
The “system” property offers several options like “cyclic,” “numeric,” “alphabetic,” “symbolic,” “additive,” and “fixed.”
Here, we chose “cyclic,” which means that if the list exceeds the number of symbols provided, it will loop back to the first one.
Once everything is set up, link your custom style to “list-style-type" like so: “list-style-type: cat-list;".
And voilà, you now have a list with unique, customised bullets.
`@counter-style` is powerful for list systems where marker style communicates tone or structure. Keep marker contrast and spacing consistent for readability.
Selector-driven improvements scale best when specificity remains controlled. Keep targeting rules readable so future edits do not become cascade puzzles.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# CSS Light Sweep Animation for Cards without JavaScript
URL: https://theosoti.com/short/featured-card-animation/
Published: 2025-08-11
Tags: CSS, modern CSS, CSS animations, CSS gradients, performance, JavaScript alternatives, frontend development
> Learn how to create a smooth light sweep effect on cards using only CSS gradients and keyframes. Simple, reusable, and no JavaScript required.
## Ever wondered how to do a light sweep animation?
It’s pretty easy when you understand the logic.
I’ll show you here how to do it for a featured card.
But this can be declined for every components you need.
And no JavaScript needed.
Let's break it down into three key parts:
1- The card itself
Start with a container. Add a child div for the animation.
Style it however you like, but don’t forget `position: relative`. This anchors everything in place.
2- The animation Effect
The rotating light effect uses CSS animations combined with a conic gradient.
The animation layer fills the card with `position: absolute` and `inset: 0`.
Use a custom CSS property to control the rotation value.
It makes the animation flexible and easy to tweak.
Next, define a keyframe to rotate the light beam 360 degrees.
This will create that smooth sweep effect.
3- Superposing the :after element
To make the card complete, we overlay an :after pseudo-element on the effect.
It should sit on top with a slight inset (e.g., 4px) to create a border.
This frames the animation and gives it a polished look.
This approach is simple yet powerful.
With just CSS, you can create a dynamic, visually engaging elements that grabs attention.
Card light-sweep effects should be subtle and purposeful. They work well to indicate interactivity, but avoid constant motion that competes with core content.
Always include reduced-motion behavior. A strong animation pattern is one that degrades cleanly while preserving meaning.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# CSS Safe Area Insets: Protect Content from Phone Notches
URL: https://theosoti.com/short/safe-area-inset/
Published: 2025-08-10
Tags: CSS, modern CSS, responsive design, CSS layout, CSS shapes, frontend development, CSS tip
> Learn how CSS safe area insets keep your UI visible on phones with notches, rounded corners, and gesture bars, with broad browser support.
## Stop your content from being cut off by phone notches.
CSS safe area insets solve this problem easily.
Today’s smartphones come with notches, rounded corners, and gesture bars.
If you’re not accounting for them, your content risks being clipped or hidden.
Enter CSS safe area insets with `env().`
They keep your UI visible and out of those unsafe zones.
With just a few lines:
padding-bottom: 48px;
padding-bottom: env(safe-area-inset-bottom);
- Your sticky navbar or bottom buttons stay tap-friendly
- Works automatically on devices with notches, rounded corners, or gesture bars
- Falls back gracefully on devices that don’t need it
The best part? This has a browser support of 96.78%!
In production, apply safe-area values only where needed, usually fixed headers, sticky footers, and full-screen overlays. Avoid adding every inset everywhere, or spacing can feel oversized.
A common pattern is combining a base padding with the environment value, for example `padding-bottom: max(16px, env(safe-area-inset-bottom));`. This preserves comfortable spacing on all devices.
Safe area insets are most important for fixed UI near device edges. Combine baseline spacing with `env(safe-area-inset-*)` so controls stay comfortable on both notched and non-notched screens.
Safe-area values matter most for fixed bars and edge-attached controls. Combine baseline spacing with `env()` insets to support both notched and non-notched devices.
Before rollout, verify long content, dense cards, and extreme widths. Stable behavior under stress is what turns a neat trick into production CSS.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# New CSS Media Query Syntax: Easier, Cleaner, Faster
URL: https://theosoti.com/short/new-media-queries/
Published: 2025-08-05
Tags: CSS, modern CSS, responsive design, CSS layout, CSS tip
> CSS media queries now support range operators like ≤ and ≥. Write cleaner, more readable responsive rules with 90% browser support.
## Media queries aren't the only solution for responsive design.
But the syntax can now be cleaner and easier to read.
You can use range operators directly in media queries.
That means `>=`, `<=`, `>`, and `<` logic in a natural order.
Before this syntax, we often wrote long rules with `min-width` and `max-width`.
They worked, but they were harder to scan.
Now you can express the same condition in a shorter way.
For example, `@media (width >= 48rem)` reads almost like plain language.
You can also combine ranges in one condition.
That makes breakpoint rules easier to reason about during reviews.
The biggest gain is readability.
When a file has many responsive conditions, simple syntax reduces mistakes.
You spend less time decoding selectors and more time checking behavior.
Range media queries are easier to read and reduce breakpoint logic mistakes. Standardize unit choice and naming conventions so responsive rules stay clear during team reviews.
Range media queries improve readability and reduce breakpoint logic errors. Keep units and naming conventions consistent so responsive rules remain easy to review.
This pattern has the best payoff when documented with one clear usage rule. Consistent adoption turns small CSS features into reliable system behavior.
A practical way to adopt `min-width` is to scope it to one high-impact component first. Then reuse that same pattern in similar contexts so behavior stays consistent and review time stays low.
Keeping breakpoint ranges non-overlapping makes cascade behavior easier to reason about and prevents hard-to-debug edge widths.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Create a Back to Top Button with Just HTML and CSS
URL: https://theosoti.com/short/back-to-top/
Published: 2025-07-22
Tags: CSS, modern CSS, scroll-driven animations, CSS animations, accessibility, performance, HTML
> Create a back to top button with HTML and CSS using native smooth scrolling. No JavaScript, lightweight, and accessible.
## Most “back to top” buttons use JavaScript.
But they really don’t have to.
The browser already knows how to scroll.
We just have to ask nicely.
All you need is:
- Give your page an `id="top"` (usually on the ``).
- Add a link with `href="#top"` that scrolls back to the top.
- Use `position: fixed` to pin it in the corner.
- And `scroll-behavior: smooth` on the html for smooth transitions
No listeners.
No scroll events.
No extra code.
This is simpler, faster, and works everywhere.
Sometimes, native HTML and CSS are all you need.
Checkout the codepen: https://codepen.io/theosoti/pen/KwdzKGx
For accessibility, give the link a clear label (for example `aria-label="Back to top"`) and ensure the focus ring is visible when navigating by keyboard. Native anchors already preserve expected browser behavior, including opening in new tabs.
If you use smooth scrolling, consider respecting `prefers-reduced-motion` by disabling animated scroll for users who opt out of motion.
A native anchor-based back-to-top control stays lightweight and predictable across browsers. Place it where it is easy to reach, keep a clear label, and ensure anchor targets remain visible under sticky headers.
Anchor-based back-to-top patterns stay lightweight and robust. Pair them with clear placement and heading offsets so users return to context without layout jumps.
Validate it with real content and realistic viewport ranges so implementation details hold up outside demos.
A practical way to adopt `position: fixed` is to scope it to one high-impact component first. After validation, expand gradually to matching components and avoid ad-hoc one-off overrides.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Style Native Elements with Full CSS Control
URL: https://theosoti.com/short/customize-selects-css/
Published: 2025-07-20
Tags: CSS, modern CSS, HTML forms, JavaScript alternatives, CSS tip
> Say goodbye to default dropdowns. Chrome now supports full CSS styling for elements, no JS or libraries needed.
## Say goodbye to ugly ``.
CSS now gives you full control.
For years, `` elements have been frustrating to style.
Often needing JavaScript or UI libraries.
Now, modern browsers bring full customization to native dropdowns.
What’s New?
Chrome introduces new CSS properties for better `` styling.
No more browser inconsistencies!
- Total Style Control:
Customize backgrounds, borders, and padding without hacks.
- Custom Dropdowns:
Control width, height, and animations.
No more default system styles.
- Multi-Column & Scrollable Options:
Create advanced layouts, scrolling areas, and better positioning.
Why It Matters?
- Dropdowns match your design system without breaking accessibility.
- Native UI is lighter and quicker.
- No need for extra libraries nor JavaScript.
Rolling out in Chrome 135+, with other browsers likely to follow.
Start experimenting today!
This will finally end painful `` styling 🔥
Checkout the codepen: https://codepen.io/theosoti/pen/VYwqEvP
Custom-styled native selects should keep native behavior and clear affordances. Test focus rings, long option labels, and high-contrast states to ensure visual customization does not reduce usability.
Styled native selects should retain clear affordances and keyboard behavior. Validate long options, focus rings, and contrast so visual customization does not harm usability.
For production forms, test keyboard order, disabled states, and validation messages with realistic labels. Native behavior should remain intact while visual polish improves.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Truncate Multiline Text with Ellipsis Using Pure CSS
URL: https://theosoti.com/short/truncate-multiline-text/
Published: 2025-07-18
Tags: CSS, modern CSS, CSS layout, responsive design, CSS typography, performance, CSS tip
> Limit text to multiple lines with ellipsis using pure CSS. Perfect for cards, previews, and tight layouts.
## Have you ever had trouble creating multiple line breaks?
Here is a cool CSS hack that cuts your text smartly.
Simply add few lines of CSS to implement multiline truncation with ellipsis.
Here’s what you need:
```css
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
```
This CSS class will limit text content to three lines and hide the rest cut off content.
To break it down:
- display: -webkit-box = creates a flexible block container, similar to flexbox
- webkit-line-clamp = limits the number of visible lines.
- webkit-box-orient = ensures the content flows vertically.
- overflow: hidden = keeps things clean by hiding the rest of the text.
The result? Cut-off text that does not end colons.
It’s ideal for cards, previews or anywhere space is tight.
The best part?
It has more than 96% browser support!
Checkout the codepen: https://codepen.io/theosoti/pen/GgKJrwq
Multi-line truncation is most useful in cards and previews where heights must stay aligned. Choose clamp values per breakpoint and provide a full-text view so users can access hidden content when needed.
Line clamping is most useful for card grids where consistent heights matter. Pair truncation with accessible full-text paths so users can still access complete content.
Test with multilingual strings and varied word lengths. Typographic CSS should remain graceful when content is unpredictable.
A practical way to adopt `truncate multiline text` is to scope it to one high-impact component first. This keeps implementation predictable and prevents style drift as the codebase grows.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Crop and Zoom Images with CSS object-view-box
URL: https://theosoti.com/short/shape-outside-image/
Published: 2025-07-16
Tags: CSS, modern CSS, CSS layout, responsive design, CSS shapes, CSS tip
> Use object-view-box in CSS to crop and zoom images precisely—no image editing, just clean, flexible layout control.
## Crop images with precision, directly in CSS.
Forget `clip-path` and forget editing assets.
With `object-view-box`, you can zoom and crop right from your stylesheet.
It's like `viewBox` in SVG, but for any element using `object-fit`.
```css
img {
object-fit: cover;
object-view-box: inset(20% 0 0 0);
}
```
The syntax is familiar: use `inset()` or even `rect()` to define your cropping zone.
This works alongside `object-fit` to give you complete control over what portion of the image is displayed.
Need to zoom on a specific part of an image?
Want to highlight just a region inside a video or iframe?
No JavaScript. No image editing. No container tricks.
Just pure CSS.
Browser support is still experimental (~76%), so don’t use it in production yet.
But it’s available behind flags or in dev builds for testing.
This property is the kind of thing you’ll soon wonder how you ever built layouts without.
Checkout the codepen: https://codepen.io/theosoti/pen/yyYeENV
Using `object-view-box` for focal cropping is useful when assets need consistent framing across different cards. Define focal rules early so important subject areas stay visible at every breakpoint.
Focal cropping with `object-view-box` helps keep important image regions visible across reusable card formats. Set focal rules once, then verify results at multiple aspect ratios.
Prefer subtle enhancement over heavy treatment. The visual should support hierarchy, not compete with content.
To roll this out safely, start by applying `clip-path` in a single UI surface where the benefit is obvious. After validation, expand gradually to matching components and avoid ad-hoc one-off overrides.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Wrap Text Around Images with CSS shape-outside
URL: https://theosoti.com/short/wrap-text-around-images/
Published: 2025-07-14
Tags: CSS, modern CSS, CSS layout, responsive design, CSS shapes, CSS typography, performance
> Use shape-outside in CSS to wrap text around custom image shapes—no JavaScript, just clean, creative layout control.
## Make your text wrap around images perfectly.
By default, text wraps around a boring rectangle.
But what if you could make it hug the actual shape of the image?
You can.
With just one CSS property:
`shape-outside: url(your-img.png);`
Add a `float` and a bit of margin with `shape-margin`,
and your layout feels instantly more refined.
No JavaScript. No layout hacks.
Just native CSS support and it's supported in over 95% of browsers.
Use it with transparent PNGs, SVGs, or even basic shapes like `circle()` or `polygon()`.
Ideal for editorial layouts, landing pages, or any design that needs more personality.
Checkout the codepen: https://codepen.io/theosoti/pen/ogjjged
`shape-outside` can produce elegant editorial layouts when image silhouettes stay simple. Test long paragraphs and varied image ratios, because float-based wrapping can become fragile in edge cases.
`shape-outside` can create editorial layouts with strong flow, but test with varied image sizes and long text. Float-based wrapping can break faster than block layouts.
Use this as a readability tool, not only a visual effect. The best result is when style improves scanning speed without adding cognitive load.
To roll this out safely, start by applying `shape-outside: url(your-img.png);` in a single UI surface where the benefit is obvious. Then reuse that same pattern in similar contexts so behavior stays consistent and review time stays low.
Before shipping, test `shape-outside: url(your-img.png);` with both short and long content, then verify behavior in narrow and wide containers.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Build an Autoplay Carousel with Pure CSS
URL: https://theosoti.com/short/autoplay-carousel/
Published: 2025-07-12
Tags: CSS, modern CSS, CSS layout, responsive design, CSS animations, accessibility, performance
> Create a smooth, accessible autoplay carousel using only CSS—no JavaScript, no libraries, and zero layout shifts.
## Still using JavaScript for an autoplay carousel?
You don’t need to anymore.
A one-directional, looping logo strip can now be built entirely with CSS.
No `setInterval`. No libraries. No layout shifts.
Here’s how it works:
You define a few CSS custom properties:
- `--width` and `--height` to size each item
- `--quantity` to set how many elements are in the loop
- `--duration` for how long one full cycle takes
Each item gets a `--position`, so we can stagger its animation using `animation-delay`.
This formula spaces each item evenly:
```css
calc((var(--duration)/var(--quantity)) * (var(--position) - 1) - var(--duration))
```
It ensures that the track starts full, no empty gaps at load.
The animation itself is simple:
From `left: 100%` to `left: -width`, sliding items across the track.
We use `position: absolute` so every item animates independently,
and `overflow: hidden` on the container to act as a viewport.
For a smooth entrance and exit, we apply a `mask-image` gradient.
This fades logos in/out without extra DOM or performance hits.
Accessibility? Covered.
Each logo gets `tabindex="0"` so keyboard users can focus them,
and the entire animation pauses on hover or focus using `:has(:focus)` and `animation-play-state: paused`.
No JavaScript.
Just CSS, doing what used to take 100KB+ of JS.
Checkout the codepen: https://codepen.io/theosoti/pen/OPVeegy
Autoplay carousels should never hide control from users. Keep pause/stop behavior clear and predictable, and ensure content remains readable without waiting for automatic cycles.
Motion should clarify state change, not distract from content. Keep timings short and ensure users can still complete actions quickly.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Create Accessible Popups with the Native HTML Popover API
URL: https://theosoti.com/short/html-popover-api/
Published: 2025-07-10
Tags: HTML, accessibility, performance, JavaScript alternatives, frontend development, web development tip
> Build tooltips, modals, and popups using the HTML Popover API—no JavaScript needed, fully accessible, and widely supported.
## Say hello to native popovers in HTML…
Without a line of JavaScript!
The Popover API lets you create smooth popups using just HTML and CSS.
With the `popover` attribute, you can define elements that behave like tooltips, modals, or popups.
Combine it with the `popovertarget` and `popovertargetaction` attributes, and you've got full control over when and how the popover opens or closes.
Why use it?
- Clean, semantic HTML structure.
- No JavaScript needed
- Full accessibility baked in (no extra ARIA attributes required).
- Native browser support for interactions like open/close states.
Bonus: It is supported by 90% of the browsers so far.
So it is pretty safe to use!
Checkout the codepen: https://codepen.io/theosoti/pen/ZENpowe
The Popover API is ideal for lightweight overlays attached to a trigger context. Keep dismissal paths obvious with Escape and close controls, so keyboard and touch interactions feel equally reliable.
The Popover API is ideal for lightweight overlays tied to a trigger context. Keep dismissal paths explicit with Escape and close controls for reliable keyboard and touch behavior.
Check autofill, mobile keyboards, and error feedback before shipping. These practical states reveal more than static demos and keep the form experience trustworthy.
A practical way to adopt `popover` is to scope it to one high-impact component first. This keeps implementation predictable and prevents style drift as the codebase grows.
For critical actions, include a clear close button inside the popover so dismissal remains obvious even without keyboard input.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Style Native Form Elements with accent-color in CSS
URL: https://theosoti.com/short/native-input-color/
Published: 2025-07-08
Tags: CSS, modern CSS, CSS colors, CSS typography, HTML forms, accessibility, performance
> Easily customize checkboxes, radios, sliders, and more using accent-color—no wrappers, no JavaScript, fully accessible.
## Tired of blue checkboxes that don’t match your brand?
Now you can fix that in one line of CSS.
Native form elements are finally catching up.
You can now change the color of checkboxes, radios, range sliders, and even progress bars.
Without JavaScript, wrappers, or hacks.
It’s as simple as:
```css
input,
progress {
accent-color: red;
}
```
Why this matters:
- Matches your brand identity down to the smallest details
- Works on all common input types, no need for custom components
- Keeps the native behavior and accessibility intact
It’s supported in all major browsers (even mobile!) with 94,6%.
So if you’re still using SVG icons or replacing inputs with divs just to match a color, you can stop now.
`accent-color` gives quick brand alignment for native controls with almost no overhead. Treat it as a polish layer and still verify contrast, focus visibility, and disabled states across form components.
`accent-color` is a fast way to align native controls with brand tone while keeping semantics intact. Treat it as polish, and still verify contrast and focus clarity across states.
On form-heavy pages, this is most effective when you keep semantics native and style enhancements lightweight. That balance preserves accessibility and reduces maintenance cost.
When introducing `accent-color`, begin with one reference component and treat it as the canonical pattern. That rollout sequence preserves clarity and reduces regressions during future refactors.
As a final check, run `accent-color` through edge cases like dense layouts, long labels, and constrained mobile widths.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Unlock All 4 CSS Focus States and 1 Hidden Gem
URL: https://theosoti.com/short/4-css-focus-states/
Published: 2025-07-07
Tags: CSS, modern CSS, accessibility, CSS tip
> Learn the 4 focus states in CSS. :focus, :focus-visible, :focus-within, and a hidden gem—for better accessibility and smoother UX.
## Did you know there are 4 different focus states in CSS?
One of them is secret 🤫
CSS Focus states might seem straightforward at first.
In reality, there are multiple selectors that control how and when focus is displayed on elements.
There are three primary focus states in CSS, plus a “secret” fourth one.
Here are the 3 main CSS focus:
- **:focus** the most common one
- **:focus-visible**, it’s like :focus, but visible only on certain conditions
- **:focus-within** will focus the container element when one of its children is focused
For the fourth one, you can go check my blog article here: https://theosoti.com/blog/css-focus-hack/
Understanding how to use these focus states correctly can elevate accessibility, create smoother interfaces, and improve user navigation.
Separating focus states by intent improves both accessibility and visual clarity. Using `:focus`, `:focus-visible`, and container states deliberately prevents noisy outlines while preserving strong keyboard feedback.
Using focus states deliberately (`:focus`, `:focus-visible`, and container variants) improves keyboard UX and avoids noisy outlines for mouse users.
Validate it with real content and realistic viewport ranges so implementation details hold up outside demos.
A practical way to adopt `:focus` is to scope it to one high-impact component first. After validation, expand gradually to matching components and avoid ad-hoc one-off overrides.
Before final rollout, validate `:focus` against real content and real interaction states, not only demo text.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Simplify CSS Animations with Individual Transform Properties
URL: https://theosoti.com/short/individual-transform-properties/
Published: 2025-07-06
Tags: CSS, modern CSS, CSS animations, HTML forms, CSS tip
> Use translate, rotate, and scale as separate CSS properties for cleaner, modular, and easier-to-maintain animations.
## Animations just got easier to manage.
And your CSS just got a whole lot cleaner.
For years, combining multiple values in transform meant repeating the full list every time you wanted to change just one.
It worked, but it wasn’t ideal.
Now you can split transforms into individual properties:
- translate
- rotate
- scale
No more repetition.
No more overrides.
Just clean, focused changes.
Why it's better:
- Each transformation is isolated. You can update one without touching the others.
- It makes animations more readable, modular, and easier to maintain.
- You can now animate them independently at different speeds or stages.
Browser support is solid across all modern browsers.
This is one of those tiny syntax upgrades that makes a big difference.
Individual transform properties make motion easier to tune because each axis can be adjusted independently. This keeps animation edits cleaner than repeatedly rewriting long `transform` value chains.
Individual transform properties make motion tuning cleaner because each axis can be adjusted independently. This avoids repeatedly rewriting long combined transform values.
For production forms, test keyboard order, disabled states, and validation messages with realistic labels. Native behavior should remain intact while visual polish improves.
A practical way to adopt `transform` is to scope it to one high-impact component first. Then reuse that same pattern in similar contexts so behavior stays consistent and review time stays low.
This is especially helpful during iterative motion tuning because each property can be adjusted without rewriting the whole transform chain.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Build Smarter Layouts with CSS Container Queries
URL: https://theosoti.com/short/container-queries/
Published: 2025-07-05
Tags: CSS, modern CSS, container queries, responsive design, CSS layout, frontend development, CSS tip
> Make components truly responsive with CSS container queries. Style elements based on parent size, not screen width.
## Responsive design isn’t just about screen size anymore.
Container queries give components the power to adapt on their own.
Responsive design isn’t just about screen size anymore.
Container queries give components the power to adapt on their own.
That’s where CSS container queries come in.
They let you apply styles based on the size of the parent, not the window.
What makes them special:
- Components become truly reusable and context-aware.
- You can build layouts that behave consistently in any environment.
- No more overrides based on where a component happens to land.
How it works:
- Set a container with `container-type: inline-size;`.
- Inside that container, use `@container` rules to style elements based on width.
Browser support is already strong at ~94%.
Just keep in mind: unsupported browsers will skip `@container` rules entirely.
So be sure your base styles hold up without them.
Don't hesitate to read the full article on my blog: https://theosoti.com/blog/container-queries/
Container queries shine when the same component appears in narrow and wide contexts. Defining behavior from container size, not viewport size, keeps modules reusable without layout-specific overrides.
Container-driven rules make components genuinely reusable across unknown parent layouts. This reduces breakpoint duplication and keeps local behavior tied to actual available space.
Use this pattern to keep responsive behavior local to the component instead of scattering viewport overrides. That makes scaling and refactoring much safer.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Add Smooth Page Transitions with CSS @view-transition
URL: https://theosoti.com/short/view-transition/
Published: 2025-07-04
Tags: CSS, modern CSS, CSS animations, performance, JavaScript alternatives, CSS tip
> Use @view-transition to create crossfade effects between pages—no JavaScript, just clean, progressive CSS.
## Page loads don’t have to be harsh.
CSS lets you add smooth transitions between pages with 1 line.
For a long time, animating between two pages required JavaScript tricks, frameworks, or SPA logic.
Now? It’s built into the browser.
By adding this tiny CSS snippet, you can crossfade between pages automatically:
```css
@view-transition {
navigation: auto;
}
```
What it does:
It tells the browser to animate between navigations, using a smooth crossfade.
Why it’s so cool:
- It works instantly, no JS needed
- If the browser doesn’t support it, nothing breaks
- It makes your site feel way more polished
This is progressive enhancement at its best.
Old browsers ignore it (81% support currently).
New ones reward you with smooth UX.
And that’s just the default behaviour.
You can customise it to animate specific elements, trigger transitions manually, and more.
`@view-transition` can make page changes feel continuous instead of abrupt. Keep transitions short and meaningful, and ensure unsupported browsers still get stable navigation without visual glitches.
View transitions are best when they explain navigation changes, not when they add spectacle. Keep timings short and fallbacks clean for unsupported browsers.
Run interaction tests with rapid clicks and navigation changes. Input should stay responsive even while transitions are active.
For teams, `@view-transition` is easiest to maintain when it starts in one documented example before broader reuse. That rollout sequence preserves clarity and reduces regressions during future refactors.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Smooth Scrolling with CSS scroll-behavior
URL: https://theosoti.com/short/smooth-scroll/
Published: 2025-07-03
Tags: CSS, modern CSS, scroll-driven animations, CSS animations, performance, JavaScript alternatives, frontend development
> Enhance anchor navigation with smooth scrolling and better section spacing using pure CSS—no JavaScript required.
## Jumping straight to a section feels rough.
Smooth scrolling makes navigation feel effortless.
When users click an anchor link, the browser jumps abruptly to the section.
This can be jarring, especially on long pages.
Instead of relying on JavaScript for smooth scrolling, CSS has a built-in solution: “scroll-behavior: smooth”.
With just this, your page transitions fluidly between sections.
But what if the content feels too close to the edge?
By default, scrolling stops at the very top of the section, sometimes cutting off important content.
Fix this with `scroll-padding` or `scroll-margin`: scroll-margin-top: 20px;
Now, every scroll lands in a comfortable, readable position.
It’s lightweight, easy to implement, and widely supported (more than 95,5%).
Here's the codepen: https://codepen.io/theosoti/pen/KKOGJjZ
Native smooth scrolling works well for in-page links and table-of-contents navigation. Pair it with heading `scroll-margin` so anchors land with comfortable spacing below fixed interface chrome.
Native smooth scrolling is great for in-page navigation when motion remains subtle. Add heading offsets so anchor jumps feel intentional, not cramped under fixed UI chrome.
Run interaction tests with rapid clicks and navigation changes. Input should stay responsive even while transitions are active.
A practical way to adopt `scroll-padding` is to scope it to one high-impact component first. This keeps implementation predictable and prevents style drift as the codebase grows.
When using sticky headers, set heading-specific `scroll-margin-top` values so each section lands with consistent breathing room.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Create Animated Gradient Borders with Pure CSS
URL: https://theosoti.com/short/border-gradient/
Published: 2025-07-02
Tags: CSS, modern CSS, responsive design, CSS layout, CSS gradients, performance, JavaScript alternatives
> Build clean, animated gradient borders using only CSS—no extra elements, fully responsive, and JavaScript-free.
## You can create gradient borders in pure CSS.
No extra elements. No pseudo-elements.
Here’s how it works:
Set a thick transparent border.
Then layer two backgrounds:
- A solid fill using `padding-box`
- A gradient using `border-box`
The transparent border acts as space for the gradient to show through.
It works seamlessly with `border-radius`.
And because it's just one element, it keeps your HTML clean.
Now let’s go a step further: animation.
With the `@property` rule, you can register custom values, like an angle.
That allows CSS to transition between them smoothly.
Define something like `--angle` for your gradient direction.
Update it on hover or with a keyframe.
CSS will animate the change automatically, no JavaScript needed.
This opens up subtle, lightweight effects for buttons, cards, and more.
And since it's based on native CSS features, it's well-supported and performant.
Here's the codepen: https://codepen.io/theosoti/pen/myJzeKN
Animated gradient borders are visually strong, so moderation matters. They work best on featured surfaces or key CTAs, while everyday components should keep calmer borders for better hierarchy.
Animated gradient borders work best as highlights, not defaults. Use them on featured components and keep everyday surfaces visually calmer to preserve hierarchy.
Always include reduced-motion behavior. A strong animation pattern is one that degrades cleanly while preserving meaning.
A practical way to adopt `padding-box` is to scope it to one high-impact component first. After validation, expand gradually to matching components and avoid ad-hoc one-off overrides.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Style Drop Caps in CSS with :first-letter and initial-letter
URL: https://theosoti.com/short/customise-first-letter/
Published: 2025-07-01
Tags: CSS, modern CSS, CSS typography, CSS tip
> Add editorial flair to paragraphs using :first-letter and initial-letter in CSS—no extra markup, just clean, elegant styling.
## Want to give your paragraphs a touch of editorial flair?
CSS lets you style and position the first letter without any extra markup.
With the :first-letter pseudo-element, you can create drop caps and elegant intros like those seen in magazines.
The real magic comes from the initial-letter property.
It controls both the size and alignment of the first letter in just one line.
For example:
`initial-letter: 3 2;` means the letter spans 3 lines and aligns against 2 lines of baseline text.
You can then style it as you want.
Browser support is solid at over 95%.
Have you used :first-letter creatively before? Would love to see it!
Here is a live example: https://codepen.io/theosoti/pen/PwoaJNP
Drop-cap styling is strongest when it supports editorial tone rather than decoration for its own sake. Keep spacing and baseline alignment controlled, especially on small screens where oversized initials can disrupt flow.
Drop-cap styling with `:first-letter` should support reading tone, not dominate it. Tune spacing carefully so the opening paragraph remains comfortable to scan.
Typography refinements are most visible with real content length and mixed wording. Validate rhythm, spacing, and line breaks on mobile where small issues become obvious.
For teams, `initial-letter: 3 2;` is easiest to maintain when it starts in one documented example before broader reuse. After validation, expand gradually to matching components and avoid ad-hoc one-off overrides.
On narrow screens, reduce the drop-cap size slightly to keep the first lines readable and avoid awkward text collisions.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Smarter Layouts with CSS calc-size() without JS
URL: https://theosoti.com/short/ratings-calc-size/
Published: 2025-06-25
Tags: CSS, modern CSS, CSS layout, responsive design, JavaScript alternatives, frontend development, CSS tip
> Use the new calc-size() CSS function to size elements dynamically from layout and attribute values—no JS, just responsive, flexible UI.
## Smarter sizing with just one CSS function.
Let your content define the layout... dynamically.
You can now use calc-size() to compute size values based on both layout constraints and live attributes.
Why is that a big deal?
Because it makes things like this rating system possible with:
- No JS
- No inline styles
- No custom properties
Just pure CSS, reacting to attributes like data-rating.
The calc-size() function is a new addition to CSS.
It allows you to perform calculations using intrinsic size values like auto, min-content, max-content, or fit-content.
These values aren’t supported in regular calc(), which makes calc-size() a powerful new tool.
You can also use this function to animate an element’s height to auto!
It’s powerful, it’s expressive, and it unlocks a new level of flexibility for dynamic UI design.
Here's the codepen link: https://codepen.io/theosoti/pen/gbOVBVp
`calc-size()` is useful when component dimensions should adapt without JS measurement loops. It works well for controls and rating UIs where content-driven sizing still needs predictable limits.
`calc-size()` helps combine content-driven sizing with fixed constraints. It is useful when UI parts should flex naturally but still remain within intentional layout bounds.
This pattern has the best payoff when documented with one clear usage rule. Consistent adoption turns small CSS features into reliable system behavior.
When introducing `calc-size()`, begin with one reference component and treat it as the canonical pattern. This keeps implementation predictable and prevents style drift as the codebase grows.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Create Animated Text Gradients with Pure CSS
URL: https://theosoti.com/short/animated-text-gradient/
Published: 2025-06-20
Tags: CSS, modern CSS, CSS animations, CSS gradients, CSS typography, performance, JavaScript alternatives
> Use background-clip: text and keyframes to animate gradients on text—no JavaScript needed, fully CSS-powered, and smooth across modern browsers.
## Animated text gradients with pure CSS?
Yes, it’s possible, and surprisingly smooth.
This technique uses `background-clip: text` to apply a gradient directly to the text.
It then animates the background to create a subtle shimmering effect.
A few things that make it work:
- Set the text color to `transparent` so only the background shows through.
- Use `background-size: 200% auto` to give the gradient space to move.
- Animate the `background-position` with keyframes to create motion.
- Repeat the first color at the end of your gradient (color1 → color2 → color1) to make the loop seamless.
No JavaScript needed. Just clever layering with modern CSS.
Support is solid across Chromium and WebKit browsers, with Firefox catching up.
Here's the codepen: https://codepen.io/theosoti/pen/ogNoRPp
Animated text gradients are most effective when motion stays restrained and contrast remains high. The static state should still read clearly, so users with reduced motion preferences keep full readability.
Animated text gradients should be treated as accent motion, not core content delivery. Keep fallback readability strong so users still understand the message without animation.
Use this as a readability tool, not only a visual effect. The best result is when style improves scanning speed without adding cognitive load.
A practical way to adopt `background-clip: text` is to scope it to one high-impact component first. After validation, expand gradually to matching components and avoid ad-hoc one-off overrides.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Trim text spacing with text-box-trim and text-box-edge
URL: https://theosoti.com/short/text-box-trim/
Published: 2025-06-19
Tags: CSS, modern CSS, CSS layout, responsive design, CSS typography, frontend development, CSS tip
> Use text-box-trim and text-box-edge to remove excess space around text for cleaner UI layouts—perfect for buttons, labels, and tight spacing.
## Tired of inconsistent text spacing in your UI?
CSS has a new fix: `text-box-trim` and `text-box-edge`.
These properties let you trim excess space around text for tighter, more predictable layouts.
Here’s the syntax:
text-box-trim: trim-start | trim-end | trim-both;
text-box-edge: cap | ex | alphabetic | text;
What they do:
- `text-box-trim`: controls which edge(s) of the text box to trim.
- `text-box-edge`: defines what the top and bottom of the box align to.
Key values:
- `cap`: aligns to the cap height of the font (great for uppercase headers).
- `alphabetic`: aligns with the text’s baseline (standard for most Latin scripts).
- `ex`: based on the height of a lowercase “x”, useful for lowercase-heavy text.
- `text`: uses the full visual bounds of the glyphs (like accents or descenders).
Pairing `trim-both` with `cap alphabetic` gives you ultra-clean vertical alignment. Perfect for buttons, labels, and tight UI layouts.
Browser support is still limited with ~80%.
But unsupported browsers will ignore them and fall back to the default text box behaviour.
Here's the codepen: https://codepen.io/theosoti/pen/azORoJZ
`text-box-trim` is valuable when vertical spacing must feel optically balanced, not only mathematically centered. It is especially useful for buttons, chips, and labels where tiny spacing inconsistencies are very visible.
`text-box-trim` helps when optical alignment matters, especially for compact controls like chips, pills, and buttons where tiny baseline gaps are noticeable.
Typography refinements are most visible with real content length and mixed wording. Validate rhythm, spacing, and line breaks on mobile where small issues become obvious.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Enhance Form UX with the Native Element
URL: https://theosoti.com/short/form-datalist/
Published: 2025-06-18
Tags: HTML forms, accessibility, performance, web development tip
> Improve form usability using for autocomplete suggestions—lightweight, accessible, flexible, and fully supported in modern browsers.
## Build better forms with one HTML tag
Autocomplete. Suggestions. Flexibility.
The native `` element can help improve form UX.
Here’s how it works:
You pair a standard ` ` with a `` using the `list` attribute.
Each `` in the datalist appears as a suggestion as the user types, but they’re free to enter a completely custom value too.
And it’s not just for text. It works with `color`, `time` and `range`.
Why it’s worth using:
- Keeps forms lightweight and fast
- No extra libraries or JS required
- Fully accessible and keyboard-friendly
- Still allows user freedom, not just a strict dropdown
It’s a small detail that can make a big difference in form usability.
And the best part? It’s supported in all modern browsers with over 95% support.
Curious how often you reach for it!
Here's the codepen: https://codepen.io/theosoti/pen/azOKxoo
`datalist` is ideal for lightweight suggestions when users still need free text input. Treat suggestions as guidance, then validate submitted values normally because users can always type custom content.
`datalist` is best for lightweight suggestions where free typing remains important. Keep server-side validation in place, because users can always submit values outside suggestions.
For production forms, test keyboard order, disabled states, and validation messages with realistic labels. Native behavior should remain intact while visual polish improves.
For teams, `list` is easiest to maintain when it starts in one documented example before broader reuse. After validation, expand gradually to matching components and avoid ad-hoc one-off overrides.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Unlock the Power of the Native Element in HTML
URL: https://theosoti.com/short/native-dialog-element/
Published: 2025-06-17
Tags: CSS, modern CSS, CSS animations, HTML, accessibility, JavaScript alternatives, frontend development
> Use the HTML element for accessible, stylable modals with built-in focus trap, keyboard support, and native transitions—no extra JS needed.
## The native `` element is more capable than you think.
It's not a new element, but it’s often overlooked in favor of custom modal components.
Yet it handles a lot out of the box:
- Built-in open/close API (.showModal() and method="dialog")
- Automatically traps focus for accessibility
- Keyboard support (like closing on Escape)
- Native backdrop with the ::backdrop pseudo-element
- Easily stylable with modern CSS
- Supports transitions with @starting-style and transition-behavior: allow-discrete
If you’ve been building modal systems from scratch, the native dialog might deserve a second look.
It’s simple, accessible, and works with the tools you already use.
Have you used `` in production?
Here's the codepen: https://codepen.io/theosoti/pen/WbvJXvZ
The native `` element simplifies focus management and modality with far less custom code. Keep close actions explicit and labels clear, then style `::backdrop` to reinforce context without reducing contrast.
The native `` element reduces custom modal plumbing while preserving expected keyboard behavior. Clear close actions and accessible labels remain essential for usability.
Check autofill, mobile keyboards, and error feedback before shipping. These practical states reveal more than static demos and keep the form experience trustworthy.
To roll this out safely, start by applying `::backdrop` in a single UI surface where the benefit is obvious. After validation, expand gradually to matching components and avoid ad-hoc one-off overrides.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Make Form Fields Auto-Resize with field-sizing: content
URL: https://theosoti.com/short/native-field-sizing/
Published: 2025-06-16
Tags: CSS, modern CSS, CSS typography, HTML forms, performance, JavaScript alternatives, CSS tip
> Let inputs and textareas resize to fit content automatically using field-sizing: content—no JavaScript needed, with growing browser support.
## Forms should adapt to content.
Not the other way around.
Ever had to manually set the width of an input field, only to realise it’s either too short or too long?
Or worse, had to use JavaScript to resize it dynamically?
Now, CSS makes it effortless with just one property: field-sizing: content;
What does it do?
- Input, select, and textarea automatically adjust to fit their content.
- No need for JS workarounds or arbitrary width settings.
- Forms look cleaner and adapt naturally to user input.
The best part?
It works with placeholders too.
Fields shrink to the perfect size even before typing begins.
With browser support growing (more than 71%), this will be a game-changer for form UX.
You can already start using it as progressive enhancement.
Here's the codepen: https://codepen.io/theosoti/pen/mydXVEy
`field-sizing: content` helps compact forms feel natural by adapting control width to user input. Add sensible min and max bounds so fields remain usable with both very short and very long values.
`field-sizing: content` improves comfort in compact forms by adapting to input length. Add sensible min/max constraints so controls stay usable with very short or very long values.
Check autofill, mobile keyboards, and error feedback before shipping. These practical states reveal more than static demos and keep the form experience trustworthy.
A practical way to adopt `field-sizing: content` is to scope it to one high-impact component first. This keeps implementation predictable and prevents style drift as the codebase grows.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Modern CSS Color Functions You Should Be Using
URL: https://theosoti.com/short/list-css-color-functions/
Published: 2025-06-15
Tags: CSS, modern CSS, CSS colors, accessibility, CSS tip
> Unlock the full power of modern CSS with color functions like oklab(), color-mix(), and color-contrast() for vibrant, accessible, future-ready designs.
## CSS color functions are more powerful than ever.
Are you using them to their full potential?
Modern CSS now supports a rich set of color functions that go far beyond just rgb() and rgba().
Here’s a quick breakdown of what’s available:
1- hsl() / hsla()
Define colors using hue, saturation, and lightness. hsla() adds transparency.
2- lab() / oklab()
Perceptual color spaces that better match human vision. oklab() is optimized for digital use.
3- lch() / oklch()
Like lab()/oklab(), but in cylindrical form—great for adjusting brightness and intensity.
4- rgb() / rgba()
Classic red-green-blue color model. rgba() adds an alpha channel for opacity.
5- color()
Use advanced color spaces like display-p3 for more vibrant, high-fidelity colors.
6- color-mix()
Blend any two colors together, with control over mix ratio and color space.
7- color-contrast()
Automatically picks the most readable color from a list based on background.
8- hwb()
Defines color by hue, whiteness, and blackness—more intuitive for some adjustments.
9- device-cmyk()
Used for defining colors in print workflows using CMYK values.
10- light-dark()
Chooses a different color depending on light or dark mode.
These tools make it easier to create accessible, responsive, and future-proof designs.
Browser support for many of these is growing fast, especially in Chromium and Safari.
Modern color functions help build tonal systems directly in CSS. Use them to derive variants from one source color so component states remain coherent.
Color and effect features are strongest when contrast remains the first priority. Check hover, focus, and disabled states before finalizing.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# End CSS Specificity Battles with Native CSS Layers
URL: https://theosoti.com/short/layer-property/
Published: 2025-06-13
Tags: CSS, modern CSS, CSS tip
> Control style priority and prevent overrides using CSS Layers—an organized, scalable way to manage specificity in modern stylesheets.
## Tired of battling CSS specificity wars?
Meet CSS Layers.
A native solution to organise and control your stylesheets more effectively.
Why use them?
- Manage specificity and source order with ease.
- Avoid unintended overrides from third-party styles or components.
- Enhance maintainability in large codebases.
How to implement:
This part shows how to set up and organize your styles using CSS Layers:
```css
@layer secondary, main;
```
This line declares the order of your layers. It's like a priority list:
- `secondary` layer loads first (lowest priority)
- `main` comes after (highest priority)
Then, you define your actual styles inside those layers:
```css
@layer main {
/* your style */
}
@layer secondary {
/* your style */
}
```
Each layer is scoped, and the order you define them in the initial @layer line determines their priority.
Check out the visual:
- Left = classic CSS behavior → body h1 wins with higher specificity
- Right = layered CSS → h1 in the main layer wins, even with lower specificity
95%+ browser support makes this a no-brainer to start using now.
`@layer` works best when you define a clear cascade order up front, such as reset, base, components, and utilities. That structure prevents specificity battles and makes style overrides predictable.
`@layer` works best with a documented cascade order like reset, base, components, and utilities. This prevents priority battles and makes overrides predictable.
Color and effect features are strongest when contrast remains the first priority. Check hover, focus, and disabled states before finalizing.
A practical way to adopt `@layer` is to scope it to one high-impact component first. Then reuse that same pattern in similar contexts so behavior stays consistent and review time stays low.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Align the Last Line of Text with This Little-Known CSS Trick
URL: https://theosoti.com/short/align-last-line-of-text/
Published: 2025-05-31
Tags: CSS, modern CSS, CSS layout, responsive design, CSS typography, CSS tip
> Use text-align-last to style only the final line of a paragraph—combine it with justified text for elegant layouts, supported by 95% of browsers.
## Did you know you can align just the last line of a paragraph?
Most developers are familiar with text-align.
But there’s a lesser-known CSS property that gives you more control.
This lets you style the final line of text differently from the rest.
Here’s how it works:
```css
p {
text-align: justify;
text-align-last: end;
}
```
—> All lines are justified, except the last one, which aligns to the end.
The best part?
A solid 95.28% across modern browsers.
So yes, it's production-ready.
`text-align-last` is most noticeable with longer justified paragraphs, so test with realistic content length before rolling it out globally. On very short text blocks, the visual effect can feel abrupt.
It is a great choice for editorial layouts, pull quotes, and card descriptions where you want a polished edge without extra markup. Keep it scoped to specific components instead of applying it site-wide.
`text-align-last` is subtle, but it adds polish to justified excerpts and editorial blocks. Apply it to selected content components rather than globally, so small UI labels are not affected unnecessarily.
`text-align-last` is subtle but effective in editorial UI and justified excerpts. Apply it selectively where line endings influence rhythm, rather than enabling it globally.
Use this as a readability tool, not only a visual effect. The best result is when style improves scanning speed without adding cognitive load.
When introducing `text-align-last`, begin with one reference component and treat it as the canonical pattern. That rollout sequence preserves clarity and reduces regressions during future refactors.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Write Cleaner CSS with 4 Essential Flexbox Shorthands
URL: https://theosoti.com/short/4-flexbox-shorthand-properties/
Published: 2025-05-28
Tags: CSS, modern CSS, Flexbox, CSS layout, CSS tip
> Simplify your Flexbox code with 4 powerful CSS shorthands. Cleaner, shorter, and perfect for reusable components and utility classes.
## Write cleaner Flexbox with these 4 shorthands.
Why write 3 lines…
When one will do the trick?
Flexbox has powerful shorthand properties that reduce clutter and make your CSS easier to read and maintain.
Here, I’ve condensed:
row-gap + column-gap → gap: row col
flex-grow + flex-shrink + flex-basis → flex: 1 1 0 (or just flex: 1)
align-content + justify-content → place-content
flex-direction + flex-wrap → flex-flow
These shorthands do exactly the same thing, just smarter and more scalable.
Pro tip: If you’re building reusable components or utility classes, using shorthands is a great way to reduce CSS bloat.
Shorthands are powerful, but clarity still matters. For example, `flex: 1` is convenient, yet `flex: 1 1 0` can be clearer when teaching or debugging layout behavior.
A good rule is to use shorthands in stable patterns (utilities and components), and keep longhand when intent might be ambiguous. That balance keeps stylesheets compact without hiding critical layout logic.
Flexbox shorthands reduce boilerplate, but they are best when intent stays explicit. Use compact forms in stable utilities, and expand longhand only when you need clearer debugging around sizing behavior.
Flexbox shorthands reduce clutter, but clarity still matters. Use compact syntax for stable utilities and keep longhand when explicit behavior helps debug complex sizing issues.
Validate it with real content and realistic viewport ranges so implementation details hold up outside demos.
To roll this out safely, start by applying `flex: 1` in a single UI surface where the benefit is obvious. Then reuse that same pattern in similar contexts so behavior stays consistent and review time stays low.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# 4 Reusable CSS Snippets for Faster, Cleaner Code
URL: https://theosoti.com/short/4-css-snipets/
Published: 2025-05-27
Tags: CSS, modern CSS, responsive design, CSS layout, CSS typography, performance, JavaScript alternatives
> Discover 4 practical CSS snippets: circle shapes, text truncation, centering, and responsive grids. Reusable, efficient, and no JavaScript needed.
## CSS snippets you’ll actually reuse.
These aren’t flashy tricks.
They’re practical, time-saving patterns you’ll find yourself using again and again.
In this image, I’ve shared four essentials:
✅ A perfect circle using aspect-ratio
```css
.circle {
aspect-ratio: 1;
border-radius: 50%;
}
```
✅ A multiline text truncation with ellipsis (no JS required)
```css
.cut {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
```
✅ A quick way to center anything (yes, literally anything)
```css
.center {
position: fixed;
inset: 0;
margin: auto;
}
```
✅ A responsive grid layout without media queries
```css
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
}
```
They’re compact.
They work across browsers.
And they’re perfect to keep in your snippets library.
Reusable snippets help most when grouped by intent, not by novelty. Organize them around tasks like spacing, truncation, and alignment, so they remain easy to reuse and review across different components.
Reusable snippets deliver most value when grouped by concrete tasks like spacing, truncation, and alignment. That makes them easier to adopt than a random list of isolated tricks.
Validate it with real content and realistic viewport ranges so implementation details hold up outside demos.
To roll this out safely, start by applying `reusable snippets` in a single UI surface where the benefit is obvious. Then reuse that same pattern in similar contexts so behavior stays consistent and review time stays low.
As a final check, run `reusable snippets` through edge cases like dense layouts, long labels, and constrained mobile widths.
Keep this pattern documented in your design system notes so future edits preserve the original intent and constraints.
A short internal cheat sheet with “when to use each snippet” helps teammates apply them consistently instead of copy-pasting blindly.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# CSS-Only Countdown From 10 to 0 Without JavaScript
URL: https://theosoti.com/short/css-countdown/
Published: 2025-05-26
Tags: CSS, modern CSS, CSS animations, performance, JavaScript alternatives, frontend development, CSS tip
> Create a pure CSS countdown from 10 to 0 using custom properties, keyframes, and pseudo-elements—no JavaScript required, with over 92% browser support.
## A CSS-only countdown.
No JavaScript needed.
This countdown from 10 to 0 is 100% CSS.
Here’s how it works:
1 - `@property` lets us define a custom CSS variable (--c) and can be animated.
2 - We animate --c from 10 to 0 using a `@keyframes` animation.
3 - Inside a `::after` pseudo-element, we use `counter-reset` to bind --c to a named CSS counter (count).
4 - Finally, we display the live-updating value using `content: counter(...)`.
No JavaScript. No `setInterval`. No updating the DOM.
Just clever use of CSS features that are finally starting to mature.
Browser support is pretty good with more than 92%
Here's a video presenting a countup in action: VIDEO
A CSS countdown is great for decorative timers and launch moments where strict time sync is not required. Keep it presentational, and pair it with semantic text when users need precise timing information.
CSS countdowns are ideal for decorative timers and promo moments where exact clock sync is not critical. Pair visual countdowns with explicit text when users need precise timing.
Validate it with real content and realistic viewport ranges so implementation details hold up outside demos.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Clean Up Your CSS with :is() and :where() Selectors
URL: https://theosoti.com/short/smarter-css-with-new-selectors/
Published: 2025-05-24
Tags: CSS, modern CSS, HTML forms, CSS selectors, CSS tip
> Simplify complex CSS selectors with :is() and :where() for cleaner styles and easier specificity management with broad browser support.
## Write smarter CSS with :is() and :where().
Writing CSS selectors can be tedious, especially when repeating long lists of elements.
Thankfully, :is() and :where() make things much cleaner.
Both of them let you group selectors, reducing redundancy and improving readability.
There is one key difference between the 2: specificity.
- :is() inherits the highest specificity from the elements inside it. If one of its child selectors has high specificity, the whole rule does too.
- :where() always has zero specificity. It won’t override other styles unless there’s no conflicting rule.
The best part ?
They are widely supported in all modern browsers.
With over 95% support!
These selectors clean up your CSS, make maintenance easier, and help avoid specificity battles.
Selector helpers are most valuable when they simplify real nesting and reduce repetition. Use them to flatten long chains early, so specificity remains intentional as the stylesheet grows.
Selector helpers are most helpful when they flatten deep selector chains. Use them to make intent obvious and to keep cascade maintenance manageable as stylesheets grow.
On form-heavy pages, this is most effective when you keep semantics native and style enhancements lightweight. That balance preserves accessibility and reduces maintenance cost.
When introducing `smarter css with new selectors`, begin with one reference component and treat it as the canonical pattern. That rollout sequence preserves clarity and reduces regressions during future refactors.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Level Up Text Styling with Advanced CSS Underlines
URL: https://theosoti.com/short/text-decoration/
Published: 2025-05-22
Tags: CSS, modern CSS, CSS colors, CSS typography, CSS tip
> Go beyond basic underlines with powerful CSS text-decoration properties like thickness, color, style, and offset. Fully supported and highly customizable.
## You probably know `text-decoration: underline;`.
But do you know what else it can do?
Most developers stop at text-decoration: underline;
But CSS has a whole toolbox of styling options hiding in plain sight.
Here are some underrated text-decoration properties you can be using:
- text-decoration-thickness: 5px;
Control the thickness of the line
- text-decoration-color: deeppink;
Change the line’s color (it can be animated independently of the text's color)
- text-decoration-style: solid | dashed | wavy;
Customize the line’s appearance
- text-decoration-skip-ink: auto | none;
Decide whether the line should skip descenders (like g/j/y)
- text-decoration-line: underline | overline | line-through;
Choose the position(s), or combine them
- text-underline-offset: 1em;
Offset the line for better spacing and clarity
Perfect for giving your typography more control and visual polish.
Bonus: it’s supported in all browsers.
Fine-tuning underline thickness, offset, and skip behavior improves legibility in dense text. This is especially useful for links inside paragraph content where default underlines can feel too heavy or too close to glyphs.
Underline controls can significantly improve link readability in dense text. Adjust thickness and offset to keep links clear without colliding with glyph descenders.
Test with multilingual strings and varied word lengths. Typographic CSS should remain graceful when content is unpredictable.
When introducing `text-decoration: underline;`, begin with one reference component and treat it as the canonical pattern. Then reuse that same pattern in similar contexts so behavior stays consistent and review time stays low.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Build a CSS Counter Without JavaScript
URL: https://theosoti.com/short/css-only-pokemon-counter/
Published: 2025-05-20
Tags: CSS, modern CSS, HTML forms, performance, JavaScript alternatives, frontend development, CSS tip
> Learn how to create a live counter using only CSS with properties like counter-reset, counter-increment, and content: counter(). No JS needed!
## Did you know you can build a counter using only CSS?
No JavaScript. Just native CSS features.
With a few underrated properties, you can make live counters in pure CSS:
- `counter-reset`: Initializes or resets a counter.
In this case, we start counting Pokémon from 0.
- `counter-increment`: Increases the counter whenever a condition is met.
Here, every time an input is checked, it bumps the count.
- `content: counter()`: Displays the current value of the counter.
It’s typically used inside `:before` or `:after` pseudo-elements.
In this example:
- Each selected Pokémon increases the count
- The total is displayed instantly
- Zero JS involved
With almost 96% browser support, it’s pretty safe to use!
Here is a live example: https://codepen.io/theosoti/pen/RNNjrQe
CSS counters are a strong teaching tool for state-like visuals without scripting. They work well for playful interactions, but production data that must persist or sync should still rely on application logic.
CSS counters are a great way to explore state-like visuals without JS. They are perfect for playful interfaces, while persistent business state should still live in application logic.
This pattern has the best payoff when documented with one clear usage rule. Consistent adoption turns small CSS features into reliable system behavior.
When introducing `counter-reset`, begin with one reference component and treat it as the canonical pattern. This keeps implementation predictable and prevents style drift as the codebase grows.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Write Cleaner CSS with Modern Properties
URL: https://theosoti.com/short/5-modern-properties/
Published: 2025-05-18
Tags: CSS, modern CSS, CSS selectors, CSS tip
> Discover 5 powerful CSS features that simplify your code, like inset, scale, :is(), and more. Writing less CSS has never been easier.
## Writing less CSS has never been easier.
Modern properties are doing some serious heavy lifting.
Here is a list of 5 CSS improvements
Instead of:
```css
div {
transform: scale(2);
}
```
Just write:
```css
div {
scale: 2;
}
```
Instead of:
```css
div {
top: 0;
right: 0;
bottom: 0;
left: 0;
}
```
Just write:
```css
div {
inset: 0;
}
```
And there's more:
- `:is()` simplifies selectors.
- `min()` or `max()` combines constraints.
- `margin-inline` or `margin-block` makes layouts more flexible.
All of these tweaks add up to cleaner CSS.
These modern properties shine with progressive enhancement. You can keep existing `transform` and positional fallbacks, then layer `scale`, `inset`, and logical properties on top for cleaner code.
Also prefer logical properties like `margin-inline` and `padding-block` early in a project. They make layouts friendlier to RTL languages and reduce refactoring when internationalizing later.
Modern properties like `inset`, `scale`, and logical spacing can clean up large codebases quickly. The gain is bigger when teams adopt them consistently, instead of mixing legacy and modern syntax at random.
Modern properties pay off when applied consistently across a codebase, not only in isolated examples. Establish clear usage conventions so teams avoid mixing old and new syntax unpredictably.
This pattern has the best payoff when documented with one clear usage rule. Consistent adoption turns small CSS features into reliable system behavior.
To roll this out safely, start by applying `:is()` in a single UI surface where the benefit is obvious. That rollout sequence preserves clarity and reduces regressions during future refactors.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Animate in CSS without @keyframes using @starting-style
URL: https://theosoti.com/short/animations-without-keyframes/
Published: 2025-05-16
Tags: CSS, modern CSS, CSS animations, JavaScript alternatives, frontend development, CSS tip
> Discover how @starting-style enables keyframe-free CSS animations—even on DOM entry. Cleaner syntax, less JS, and smoother UI transitions.
## Did you know you can animate without @keyframes?
The new `@starting-style` makes it possible.
It lets you define an element’s initial state.
CSS handles the transition from there.
- Simpler syntax
- Cleaner code
- Perfect for quick UI effects
But here’s the real magic:
You can now animate elements as they enter the DOM.
Think modals, tooltips, or anything that appears dynamically.
Before, CSS couldn’t animate them, it didn’t know their "before" state.
@starting-style changes that.
No more hacks. No more JS for basic transitions.
It’s supported in ~88% of browsers, so not production-ready yet.
Already tried it?
I’d love to hear how you’d use it.
A practical pattern is to combine it with transitions on `opacity` and `transform`, then define the entry state inside `@starting-style`. That gives you smooth mount animations without JS choreography.
Remember motion preferences too: wrap non-essential transitions in `@media (prefers-reduced-motion: no-preference)` so users who prefer reduced motion get a calmer experience.
`@starting-style` is especially helpful for entry transitions where elements mount dynamically. It keeps animation logic in CSS and removes common JS setup code for modals, popovers, and drawers.
`@starting-style` is especially useful for entry transitions on mounted elements like dialogs and popovers. It keeps animation behavior in CSS and reduces setup logic in JavaScript.
Motion should clarify state change, not distract from content. Keep timings short and ensure users can still complete actions quickly.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Simplify layouts with CSS display: contents
URL: https://theosoti.com/short/smarter-layouts-with-display-contents/
Published: 2025-03-30
Tags: CSS, modern CSS, CSS Grid, CSS layout, Flexbox, responsive design, CSS tip
> Use display: contents to restructure layouts without extra markup. Works with Flexbox & Grid for cleaner, more flexible designs.
## Did you know CSS has a hidden gem called display: contents?
It's a powerful tool that helps simplify layouts without adding extra DOM elements.
It removes an element from the layout while keeping its children intact.
## Why is this so powerful?
- It lets you restructure layouts without adding extra wrappers.
- It works seamlessly with Flexbox and Grid, making designs more flexible.
- It pairs perfectly with order to rearrange content without duplication.
On desktop, blocks 2 and 3 sit next to block 1 inside a container.
On mobile, block 2 moves above block 1, and block 3 drops below it.
Normally, this would require workarounds: extra markup, hacks, or even duplicated content.
With display: contents, it's just one line of CSS.
Browser support is already at 97%!
But be aware, this property has some accessibility issues, especially with screen readers.
```html
```
```css
section {
grid-template-columns: 1fr;
}
.container {
display: contents;
}
.first {
order: 2;
}
.second {
order: 1;
}
.third {
order: 3;
}
```
Full code available here: https://codepen.io/theosoti/pen/KwPZGQQ
`display: contents` can simplify wrapper-heavy markup in layout contexts, but semantics still matter. Verify accessibility tree behavior for interactive and labeled content before broad adoption.
Selector-driven improvements scale best when specificity remains controlled. Keep targeting rules readable so future edits do not become cascade puzzles.
To roll this out safely, start by applying `display: contents` in a single UI surface where the benefit is obvious. That rollout sequence preserves clarity and reduces regressions during future refactors.
Use `display: contents` selectively on structural wrappers, and keep semantic elements intact when labels or landmarks are required.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Create an inverted hover effect with CSS :has()
URL: https://theosoti.com/short/custom-hover-effect-with-has/
Published: 2025-03-28
Tags: CSS, modern CSS, CSS selectors, performance, JavaScript alternatives, CSS tip
> Achieve a smooth inverted hover effect using CSS :has()—no JavaScript, no hacks! Style parent elements dynamically with clean, native CSS.
## Have you ever wanted to create an inverted hover effect?
I'll show you how with just 2 extra lines of CSS!
No hacks.
No JavaScript.
Just the power of :has().
For years, CSS couldn't style parent elements based on their children.
But now, :has() changes the game.
Hover over an element, and :has() lets the parent react, applying styles to everything else.
- No extra HTML.
- No complex workarounds.
- Just a clean, native way to control interactions.
The result?
A smooth hover effect that shifts focus exactly where you want it.
And this is just one use case.
With :has() you can also:
- Build dark mode toggles.
- Improve form validation styles.
- Create advanced parent-child interactions.
- Enhance focus and selection effects.
- And more...
With over 93% browser support, it’s pretty safe to use.
```html
```
```css
.cardlist:has(.card:hover) .card:not(:hover) {
filter: blur(4px);
}
```
Full code available here: https://codepen.io/theosoti/pen/VwJryJK
`:has()` makes parent-reactive hover states much cleaner for cards and list items. Keep selectors focused on clear interaction zones, so the effect feels intentional and remains predictable across dense layouts.
`:has()` enables parent-reactive hover patterns that used to need JS or extra wrappers. Keep selectors focused and local so interactions stay predictable in dense component trees.
Selector-driven improvements scale best when specificity remains controlled. Keep targeting rules readable so future edits do not become cascade puzzles.
When introducing `:has()`, begin with one reference component and treat it as the canonical pattern. This keeps implementation predictable and prevents style drift as the codebase grows.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Improve text readability with CSS text-wrap
URL: https://theosoti.com/short/balanced-and-pretty-texts/
Published: 2025-03-26
Tags: CSS, modern CSS, CSS typography, CSS tip
> Say goodbye to awkward text wrapping! Use text-wrap: balance; for even lines and text-wrap: pretty; for better spacing.
## No more awkward text wrapping. Let CSS handle it beautifully.
CSS gives us two new ways to make typography more visually pleasing:
- text-wrap: balance;
- text-wrap: pretty;
## What’s the difference?
1 - text-wrap: balance;
It makes sure each line has a similar length, preventing one-word last lines or uneven spacing.
Perfect for headlines or short text blocks.
2 - text-wrap: pretty;
It optimizes letter and word spacing to create a more natural and visually appealing text flow.
Great for longer paragraphs where readability is key.
## Why should you care?
- No more weird line breaks.
- More control over how text is displayed.
- Better readability without extra HTML or JS.
Browser support is already at ~88%.
It’s not perfect, but since unsupported browsers just ignore it, there’s no downside.
Simple CSS tricks make a huge difference.
```css
.balanced {
text-wrap: balance;
}
.pretty {
text-wrap: pretty;
}
```
Full code available here: https://codepen.io/theosoti/pen/VYwBRBK
`text-wrap: balance` works best for short headings, while `text-wrap: pretty` improves long paragraphs by reducing awkward line endings. Choosing the mode per content type creates cleaner rhythm without manual line breaks.
`text-wrap: balance` works best on short headings, while `text-wrap: pretty` improves long paragraphs by reducing awkward endings. Using each mode intentionally gives cleaner rhythm across content types.
Use this as a readability tool, not only a visual effect. The best result is when style improves scanning speed without adding cognitive load.
For teams, `text-wrap: balance` is easiest to maintain when it starts in one documented example before broader reuse. This keeps implementation predictable and prevents style drift as the codebase grows.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Stylish drop caps in CSS with just two lines
URL: https://theosoti.com/short/stylish-drop-caps/
Published: 2025-03-24
Tags: CSS, modern CSS, CSS typography, CSS tip
> Make your text stand out with a simple CSS trick! Use :first-letter and initial-letter to create elegant drop caps effortlessly.
## Make your text pop with a single CSS trick.
Ever noticed those stylish drop caps in print media?
You can achieve the same effect in CSS easily.
The magic?
Just two lines of CSS:
```css
p:first-letter {
initial-letter: 3 2;
}
```
- :first-letter targets the first character of a paragraph.
- initial-letter: 3 2; pushes text aside by two lines, creating a balanced look
This is perfect for giving articles, blogs, or long-form content a more refined and readable look.
Browser support is great for :first-letter with more than 97,5%.
For initial-letter it's good but not great with more than 91%.
Full code available here: https://codepen.io/theosoti/pen/PwoaJNP
For robust typography, combine `initial-letter` with a visual fallback using `float`, `line-height`, and margin. Browsers that support `initial-letter` get the ideal layout, while others still show a clean decorative first letter.
Keep accessibility in mind: large ornate drop caps can hurt readability on narrow screens. Reduce the size on mobile and verify line wrapping with short and long first words.
Drop caps look better when size, line-height, and paragraph spacing are tuned together. Keep the effect moderate on small screens so decorative typography adds tone without hurting reading flow.
Drop caps work best when size, spacing, and line-height are tuned together. Keep the effect restrained on mobile so decorative typography does not disrupt reading flow.
Typography refinements are most visible with real content length and mixed wording. Validate rhythm, spacing, and line breaks on mobile where small issues become obvious.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Smooth scrolling with CSS for clean, JS-free navigation
URL: https://theosoti.com/short/smooth-scrolling-css-only/
Published: 2025-03-21
Tags: CSS, modern CSS, scroll-driven animations, CSS animations, performance, JavaScript alternatives, CSS tip
> Enhance user experience with scroll-behavior: smooth in CSS—no JavaScript needed! Learn how to fix abrupt jumps and improve readability with scroll-margin.
## Smooth scrolling makes navigation feel effortless.
When users click an anchor link, the browser jumps abruptly to the section.
This can be jarring, especially on long pages.
Instead of relying on JavaScript for smooth scrolling, CSS has a built-in solution: “scroll-behavior: smooth”.
With just this, your page transitions fluidly between sections.
But what if the content feels too close to the edge?
By default, scrolling stops at the very top of the section, sometimes cutting off important content.
Fix this with `scroll-padding` or `scroll-margin`: scroll-margin-top: 20px;
Now, every scroll lands in a comfortable, readable position.
It’s lightweight, easy to implement, and widely supported (more than 95,5%).
```css
html {
scroll-behaviour: smooth;
}
section {
scroll-margin-top: 20px;
}
```
Smooth anchor navigation feels best when paired with `scroll-margin-top` on headings, especially under sticky headers. Also account for reduced-motion preferences so users who disable motion still get direct navigation.
Smooth anchor navigation is strongest when paired with heading offsets. Combine `scroll-behavior` with `scroll-margin-top` so anchors land cleanly under sticky headers.
Run interaction tests with rapid clicks and navigation changes. Input should stay responsive even while transitions are active.
To roll this out safely, start by applying `scroll-padding` in a single UI surface where the benefit is obvious. After validation, expand gradually to matching components and avoid ad-hoc one-off overrides.
Before shipping, test `scroll-padding` with both short and long content, then verify behavior in narrow and wide containers.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# CSS Text Gradients: Stunning Effects in 3 Lines
URL: https://theosoti.com/short/texts-gradients/
Published: 2025-03-14
Tags: CSS, modern CSS, CSS gradients, CSS typography, performance, frontend development, CSS tip
> Enhance your typography with CSS text gradients! No images, no extra elements—just pure CSS magic for bold, eye-catching UI design.
## Good design is in the details.
A simple text gradient can transform your UI.
Flat colors are great, but gradients bring extra depth and personality to your typography.
With just three lines of CSS, you can turn any text into a gradient without using images or extra elements.
## How it works?
- A linear gradient sets the background with a smooth transition between colors.
- The background-clip: text; property ensures the gradient follows the text shape.
- Setting color: transparent; hides the original text color, making the gradient visible.
## Why use this technique?
- It’s a lightweight solution that doesn’t require additional elements.
- The effect works on any font size and adapts seamlessly to different layouts.
- You can create endless variations by adjusting colors and angles.
Perfect for headlines and UI elements that need an extra visual impact.
Browser support is great.
Over 97% of users can see the effect!
And if you want to go even further, you can animate it with @property variables and keyframes animation.
```html
Theosoti.com
For best CSS content
```
```css
article {
background: linear-gradient(to right, deeppink, orange);
background-clip: text;
color: transparent;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
```
Full code available here: https://codepen.io/theosoti/pen/ogNoRPp
Gradient text effects should preserve readability first. Tune color stops and contrast against the page background so the visual style stays expressive without reducing legibility.
Typography refinements are most visible with real content length and mixed wording. Validate rhythm, spacing, and line breaks on mobile where small issues become obvious.
For teams, `texts gradients` is easiest to maintain when it starts in one documented example before broader reuse. After validation, expand gradually to matching components and avoid ad-hoc one-off overrides.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# CSS-Powered Before/After Slider with Minimal JS
URL: https://theosoti.com/short/comparison-slider-in-css/
Published: 2025-03-12
Tags: CSS, modern CSS, CSS Grid, CSS layout, CSS shapes, JavaScript alternatives, CSS tip
> Create a before/after image slider with just 1 line of JavaScript! Learn how CSS masking and grid make it efficient, simple, and widely supported.
## Before/after slider with just 1 line of JavaScript?
Yes, it’s possible!
Most comparison sliders rely on extra JavaScript,
But CSS can do most of the work for us.
## How does it work?
- Both sections and the slider are placed in the same grid cell, ensuring perfect overlap.
- A mask on each section controls its visibility. As the slider moves, one section gradually disappears while the other is revealed.
- The range input updates the mask position dynamically, creating a seamless effect.
## Why use this approach?
- No need for complex JavaScript logic.
- CSS handles most of the work efficiently.
- A simple structure with clear separation of concerns.
- Over 95% browser support!
This technique is perfect for image comparisons, UI demos, and interactive before/after effects.
```html
```
```css
.compare {
display: grid;
> * {
grid-area: 1 / 1;
}
}
section {
display: grid;
align-items: center;
justify-content: center;
}
.before {
background-color: #fff8f0;
mask: linear-gradient(to right, #000 0, var(--pos, 50%), transparent 0);
}
.after {
background-color: #122b1f;
mask: linear-gradient(to right, transparent 0, var(--pos, 50%), #000 0);
color: #fff8f0;
}
```
```js
range.oninput = () => document.body.style.setProperty('--pos', range.value + '%');
```
Full code available here: https://codepen.io/theosoti/pen/JojrrgB
For before/after sliders, clarity matters more than novelty. Keep handles discoverable, maintain enough contrast between states, and ensure users can understand the comparison without precise drag control.
A quick edge-case audit with inserted elements helps confirm that selector logic stays predictable over time.
For teams, `comparison slider` is easiest to maintain when it starts in one documented example before broader reuse. That rollout sequence preserves clarity and reduces regressions during future refactors.
Before final rollout, validate `comparison slider` against real content and real interaction states, not only demo text.
If you keep a visible divider and clear labels for both states, users understand the comparison instantly without trial-and-error dragging.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# CSS Anchor Positioning: Simplify Dynamic UI Layouts
URL: https://theosoti.com/short/css-anchor-positioning/
Published: 2025-03-10
Tags: CSS, modern CSS, CSS layout, responsive design, JavaScript alternatives, frontend development, CSS tip
> Learn how CSS Anchor Positioning makes UI layout easier by allowing elements to be positioned dynamically—no more complex JS or layout shifts!
## Positioning UI elements just got easier.
Meet CSS Anchor Positioning!
Positioning elements dynamically has always been a hassle.
Absolute positioning, JavaScript calculations, and unexpected layout shifts...
But the new Anchor Positioning makes everything better!
## How does it work?
- Define an element as an anchor using `anchor-name`
- Then position another element relative to it with `anchor()`.
No more guesswork, just native, flexible, and precise positioning.
## Some real-world use cases:
- Tooltips that stay perfectly attached to their trigger.
- Dropdown menus that align dynamically with their button.
- Modals or popovers that position relative to an element instead of the whole viewport.
- Notifications that stick near the related content.
What about browser support?
Not great for now, with only a bit more than 71%.
It doesn’t work on firefox and safari.
With features like this, CSS is proving once again that it's becoming more powerful than ever!
```html
Anchor
I'm a great notice!
```
```css
.anchor-button {
anchor-name: --anchor-el;
}
.notice {
position-anchor: --anchor-el;
position: absolute;
bottom: anchor(top);
justify-self: anchor-center;
margin: 0.5rem 0;
}
```
You can find the codepen link here: https://codepen.io/theosoti/pen/JojyajY
Anchor positioning is great for tooltips, menus, and notices tied to a trigger. It reduces manual offset calculations and keeps overlay placement attached to the relevant UI element.
Layout features like this provide real value when components move between narrow and wide containers. Testing nested contexts early prevents brittle breakpoint rules.
For teams, `anchor-name` is easiest to maintain when it starts in one documented example before broader reuse. After validation, expand gradually to matching components and avoid ad-hoc one-off overrides.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Scroll-Linked CSS Progress Bar Without JavaScript
URL: https://theosoti.com/short/create-progress-bar-css-only/
Published: 2025-03-07
Tags: CSS, modern CSS, scroll-driven animations, CSS animations, HTML forms, performance, JavaScript alternatives
> Create a scroll-synced progress bar using pure CSS and animation-timeline—no JavaScript needed. Lightweight, and performant.
## Use only CSS for progress bar
A progress bar is a perfect example of what's possible with modern CSS.
It leverages the power of animation-timeline: scroll();, which links the animation's progress directly to your scroll position.
As you scroll down the page, the progress bar fills up in perfect sync with your movement.
It's a subtle but effective way to enhance the user experience.
CSS keyframes also play a crucial role, defining the animation's behavior.
In this case, a simple width change from 0 to 100%.
The result?
A smooth, natural animation that feels intuitive and performant.
Plus, CSS animations are:
- Performant (hardware-accelerated)
- Accessible
- Great for reducing JavaScript dependencies
The only downside for now is the browser support.
The support for animation-timeline is currently around 73%.
I talk about CSS scroll animations in one of my blog posts.
If you want to deep dive into this subject, you can check it out here: https://lnkd.in/eX-tmD6V
```html
```
```css
.progress {
background: #55ad9b;
width: 0;
height: 8px;
animation: grow linear forwards;
animation-timeline: scroll();
}
@keyframes grow {
to {
width: 100%;
}
}
```
VIDEO
Scroll-linked progress is useful when it reflects real reading position and does not distract. Keep the indicator thin and calm, then verify that long and short articles produce consistent feedback.
This pattern has the best payoff when documented with one clear usage rule. Consistent adoption turns small CSS features into reliable system behavior.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Create custom form validation with CSS only
URL: https://theosoti.com/short/form-validation-css-only/
Published: 2025-03-05
Tags: CSS, modern CSS, HTML forms, performance, JavaScript alternatives, CSS tip
> Validate forms with CSS! Use :valid and :user-invalid pseudo-classes for dynamic input styling without JavaScript. Enhance UX effortlessly.
## Validate forms with just CSS? Yes, it's possible!
Form validation is often handled with JavaScript, but did you know CSS can do a lot of the heavy lifting?
It works thanks to 4 built-in pseudo classes:
- `:valid` → The input meets all constraints.
- `:invalid` → The input fails validation.
- `:user-valid` → The input is valid and the user has interacted with it.
- `:user-invalid` → The input is invalid but only after the user typed something and left the field.
You can also define validation rules directly in HTML using attributes like required, type, and pattern.
CSS selectors combined with these pseudo-classes allow you to style form elements based on their validation state.
You can show or hide messages, change input borders, or visually guide users as they type.
Regarding browser support:
- `:valid` & `:invalid` have more than 96%
- `:user-valid` & `:user-invalid` have a bit more than 90%
This approach isn’t a full replacement for JavaScript validation, but it’s a powerful way to enhance forms with minimal effort.
Codepen link: https://codepen.io/theosoti/pen/ogNZzdJ
```html
```
```css
input:user-valid {
--state-color: green;
}
input:user-invalid {
--state-color: red;
}
input:valid + .validation [data-matches="valid"],
input:invalid + .validation [data-matches="invalid"] {
display: block;
}
input:user-valid + .validation [data-matches="user-valid"],
input:user-invalid + .validation [data-matches="user-invalid"] {
display: block;
}
input {
--state-color: black;
```
CSS validation patterns are excellent for immediate feedback, but they should complement real form validation logic. Keep messages clear and state changes gentle so users understand how to recover quickly.
For production forms, test keyboard order, disabled states, and validation messages with realistic labels. Native behavior should remain intact while visual polish improves.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# CSS Gets Dynamic with the New attr() Function
URL: https://theosoti.com/short/evolved-attr-function/
Published: 2025-03-03
Tags: CSS, modern CSS, experimental CSS, JavaScript alternatives, CSS tip
> Explore the enhanced CSS attr() function in Chrome 133—now supporting all properties and data types for more dynamic, JS-free styling possibilities.
## CSS just got smarter!
Meet the upgraded attr() function.
Chrome 133 introduces a significant enhancement to the CSS attr().
It can now be used with any CSS property, including custom properties, not just content.
It also supports multiple data types like colors, lenghts, IDs, and more, instead of being limited to strings.
This unlocks tons of possibilities to make CSS more dynamic—without a single line of JavaScript!
Some cool examples:
- Dynamically change text color based on an HTML attribute.
- Simplify visual transitions effortlessly.
- Adapt styles based on metadata
⚠️ Currently, this feature is available ONLY in Chrome 133.
So don’t use it in prod just yet!
The revamped attr() will make CSS even more powerful.
I can’t wait to use it safely in my projects!
Example 1:
```html
test
```
```css
div {
color: attr(data-foo type(), red);
}
```
Example 2:
```html
```
```css
.card {
/* card-1, card-2, card-3, etc. */
view-transition-name: attr(id type(), none);
view-transition-class: card;
}
```
Example 3:
```html
test
```
```css
div {
font-size: attr(data-size px);
}
```
The newer `attr()` usage is useful when values can come from semantic attributes, reducing duplicated constants in CSS. It is most effective when attribute data is stable and validated.
This pattern has the best payoff when documented with one clear usage rule. Consistent adoption turns small CSS features into reliable system behavior.
A practical way to adopt `attr()` is to scope it to one high-impact component first. Then reuse that same pattern in similar contexts so behavior stays consistent and review time stays low.
Before final rollout, validate `attr()` against real content and real interaction states, not only demo text.
Keep fallback values explicit when reading attributes in CSS so missing data never produces unreadable or broken UI output.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Create a responsive grid layout with just 1 line of CSS
URL: https://theosoti.com/short/one-liner-responsive-layout/
Published: 2025-02-28
Tags: CSS, modern CSS, CSS Grid, CSS layout, responsive design, CSS tip
> Responsive layouts in one line of CSS? Discover the power of flexible, media-query-free design for seamless adaptation to any screen.
## Achieve responsive layouts now with one line of CSS
Let’s analyse the magic behind grid-template-columns:
1. Repeat()
- repeat(x, value): generates multiple columns or rows with the same pattern.
- auto-fit: automatically adjusts the number of columns based on available space.
2. minmax(min(100%, 18.75rem), 1fr)
- min(100%, 18.75rem): ensures the column never exceeds 18.75rem but also doesn’t exceed the container’s width.
- minmax(..., 1fr): Sets a minimum size while allowing columns to grow and evenly fill available space.
Why this work so well?
- No media queries needed.
- Thanks to min(100%, 18.75rem), elements won’t exceed their container.
- Automatically adapts to different screen sizes and layouts.
- One line does all the work!
All these functions (repeat, minmax, min) have more than 96% browser support.
This one-liner is perfect for card layouts, galleries, and grids that need to be both structured and flexible.
Have you used this approach or a similar one before?
Let’s discuss in the comments!
```html
```
```css
.container {
display: grid;
gap: 1rem;
/* this line does it all */
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18.75rem), 1fr));
}
.card {
height: 12.5rem;
background: #fff;
border: 4px solid #000;
}
```
Codepen link: https://codepen.io/theosoti/pen/WbNomqr
One-line responsive grids are ideal for content collections where item count changes often. The value is not just shorter CSS; it is predictable wrapping behavior with fewer breakpoint-specific overrides.
Layout features like this provide real value when components move between narrow and wide containers. Testing nested contexts early prevents brittle breakpoint rules.
A practical way to adopt `minmax()` is to scope it to one high-impact component first. Then reuse that same pattern in similar contexts so behavior stays consistent and review time stays low.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Select a range of elements without classes
URL: https://theosoti.com/short/select-range-of-element/
Published: 2025-02-24
Tags: CSS, modern CSS, HTML forms, CSS selectors, CSS tip
> Target a precise range of elements with :nth-child(). Learn how to combine selectors to style specific segments of a list—no extra classes needed.
## Unlock the power of :nth-child() to target elements
But did you know you can use it to select an entire range of elements?
Instead of styling every third item or just the odd/even ones, you can define a precise segment.
This works by combining two `:nth-child()` selectors:
- `nth-child(n+4)`: Selects everything from the 3rd item onward.
- `nth-child(-n+7)`: Selects everything up to the 7th item.
Together, they create a flexible way to target only a specific range.
This trick is perfect for highlighting content or dynamically styling sections of a list.
Next time you need to style a group of elements, without adding extra classes, think about `:nth-child()`.
Range targeting is especially useful for featured windows in lists, like highlighting items 4 to 7 in a feed. The pattern is easy to adjust: change the first selector start index and the second selector end index.
If your list mixes different element types, prefer `:nth-of-type()` to avoid surprises when non-matching nodes are inserted. It keeps the range predictable as content evolves.
This range pattern is great for featured windows in lists, timelines, or product grids. You can move the highlighted segment by changing only two numbers, which keeps HTML clean and avoids class-heavy markup.
Range targeting with `:nth-child()` helps highlight windows of items without adding classes. It is useful in timelines, ranked lists, and featured groups where boundaries change over time.
On form-heavy pages, this is most effective when you keep semantics native and style enhancements lightweight. That balance preserves accessibility and reduces maintenance cost.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Write smarter CSS with 2 new selectors
URL: https://theosoti.com/short/css-new-selectors/
Published: 2025-02-21
Tags: CSS, modern CSS, HTML forms, CSS selectors, CSS tip
> Writing CSS selectors can be tedious, especially when repeating long lists of elements. Thankfully, :is() and :where() make things much cleaner.
## Tired of writing long, tedious CSS selectors?
Thankfully, `:is()` and `:where()` make things much cleaner.
Both of them let you group selectors, reducing redundancy and improving readability.
There is one key difference between the 2: specificity.
- `:is()` inherits the highest specificity from the elements inside it. If one of its child selectors has high specificity, the whole rule does too.
- `:where()` always has zero specificity. It won’t override other styles unless there’s no conflicting rule.
The best part ?
They are widely supported in all modern browsers.
With over 95% support!
These selectors clean up your CSS, make maintenance easier, and help avoid specificity battles.
In practice, `:where()` is ideal for base styles and component resets because its zero specificity stays easy to override. Reserve `:is()` for situations where you still need normal cascade weight.
A good pattern is `:where(article, section, aside) h2` for defaults, then targeted component classes for variations. This keeps selectors short without creating specificity debt.
`:is()` and `:where()` both reduce selector repetition, but specificity is the key difference. Use `:where()` for low-specificity defaults, and keep `:is()` for component rules where matching normal cascade weight is useful.
Use `:where()` for low-specificity defaults and keep `:is()` for grouped rules that should keep normal cascade weight. That split keeps selectors short while avoiding specificity fights later.
On form-heavy pages, this is most effective when you keep semantics native and style enhancements lightweight. That balance preserves accessibility and reduces maintenance cost.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# New Viewport Units for Improved Mobile Design
URL: https://theosoti.com/short/css-new-viewport-unit/
Published: 2025-02-19
Tags: CSS, modern CSS, CSS layout, responsive design, frontend development, CSS tip
> Prevent 100vh layout shifts with new CSS viewport units (svh, lvh, dvh). Build smoother, more reliable mobile designs with dynamic toolbars.
## For years, `vh` and `vw` were our go-to viewport units.
But mobile browsers’ dynamic toolbars made `100vh` unreliable, causing layout shifts.
Now, CSS gives us better control with new viewport units.
- `svh` / `svw` → Small Viewport (when toolbars are fully visible)
- `lvh` / `lvw` → Large Viewport (when toolbars are fully collapsed)
- `dvh` / `dvw` → Dynamic Viewport (adjusts in real time as the UI changes)
With these units, layouts can adapt fluidly without sudden jumps when the browser UI changes.
Need a full-height section?
Use `100dvh`, and it’ll always fit.
Want a consistent design even when toolbars disappear?
`lvh` has you covered.
The browser support is already solid at ~95%, so it’s pretty safe to use.
The new viewport units are most useful on mobile screens with dynamic browser bars. Use `dvh` for live viewport behavior, and keep `svh` for stable areas like heroes where jumpy resizing would feel distracting.
The new viewport units solve real mobile issues caused by browser UI bars. Use `dvh` when you want live viewport behavior and prefer `svh` when you want a stable, non-jumpy section height.
Layout features like this provide real value when components move between narrow and wide containers. Testing nested contexts early prevents brittle breakpoint rules.
A practical way to adopt `svh` is to scope it to one high-impact component first. Then reuse that same pattern in similar contexts so behavior stays consistent and review time stays low.
For fullscreen sections on mobile, compare `svh`, `lvh`, and `dvh` side by side so you can choose the unit that matches the interaction you want.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# How CSS subgrid fixes alignment issues for good
URL: https://theosoti.com/short/css-subgrid-layout/
Published: 2025-02-17
Tags: CSS, modern CSS, CSS Grid, CSS layout, responsive design, frontend development, CSS tip
> Master CSS Subgrid for perfect layout alignment! Build consistent pricing cards, blog listings, and dashboards with ease.
## Subgrid is your new best friend.
Instead of each item defining its own layout, it inherits the parent’s grid structure.
That means:
- No more inconsistent heights.
- No more manually tweaking spacing.
- Everything stays perfectly aligned.
To make it work, it’s simple:
- Set `display: grid` on the child element.
- Use `grid-template-rows: subgrid` or `grid-template-columns: subgrid`.
- Use `grid-row: span 3;` (or equivalent) to stretch items across multiple tracks within the parent grid.
- The child now follows the parent’s grid.
In the pricing card example, all sections stay aligned across all cards, no extra hacks needed.
Subgrid is great in component-based layouts:
- Blog listings where titles, excerpts, and metadata align.
- Feature grids with perfectly balanced headings and descriptions.
- Dashboards where stats and labels need precise positioning.
- Forms with consistent input and label alignment.
Subgrid is reliable for most modern browsers with 91.51% support.
Some older browsers might need fallbacks, so use it as progressive enhancement where needed.
Here is the code:
```html
First plan - super long title
Content
Super Cheap
Best plan
Content
Great Value
...
```
```css
.container {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 1rem;
}
.card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 3;
}
```
`subgrid` is most useful when child elements must align with a parent track system. It removes repeated grid definitions and keeps vertical rhythm stable across cards and editorial layouts.
Layout features like this provide real value when components move between narrow and wide containers. Testing nested contexts early prevents brittle breakpoint rules.
For teams, `display: grid` is easiest to maintain when it starts in one documented example before broader reuse. After validation, expand gradually to matching components and avoid ad-hoc one-off overrides.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# Great visuals with mask effects
URL: https://theosoti.com/short/css-mask-effects/
Published: 2025-02-15
Tags: CSS, modern CSS, CSS animations, CSS shapes, CSS tip
> With mask-image, you can create ink effects, smooth transitions, and elegant content reveals, without heavy graphics or extra elements.
## One property. Endless creative possibilities. Let’s explore CSS masks.
CSS masks control an element’s opacity using a shape, gradient, or image.
It even works with GIFs!
It results in smoother, more creative effects, all with pure CSS.
## How does it work?
1. mask-image: defines the image or gradient used as a mask.
2. mask-size: controls how it fits the element.
3. mask-position: determines its placement.
## Why is it powerful?
With mask-image, you can create ink effects, smooth transitions, and elegant content reveals, without heavy graphics or extra elements.
And It already has more than 96% browser support.
```html
```
```css
.banner:before {
content: '';
position: absolute;
inset: 0;
background-image: url('image.jpeg');
background-size: cover;
background-position: top;
z-index: -1;
mask-image: url(mask.png);
mask-size: cover;
mask-position: center;
}
```
Codepen link: https://codepen.io/theosoti/pen/GgRgMMj
Mask effects are strongest when they guide attention, not when they hide too much content. Start with simple gradient masks, then increase complexity only if shape readability stays clear in both light and dark contexts.
Mask effects should guide attention, not hide content. Start with simple gradients and test on different backgrounds to make sure the visual style remains readable and doesn’t reduce perceived contrast.
Media-related CSS needs testing across different asset ratios and resolutions. Real image variance is where composition rules are truly validated.
A practical way to adopt `mask-image` is to scope it to one high-impact component first. Then reuse that same pattern in similar contexts so behavior stays consistent and review time stays low.
Also test masked elements against both solid and textured backgrounds to confirm the focal area stays obvious in every theme.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!
---
# New way of declaring media queries
URL: https://theosoti.com/short/css-container-queries/
Published: 2025-02-13
Tags: CSS, modern CSS, container queries, responsive design, CSS layout, CSS tip
> CSS container queries let elements adapt to their container, not just the viewport. Making layouts more flexible and reusable.
## Are you still relying only on media queries for responsive design?
Let me show you another way...
...The CSS container queries!
Instead of adjusting elements based on the viewport, they adapt to the size of their container.
This makes your layouts more flexible, reusable, and predictable.
It’s in general used for component-based design.
## How it works:
- Define a `container-name` (acts as a reference).
- Set a `container-type` (to respond to width or height).
- Use `@container` to apply styles based on the container’s size.
## With this new way, a new set of measurement units is introduced:
1. `cqw`: % of container width
2. `cqh`: % of container height
3. `cqi`: % of the inline dimension of the container
4. `cqb`: % of container block dimension
5. `cqmin`: the smaller value between cqi and cqb
6. `cqmax`: the larger value between cqi and cqb
With container queries, components resize based on their container, not the viewport.
This means they behave consistently across different layouts, making your designs more adaptable.
You also get more control over how elements react to their immediate environment.
And with 93% browser support, they’re ready for production.
```html
```
```css
.container {
container: example / inline-size;
}
.resize {
background-color: #065f46;
}
@container example (width < 20ch) {
.resize {
background-color: #fc5757;
}
}
@container example (width > 30ch) {
.resize {
background-color: #27ae60;
}
}
```
Codepen link: https://codepen.io/theosoti/pen/qBzybLP
Container queries are strongest when the same component appears in multiple layout slots. Defining behavior by container width, not viewport width, keeps modules reusable in sidebars, cards, and full-width sections.
Layout features like this provide real value when components move between narrow and wide containers. Testing nested contexts early prevents brittle breakpoint rules.
---
If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.
Go check it out https://theosoti.com/you-dont-need-js/ and enjoy 20% OFF for a limited time!