Components - Skeleton

Skeleton

  1. resources
  2. contribute
  3. components

Components

Guidelines for contributing new Skeleton components.

Dev Server

To run all packages/playgrounds/sites in watch mode, use the following command in the monorepo root:

Terminal window
pnpm dev

If you wish to focus on a specific framework you may run the framework-specific dev command:

Terminal window
pnpm dev:{framework}

This will limit the monorepo to only running the framework package (and its dependencies) and the corresponding playground for the specified framework and thus reduce resource usage and console bloat.

Server Ports

The following represents the default localhost address and port for each project. This will be displayed in the terminal when starting each dev server.

  • Documentation Site: http://localhost:4321/
  • Svelte Playground: http://localhost:5173/
  • React Playground: http://localhost:3000/

You may run the sites and playgrounds in parallel at the same time. If a server shares a port, this will increment by one for the next server (ex: 5174, 5175, etc). Keep your eye on the terminal to retrieve the specific local address for each.

Zag.js

Skeleton components are built using a foundation of Zag.js. This provides a suite of headless component primitives that handle logic and state, while providing a universal set of features per all supported frameworks. The Skeleton design system is then implemented as a layer on top of this.

When introducing a new Skeleton component, please refer to the documentation for each respective framework. For example:

Continue reading below to learn how implement the Zag primitives as components using Skeleton-specific conventions.


Adding Components

Anatomy

When creating a component, start by breaking it down into its core parts. If the component utilizes a Zag primitive, you may copy the source directly from Zag’s Usage section. For custom in-house components, you may use Skeleton’s common terminology and discuss the potential anatomy with the Skeleton team.

For example, the Zag Avatar component utilizes the following DOM structure:

<div>
<img />
<span>...</span>
</div>

As such, we’ll implement one component part respective of each element:

  • <Avatar> - the root element
  • <Avatar.Image> - the child image
  • <Avatar.Fallback> - the fallback span

We’ll also include a context component, to enable power-users access to the component tree’s Context API.

  • <Avatar.Context> - the context component; this is omitted from the component documentation.

Directory and File Names

Components are housed in the following location per framework:

FrameworkDirectory
React/src/components
Svelte/src/components

Skeleton uses a consistent naming convention per component:

avatar/
├── anatomy/
│ ├── fallback.{tsx|svelte}
│ ├── image.{tsx|svelte}
│ ├── root-context.{tsx|svelte}
│ └── root.{tsx|svelte}
├── modules/
│ ├── anatomy.ts
│ └── root-context.ts
└── index.ts

Anatomy Folder

The anatomy folder contains each component part inside a seperate file.

Component Part File

Every component part should export their component as a default export and their prop types as named exports.

React

avatar-root.tsx
export interface AvatarRootProps {
// ...
}
export default function (props: AvatarRootProps) {
// ...
}

Svelte

avatar-root.svelte
<script lang="ts" module>
export interface AvatarRootProps {
// ...
}
</script>
<script lang="ts">
const props: AvatarRootProps = $props();
// ...
</script>
<!-- ... --->

Note that you may need to extend or omit portions of the type to avoid conflicts between Zag and HTML attributes.

Extend

  • PropsWithElement - from Skeleton’s @/internal/props-with-element; allow for HTML template overrides.

Omit

  • Omit<Props, 'id'> - omit the id field from the Props interface as they will be provided inside the component itself.
  • React: Omit<ComponentProps<'div'>, 'id' | 'dir'> - omit conflicting HTML attributes.
  • Svelte: Omit<HTMLAttributes<HTMLDivElement>, 'id' | 'dir'> - omit conflicting HTML attributes.

Modules Folder

Anatomy File

The anatomy.ts file contains the exported anatomy, which enables the friendly dot notation syntax when consumed.

avatar-anatomy.ts
import Root from '../anatomy/root';
import Image from '../anatomy/image';
import Fallback from '../anatomy/fallback';
export const Avatar = Object.assign(
Root, // <Avatar>
{
Image: Image, // <Avatar.Image>
Fallback: Fallback // <Avatar.Fallback>
}
);

Context Part File

The {part}-context.ts file contains the exported context for each part’s context. This pattern enables strong typing.

For most components this will only be necessary for the root component, some components however may require context for other parts as well, reference other components for examples.

avatar-root-context.ts
import { createContext } from 'react';
export interface AvatarContextType {
// ...
}
export const AvatarContext = createContext<AvatarContextType>(null!);

NOTE: The interface is suffixed Type to avoid conflicting with the context reference itself.

Index File

The index prepares all above files for export.

export { Avatar } from './modules/anatomy';
export type { AvatarRootProps } from './anatomy/root';
export type { AvatarRootContextProps } from './anatomy/root-context';
export type { AvatarImageProps } from './anatomy/image';
export type { AvatarFallbackProps } from './anatomy/fallback';
export type { AvatarRootContextType as AvatarRootContext } from './modules/root-context';

Component Exports

Finally, make sure to export the new component for each respective component’s framework package. This is handled in /packages/skeleton-{framework}/src/index.ts.

export * from './components/accordion/index';
export * from './components/avatar/index';
// ...

Using Zag Primitives

Source Code

Locate the respective framework component source code on the Zag website. Here’s Avatars for example:

FrameworkDirectory
ReactAvatar Docs
SvelteAvatar Docs

In most cases, Zag provides all source code in a single file. Take care when splitting this into multiple component parts. We recommend starting with the root component - including the primitive imports, and defining the machine and api. Then utilize Context API and child components for each additional sub-component.

Context API

In some cases you may need to pass data from parent down to child components. For this, we can utilize each framework’s Context API:

FrameworkDocumentation
ReactView Component API docs
SvelteView Component API docs

Note that Skeleton implements a set convention for Context API to enable strong typing.

Common Conventions

While each component will present a unique set of challenges, we recommend you reference other existing components to understand how they were implemented. But there are a few common conventions we’ll detail below.

  • Try to stick as close to the Zag implementation DOM structure and naming as possible; don’t get creative.
  • Use whitespace to seperate Zag versus Skeleton logic, such as props, attributes, and context definitions.
  • Avoid hardcoded english text or icons. Consider pass-throughs using props, snippets, or sub-components.
  • Default to the named import pattern, such as import { foo, bar, fizz } from 'whatever'; (Including Zag’s imports, even though Zag uses catch-all imports in their docs).

React Specific

  • Pass the id field into the useMachine hook using the useId() hook from react.
  • Consume context using useContext() from react.
  • Use the className attribute to pass Skeleton classes.

Svelte Specific

  • Pass the id field into the useMachine function using the props.id() rune from svelte.
  • Consume context using the {context}.consume().
  • Use the class attribute to pass Skeleton classes.

NOTE: if you’re a contributor reading this and you feel there are more conventions to share. Please consider bringing this up with the Skeleton team. We might ask you to append these as you work on your contributions. The more knowledge we share, the more everyone benefits!


Styling Components

Styles are common and shared between all framework iterations of the same component. These reside in the skeleton-common package and are named to match their respective component.

packages/
└── skeleton-common/
└── src/
├── classes/
| ├── accordion.ts
| ├── avatar.ts
| └── ...
└── index.ts

Here’s an example of the Avatar styles found in avatar.ts:

avatar.ts
import { defineSkeletonClasses } from '../internal/define-skeleton-classes.js' with { type: 'macro' };
export const classesAvatar = defineSkeletonClasses({
root: 'isolate bg-surface-400-600 size-16 rounded-full overflow-hidden',
image: 'w-full object-cover',
fallback: 'size-full flex justify-center items-center'
});
  • We’ll cover the import { type: 'macro' } in the style prefix section.
  • Use the naming convention of classes{Component}
  • Create a key for each component part.
  • Set the value to each component’s default class list.
  • You can optionally pass an array of strings ['', ''] to document multi-line.
  • Make sure to export the component class file in index.ts.

Array Notation

You can optionally provide an array of strings whenever the class list is long or can be split into logical sections. This improves readability. The defineSkeletonClasses function will flatten the array into a single string at build time.

avatar.ts
import { defineSkeletonClasses } from '../internal/define-skeleton-classes' with { type: 'macro' };
export const classesProgressLinear = defineSkeletonClasses({
root: [
// Common
'items-center justify-center gap-2',
// Horizontal Orientation
'data-[orientation=horizontal]:flex data-[orientation=horizontal]:flex-row data-[orientation=horizontal]:w-full',
// Vertical Orientation
'data-[orientation=vertical]:inline-flex data-[orientation=vertical]:flex-col'
]
// ...
});

Style Prefix

It’s worth noting that during build time, Skeleton will automatically prefix each class in the class list with skb: (short for “Skeleton Base”). By applying with { type: 'macro' } to the import, the import will run defineSkeletonClasses specifically at build time. This variant prefix will assign each class to the Tailwind @base layer, ensuring user-provided classes take precedence over our internally defined classes. This is accomplished using the following Tailwind custom variant.

/packages/skeleton/src/variants/base.css
@custom-variant skb {
@layer base {
@slot;
}
}

If you need to prevent a class from being prefixed at build time, apply a variant of not-skb: to that class.

NOTE: This should be a rare use-case requiring discussion with the Skeleton team prior to implementation as it means the user won’t be able to override that specific class without using the anti-pattern: ! for !important.

Importing Class Lists

For Zag primitives, you can import and implement each class list Using Zag’s mergeProps utility for attributes.

avatar-root.tsx
import { mergeProps } from '@zag-js/react';
import { classesAvatar } from '@skeletonlabs/skeleton-common';
export default function (props: AvatarRootProps) {
const { children, ...restAttributes } = props;
const attributes = mergeProps(
// Zag Props
api.getRootProps(),
// Skeleton Props
{ className: classesAvatar.root }
// User Props
restAttributes
);
return <div {...attributes}>{children}</div>;
}

The process is similar for custom components without Zag primitives. We still use the Zag mergeProps utility.

navigation-root.tsx
import { mergeProps } from '@zag-js/react';
import { classesNavigation } from '@skeletonlabs/skeleton-common';
export default function (props: NavigationRootProps) {
const { children, ...restAttributes } = props;
const attributes = mergeProps(
// Skeleton Props
{ className: classesNavigation.root }
// User Props
restAttributes
);
return <div {...attributes}>{children}</div>;
}

Additional Resources