RSS FeedTwitterMastodonBlueskyShare IconHeart IconGithub IconArrow IconClock IconGUI Challenges IconHome IconNote IconBlog IconCSS IconJS IconHTML IconShows IconGit IconSpeaking IconTools IconShuffle IconNext IconPrevious IconCalendar IconCalendar Edit Icon
A headless phantom in a graveyard
Evil cyborg Adam9 min read

Headless, boneless, skinless & lifeless UI

css

UI abstractions continue to evolve year over year. Let's talk about a couple of them, what they do, a little about why, and tease them a bit with silly names.

Folks don't say skinless, boneless or lifeless. I'm teasing these strategies, but also, I think these labels may make more sense than how we talk about them now.

๐ŸŽƒ

I won't be covering components with heads, bodies, skin and bones, like Chakra, ShadCN UI, Radix or MUI. These "fully loaded" / batteries included libraries have their own mix of abstractions for bones, skin and life.

This post is interested in the layered abstractions the front-end community is using so they can compose the layers themselves; moving quickly but with high customizability, to build their own design systems or fulfill their own unique application logic.

Terms #

Here's a quick overview of the terms and what I mean when I say them.

Headless
Working components; BYO (bring your own) style.
Boneless
Just styles; BYO bones and life.
Skinless
Just the markup; BYO skin and life.
Lifeless
Just the life; BYO bones and skin.
๐Ÿ’€

Headless UI #

This is the one term in this post that is common jargon amongst front-end developers. There's even a whole library that tries to embody (heh, see what I did there) the category: HeadlessUI.

This is what a headless UI offering looks like in code:

import { Popover, PopoverButton, PopoverPanel } from '@headlessui/react'

function Example() {
  return (
    <Popover className="relative">
      <PopoverButton>Solutions</PopoverButton>
      <PopoverPanel anchor="bottom" className="flex flex-col">
        <a href="/analytics">Analytics</a>
        <a href="/engagement">Engagement</a>
        <a href="/security">Security</a>
        <a href="/integrations">Integrations</a>
      </PopoverPanel>
    </Popover>
  )
}

Components bundled with functionality: bones and minimal style.

Headless doesn't quite match what you get IMO, since these totally have a brain, muscle and structure. This mismatch is what led me to boneless, skinless and lifeless terms.

Here's an incomplete list of headless libraries:

Boneless UI #

This abstraction layer is all about composable styles. Style without markup, skin without bones. On their own, boneless libraries don't do much, but when applied to bones you can compose nice UI.

Here's some Tailwind, where the libraries provided skin is applied to some bones.

<div class="lg:flex lg:gap-x-12">
  <a href="#" class="text-sm font-semibold leading-6 text-gray-900">Product</a>
  <a href="#" class="text-sm font-semibold leading-6 text-gray-900">Features</a>
  <a href="#" class="text-sm font-semibold leading-6 text-gray-900">Marketplace</a>
  <a href="#" class="text-sm font-semibold leading-6 text-gray-900">Company</a>
</div>

The value these libraries provide is a system, consistency, or premade combinations that aid in making beautiful UI.

Here's an incomplete list of boneless ๐Ÿ›น libraries:

Skinless UI #

This is like headless UI but is unstyled; functioning bones. No brains, no heart, no skin. While checking out headless UI libraries, if they're unstyled, I'd put them in this category.

This is what a skinless popover UI offering from React Aria looks like in code:

import {Button, Dialog, DialogTrigger, Heading, OverlayArrow, Popover, Switch} from 'react-aria-components';

<DialogTrigger>
  <Button>Settings</Button>
  <Popover>
    <OverlayArrow>
      <svg width={12} height={12} viewBox="0 0 12 12">
        <path d="M0 0 L6 6 L12 0" />
      </svg>
    </OverlayArrow>
    <Dialog>
      <div className="flex-col">
        <Switch defaultSelected>
          <div className="indicator" /> Wi-Fi
        </Switch>
        <Switch defaultSelected>
          <div className="indicator" /> Bluetooth
        </Switch>
        <Switch>
          <div className="indicator" /> Mute
        </Switch>
      </div>
    </Dialog>
  </Popover>
</DialogTrigger>

The libraries often offer CSS starting places or helper layouts and positioning, but the goal is to let you have full styling control. You can even see little bits of styling inline in the bones, hinting or providing meaningful minimal defaults as to the styling you'll likely use.

Here's an incomplete list of skinless libraries:

Lifeless UI #

Another category that would usually get lumped into headless, but I feel there's a more precise way to describe a subset of these headless kits. Like spirits, phantoms or ghosts that "know stuff" but can't actually manifest it in the real world, so are lifeless UI abstractions.

๐Ÿ‘ป

TanStack describes their lifeless offerings as:

  1. typesafe hooks
  2. doesn't render or supply any actual UI elements
  3. maximum inversion of control in mind

It's on us developers to wire up these functions to appropriate elements and styles. As if we're inserting a spirit into a body of bones and skin.

This is what a TanStack range input lifeless UI offering looks like in code:

import React from 'react'
import { createRoot } from 'react-dom/client'
import { useRanger, Ranger } from '@tanstack/react-ranger'

function App() {
  const rangerRef = React.useRef<HTMLDivElement>(null)
  const [values, setValues] = React.useState<ReadonlyArray<number>>([
    10, 15, 50,
  ])

  const rangerInstance = useRanger<HTMLDivElement>({
    getRangerElement: () => rangerRef.current,
    values,
    min: 0,
    max: 100,
    stepSize: 5,
    onChange: (instance: Ranger<HTMLDivElement>) =>
      setValues(instance.sortedValues),
  })

  return (
    <div className="App">
      <div ref={rangerRef}>
        {rangerInstance
          .handles()
          .map(
            (
              {
                value,
                onKeyDownHandler,
                onMouseDownHandler,
                onTouchStart,
                isActive,
              },
              i,
            ) => (
              <button
                key={i}
                onKeyDown={onKeyDownHandler}
                onMouseDown={onMouseDownHandler}
                onTouchStart={onTouchStart}
                role="slider"
                aria-valuemin={rangerInstance.options.min}
                aria-valuemax={rangerInstance.options.max}
                aria-valuenow={value}
              />
            ),
          )}
      </div>
    </div>
  )
}

Behaviors attached to elements, but made easy because the logic is provided.

Here's an incomplete list of lifeless libraries:

Outro #

A timely for the month, a tongue-in-cheek abstract article about ghastly abstractions.

Theo found the article and does a spoopy read of it on their channel:

Super appreciate the kind words Theo!

And as Nils said, this post doesn't even cover how much JS work that HTML and CSS are covering: popover, anchor, dialog, view transitions, scroll driven animation and custom select!

๐Ÿฆ‡

Mentions #

Join the conversation on

26 likes
9 reposts
  • โ€  lucia scarlet ????
  • Erik
  • citriqa
  • BoteComm
  • Konstantin Denerz
  • hoodie????
  • Fogrew :verified: ????????
  • Brett Peary
this texture goes so hard
โ€  lucia scarlet ????โ€  lucia scarlet ????

@argyleink love these new definitions ????

Brett PearyBrett Peary

Crawl the CSS Webringโ€ฝ