Components - Skeleton

  1. resources
  2. contribute
  3. components

Components

Guidelines for contributing new Skeleton components.

Packages

Component packages can be located here within the monorepo.

PackageFrameworkApp Framework
/packages/skeleton-svelteSvelte 5SvelteKit
/packages/skeleton-reactReactVite/React

The purpose of our component packages is two-fold. First, they house and export the full set of components available to that framework. Additionally, each project contains a dedicated meta-framework. This is used to test the components in their native environment. Use these projects as a sandbox to test and iterate before adding the components to the public facing Astro-base documentation website.

Dev Server

To run each app framework, change your directory to the respective package and run the following command.

Terminal window
cd packages/skeleton-svelte
pnpm run dev

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 Package App: http://localhost:5173/
  • React Package App: http://localhost:5173/

You may run the documentation site and framework packages in parallel at the same time. If the 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.

Add Components

Components are housed in the following location per framework:

FrameworkDirectory
Svelte/src/lib/components
React/src/lib/components

Use the following path and filename conventions when creating a new component:

/components
/ComponentName
ComponentName.{svelte|tsx|...}
ComponentName.test.ts
types.ts

Zag.js

Skeleton components are built atop a foundation of Zag.js. This provides a suite of headless component primitives handle logic and state, while providing a universal set of features. 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:

In most cases you can follow the documentation instructions verbatim to generate the foundation for the new component. Please refer to other components for specific implementation details. Continue reading below to learn how to further augment the component with all Skeleton-specific conventions.


Props

Skeleton designates a few categories of component properties. These should be maintained in the following order.

let {
// Functional
open: false,
// Style
base: '...',
bg: '...',
classes: '...',
// Event
onclick: () => {},
// Children
lead
trail
children
} = $props<ExampleProps>();
  • Functional - these should be single instance props that directly affect the functionality of the component.
  • Style - contain and accept Tailwind utility classes to affect the style of the component.
  • Event - provide callback functions for external event handlers.
  • Children - contain reference to React children, Svelte Snippets, or similar.

Style Prop Conventions

Style props implement sementic naming conventions to enable specificity and avoid naming conflicts. These are organized into three specific categories. Each should be maintained in the following order.

let {
// Parent
base: '...',
bg: '...',
classes: '...',
// Child
controlBase: '...',
controlBg: '...',
controlClasses: '...',
// Child (or Grandchild)
panelBase: '...',
panelBg: '...',
panelPadding: '...',
panelClasses: '...',
} = $props<ExampleProps>();
<!-- Parent -->
<div class="{base} {bg} {classes}">
<!-- Child: Control -->
<div class="{controlBase} {controlBg} {controlClasses}">...</div>
<!-- Child: Panel -->
<div class="{panelBase} {panelBg} {panelPadding} {panelClasses}">...</div>
</div>
  • base - houses any structual utility classes, which can be replaced in a faux-headless manner.
    • The outter most element is referred to as the Parent in this context. Elements within are children.
    • Parent props are not prefixed, which helps maintain parity cross-framework
    • Child props are prefixed: titleBase, panelBase, controlBase.
  • {property} - individual style props that house one or more “swappable” utility classes.
    • Naming should follow Tailwind convention, except for single letter descriptors (ex: padding instead of p).
    • Parent props are not prefixed: background, margin, border.
    • Child props are prefixed: titleBg, controlMargin, panelBorder.
  • classes - allows you to extend or override the class list with an arbitrary set of utility classes.
    • Uses classes (plural) to avoid conflict with the standard class attribute.
    • Parent instances are not prefixed: classes
    • Child instances are prefixed: titleClasses, controlClasses, panelClasses

Dynamic Style Props

You may need to conditionally update or swap between one or more sets of style prop classes. For this, we will use an interceptor pattern as demonstrated below. The rx naming convention denotes that the value will be derived in a reactive manner. This allows maintainers to quickly distinguish these from style props.

<script lang="ts">
let {
active = true,
// ...
fooActive = '...',
fooInactive = '...'
// ...
} = $props<ExampleProps>();
// Interceptor
const rxActive = $derived(active ? fooActive : fooInactive);
</script>
<div class="{base} {rxActive} {classes}">...</div>

TIP: use this convention in React as well, but make use of a ternary within useState or useMemo hooks, as appropriate.


Type Definitions

All component props should be strongly typed using Typescript. Each prop should be described using JSDoc comments. This provides additional context through Intellisense features of text editors and IDEs and helps generate the schema used for API references on each documentation page.

types.ts
export interface AccordionItemProps {
/** Sets the open state of the item. */
open?: boolean;
// Parent ---
/** Set the parent base styles. */
base?: string;
/** Set the parent background styles. */
background?: string;
/** Provide the parent a set of arbitrary classes. */
classes?: string;
// Lead ---
/** Sets the lead snippet element's base styles. */
leadBase?: string;
/** Sets the lead snippet element's padding styles. */
leadPadding?: string;
/** Provide arbitrary CSS classes to the lead snippet. */
leadClasses?: string;
// Children ---
/** The default slot contents within the component. */
children?: Snippet;
}
  • The type file should be co-located with the component and named types.ts.
  • The inteface name should match {ComponentName}Props.
  • Use comments to denote blocks of related props.
  • Keep each prop description short and semantic.
  • The order should match prop implementation.

Context API

Svelte

Skeleton expands the Svelte Context API via a utility called createContext. This adds the following functionality.

  • Generates a unique key using the Symbol API.
  • Ensures type safety when getting and setting context.
  • Allows for a fallback when there is no context provided.

Here’s an example of how this can be used:

import { createContext } from '$lib/internal/create-context.js';
interface Session {
user: 'foo' | 'bar';
}
export const [setSession, getSession] = createContext<Session>({ user: 'foo' });

This can then be consumed as follows:

import { setSession, getSession } from './context.js';
// Must be set as type of Session:
setSession({ user: 'bar' });
// Returns as type of Session:
const session = getSession();

Form Components

When implementing form components (ex: Segment Control), please adhere to the following conventions:

  • For components with a parent/child structure, pass all shared values through the parent.
  • Data in should use a value prop, while data out should use an onValueChange event handler.
  • When supported by the framework, use of two-way binding for the value is encouraged.
  • Pass state that’s unique to the child components directly to each child item.
  • When embedding form inputs within components for state management, always use type="hidden".

See the examples per each framework below.

Svelte

const value = $state('foo');
<!-- onChange is option and not required -->
<Control bind:value={value} name="flavors">
<Control.Item id="chocolate" value="chocolate">Chocolate</Control.Item>
<Control.Item id="strawberry" value="strawberry">Strawberry</Control.Item>
</Control>

React

const [value, setValue] = useState('foo');
<Control value={value} name="flavors" onValueChange={setValue}>
<Control.Item id="chocolate" value="chocolate">Chocolate</Control.Item>
<Control.Item id="strawberry" value="strawberry">Strawberry</Control.Item>
</Control>

Component Schema

When you add or change component types.ts data, new schema data will be generated to populate the component’s API Reference in each component’s documentation page. This should generate automatically, but can be triggered using pnpm schema in the monorepo root. In most cases you you should only need to implement the follow component in the page to display the generated schema data. The schema path will be discovered automatically, and no import is required.

## API Reference
<ApiTable />

Generate schema assets reside in the documenation project, within the following directory.

/src/content/schemas/{framework}/{component}.json

Animations

Skeleton opts for what we feel is the most optimal solution for animations per framework. This means implementation and capabilities may differ slightly for each. Please refer to the Accordion component source code as reference.

PackageSolution
/packages/skeleton-svelteSvelte Transition
/packages/skeleton-reactZag Presence

Composed Pattern

To keep component syntax consistent cross-framework, utilize what we refer to as a “composed pattern”. Components are composed using a set of smaller components, named slots, or snippets - depending on the framework. Implementation differs per framework.

React

For React, this is handled via piecemeal components using a dot notation syntax, as described in the section below.

<Accordion>
<Accordion.Item>
<Accordion.Control>(control)</Accordion.Control>
<Accordion.Panel>(panel)</Accordion.Panel>
</Accordion.Item>
</Accordion>

Svelte

This is handled by pairing child components with Svelte Snippets.

<Accordion>
<Accordion.Item>
{#snippet controlLead()}(lead){/snippet}
{#snippet control()}(control){/snippet}
{#snippet panel()}(panel){/snippet}
</Accordion.Item>
</Accordion>

Dot Notation Syntax

The implementation of this will differ per component framework.

React

To implement this, we use Object.assign()

Object.assign(RootComponent, { children });

For example, the accordion component would be setup as follows as the bottom of the Accordion.tsx file.

export const Accordion = Object.assign(
AccordionRoot, // -> <Accordion>
{
Item: AccordionItem, // -> <Accordion.Item>
Control: AccordionControl, // -> <Accordion.Control>
Panel: AccordionPanel // -> <Accordion.Panel>
}
);

Svelte

Create a new migrate.ts file colocated within the /component/<ComponentName> directory:

import AccordionRoot as Accordion from './Accordion.svelte';
import AccordionItem as Item from './AccordionItem.svelte';
export default Object.assign(
Accordion, // -> <Accordion>
{
Item, // -> <Accordion.Item>
}
);

Component Exports

Finally, make sure components are included in the package export list.

React

Found in /src/lib/migrate.ts.

export { Foo } from '../components/Foo/Foo.js';
export { Bar } from '../components/Bar/Bar.js';

Svelte

Found in /src/lib/index.ts.

export { default as Foo } from './components/Foo.svelte';
export { default as Bar } from './components/Bar.svelte';

Additional Resources