Edit Profile

Dark Mode

Copy config

Copy and paste the following code into your global.css file to apply the styles.

Contributing

Thinking about contributing to the project, but don't know where to start? You're in the right place!

We'll get you up in shape in no time, and ready to hop into the Qwik UI code cave.

The Qwik UI Code Cave

There are two projects we currently work on:

Qwik UI Headless

Don't know what a headless library is? This might help.

Qwik UI Styled

A styled copy-paste component library collection built with TailwindCSS, cva and tailwind-merge - inspired by Shadcn.

There's a lot of stuff here!

Yep, 99% of the time you're gonna be in two directories:

website - apps/website for the docs

packages - packages/kit-headless or packages/kit-styled

What can I do?

We welcome any sort of contributions, whether it's a new feature/component, bug fix, or documentation.

Feel free to hang out on Discord if you have a question, need help, or would like to help others.

Headless

Check out the introduction section to see the principles of Qwik UI, and the project goals. As a heads up, those may change over time with more discussion!

I don't know anything about accessibility, can I still contribute?

Of course! Neither did we before starting this project. Our go-to resource is the Aria Authoring Practices Guide. Find the component you're going for, and read through the component guide.

We also have plenty of other accessibility resources you can skim through. Feel free to ask questions!

When is a headless component beta?

It has features

It can be used for most common use cases, and maybe some advanced ones (if you'd like to go further).

What I like to do is look at other places around the web and see how things work! What kind of features do they have? Does someone already have a need for this in the Qwik community? How would I go about approaching this?

We also take inspiration from awesome headless libraries in other communities. For example, like the popular headless libraries below:

  • React Aria is a React Headless library.
  • Radix UI is a React Headless library.
  • Melt UI is a Svelte headless library.
  • Kobalte is a Solid JS headless library
  • Headless UI is a React and Vue headless library.
  • Ark UI is a headless library that uses state machines.
  • React Headless Hooks is a hooks based headless library for React.
  • Downshift is a hooks based library for accessible comboboxes and select components.

I love going through these projects and understanding the why and what problems they solve. What kinda features do all of them have in common? How do they name things? What conventions do they use? How satisfied are people consuming it?

I think this is a good way to figure out what to work on. That said, we also want to keep things simple, and not add features unless we know there is a demand for them (hence looking for similarities).

Tests

Tests ensure we can sleep sound at night and know that our component behavior is working as intended. Part of the Qwik core team, Shai Reznik (and also a contributor here!) talks a lot about test driven development.

TDD Process

  • we need a new feature!
  • make a failing test with the desired behavior (wut?)
  • get the test passing by adding said feature!
  • enjoy life when refactoring 🏝️

We strongly recommend TDD development for the headless library, and we are currently in the process of a playwright integration.

Currently, the component testing integration for Qwik & Playwright is in development, and we are using e2e tests for the time being. That said, most tests should be very easy to migrate later on.

Getting started w/ testing

Here's an example way of getting a testid of the Hero select docs example in index.mdx, without affecting any visible markup.

<div data-testid="select-hero-test">
  <Showcase name="hero" />
</div>

Then, we get our testDriver, you can think of it as reusable code we use throughout the test. For example, in the Select component we constantly grab the listbox, trigger, etc.

import { Locator, Page } from '@playwright/test';
 
export type DriverLocator = Locator | Page;
 
export function selectTestDriver<T extends DriverLocator>(locator: T) {
  const getRoot = () => {
    return locator.getByRole('combobox');
  };
 
  return {
    ...locator,
    locator,
    getRoot,
    getListbox() {
      return getRoot().getByRole('listbox');
    },
    getTrigger() {
      return getRoot().getByRole('button');
    },
    // get all options
    getOptions() {
      return getRoot().getByRole('option').all();
    },
  };
}

Now we can write some tests:

import { test, expect } from '@playwright/test';
import { selectTestDriver } from './select.driver';
 
test.beforeEach(async ({ page }) => {
  await page.goto('/docs/headless/select');
});
 
test.describe('critical functionality', () => {
  test(`GIVEN a basic select
        WHEN clicking on the trigger
        THEN open up the listbox
        AND aria-expanded should be true`, async ({ page }) => {
    const testDriver = selectTestDriver(page.getByTestId('select-hero-test'));
 
    const { getListbox, getTrigger } = testDriver;
 
    await getTrigger().click();
 
    await expect(getListbox()).toBeVisible();
    await expect(getTrigger()).toHaveAttribute('aria-expanded', 'true');
  });
 
  test(`GIVEN a basic select
        WHEN focusing the trigger and hitting enter
        THEN open up the listbox
        AND aria-expanded should be true`, async ({ page }) => {
    const testDriver = selectTestDriver(page.getByTestId('select-hero-test'));
 
    const { getListbox, getTrigger } = testDriver;
 
    await getTrigger().focus();
    await page.keyboard.press('Enter');
 
    await expect(getListbox()).toBeVisible();
    await expect(getTrigger()).toHaveAttribute('aria-expanded', 'true');
  });
});

To run the tests, use the pnpm playwright (only in my branch) command, or the nx e2e website longform. You can also do --ui to open the UI mode (which is pretty awesome!)

Once we've added a failing test with what we expect, we can now go ahead and implement the feature for that!

Heads up, I (Jack) am also relatively new to playwright myself 😅. I'm guessing there is a way to not need the testDriver to be consumed on each test. Feel free to experiment!

Docs

We use MDX for interactive markdown.

You can find the headless docs here.

When creating the docs, we have a showcase component, which gives typescript support, a component preview of your example, and automatically updates the code example as you edit it! 🤯

The same thing goes for our snippet component, which is for showing code blocks only.

Docs Components

We also have more docs components to make your life easier! Some examples being:

Notes

API table

PropTypeDescription
behavior
string

Determines whether the Accordion will open one or multiple items at a time.

onSelectedIndexChange$
function

An event hook that gets notified whenever the selected index changes.

onFocusIndexChange$
function

An event hook that gets notified whenever the focus index changes.

Feature list

  • feature A
  • feature B

Component status banner

DISCLAIMER: This component is in
Beta
status. That means that it is ready for production, but the API might change.

info popup (uses the popover)

What if I only want to do docs contributions, is that ok?

Absolutely, documentation is a critical part of the project, and something that can be very much improved! I recommend checking out Sarah Rainsberg's Docs Guide, it's partly towards Astro, but is also a great general resource for writing good documentation.

Where should I learn the Qwik parts?

If you find yourself stuck on a certain pattern, try taking a look through Qwik UI beta components. For example, the select component would be a good one to look through.

We're also happy to help! We love experimenting with things and having a blast while doing it.

What's something I should avoid?

useVisibleTask$. It's an escape hatch, and for 95% of UI components I can promise you that it's not needed.

You're pretty much saying "hey Qwik! All those benefits you do to lazy load and delay the execution of code? Let's throw those away".

Instead, look at it this way:

Where does my user interact with things? And how can I make sure that we can delay the execution of that code until the user ABSOLUTELY needs it.

Here's a code example I've seen in the Qwik discord. The developer is trying to make sure that an open menu navbar is closed when the window is resized over 1248px

useVisibleTask$(({ cleanup }) => {
  const updateDocumentClass = () => {
    if (menuOpen.value && window.innerWidth > 1248) {
      menuOpen.value = false;
      document.documentElement.classList.remove('modal-open');
    }
  };
 
  window.addEventListener('resize', updateDocumentClass);
 
  cleanup(() => {
    window.removeEventListener('resize', updateDocumentClass);
  });
});

Because this code is directly tied to an event, we should not be using a visible task. We could achieve the same result with:

useOnWindow('resize', $(() => {
  if (menuOpen.value && window.innerWidth > 1248) {
    menuOpen.value = false;
    document.documentElement.classList.remove('modal-open');
  }
});

We promise that creating ui elements gets easier once you have a clear mental model for API's like useTask$. Here are some alternatives to explore.

  • Events - onClick$, onScroll$, onKeydown$
  • useTask$ - (running code initially on server, tracked change on client)
  • useComputed$ - deriving state synchronously
  • Custom Events. Check out random-island.tsx too.
  • sync$ - perform some browser work ex: preventDefault w/ onKeyDown$, localStorage
  • useVisibleTask$ (the last resort)

We want to squeeze as much possible performance out of Qwik, and stay with the principle that things execute on interaction. This allows consumers to have a fast app without even trying!

How do I make a PR?

We cover it in-depth in the contributing guide here.

That's it!

Hopefully you should have enough to get up and running with Qwik UI Headless, if you have any questions don't let us stop you from reaching out, and happy building :qwik:

If you'd like to work on the styled library that's entirely a possibility too, there's currently documentation on the headless is all.