Preline UI, komponenty Tailwind CSS bez frameworka
Preline UI to zestaw komponentów interfejsu zbudowanych na Tailwind CSS, w którym zachowanie steruje się atrybutami w znacznikach zamiast pisaniem kodu. Bieżąca wersja to 4.2.0. Biblioteka działa bez żadnego frameworka, a osobno udostępniane są nakładki dla popularnych ekosystemów.
Sterowanie atrybutami, czyli główna cecha
Podejście, które tu obowiązuje, było standardem w bibliotekach sprzed ery frameworków i wróciło do łask razem z renderowaniem po stronie serwera.
Zamiast importować komponent i przekazywać mu właściwości, dopisujesz do znacznika atrybut opisujący, czym ten element ma być. Element z odpowiednim atrybutem staje się przełącznikiem otwierającym okno, inny staje się tym oknem, a biblioteka wiąże je ze sobą po identyfikatorze. Cały kod interakcji siedzi w jednym skrypcie wczytanym raz.
Zysk widać najwyraźniej tam, gdzie nie ma warstwy komponentowej: strona generowana po stronie serwera, szablon w języku szablonów, panel dopisany do istniejącej aplikacji. Nie musisz wprowadzać frameworka tylko po to, żeby mieć działające okno modalne.
Cena jest taka, że tracisz to, co daje warstwa komponentowa: typowanie, podpowiadanie w edytorze i wykrywanie błędów przed uruchomieniem. Literówka w nazwie atrybutu nie zgłasza się w żaden sposób, element po prostu nic nie robi. Przy większym projekcie warto to opakować we własne komponenty, żeby literówkę zamienić na błąd kompilacji.
Inicjalizacja po nawigacji, czyli najczęstszy problem
To jest rzecz, o którą potyka się większość osób łączących tę bibliotekę z aplikacją jednostronicową, i warto ją nazwać wprost, bo objawy bywają mylące.
Biblioteka przeszukuje dokument w poszukiwaniu elementów z odpowiednimi atrybutami i podpina do nich obsługę zdarzeń. Robi to raz, przy wczytaniu strony. Jeśli aplikacja podmienia zawartość bez przeładowania dokumentu, nowe elementy nie zostaną znalezione i nie zadziałają.
Objaw wygląda tak: komponent działa po odświeżeniu strony i przestaje działać po przejściu do niej z innej podstrony. Łatwo wziąć to za błąd biblioteki, a jest to kwestia cyklu życia. Rozwiązaniem jest ponowne uruchomienie przeszukiwania po każdej zmianie widoku, co w Next.js sprowadza się do wywołania go w efekcie reagującym na zmianę ścieżki.
Druga odmiana tego samego problemu dotyczy treści ładowanej asynchronicznie. Lista pobrana z serwera i wstawiona do dokumentu po pierwszym renderowaniu też wymaga ponownej inicjalizacji, bo w momencie pierwszego przeszukania jeszcze jej nie było.
Warto przy tym uważać na odwrotną skrajność, czyli wywoływanie inicjalizacji przy każdym renderowaniu komponentu. Przeszukanie całego dokumentu nie jest darmowe, a wielokrotne podpięcie obsługi do tego samego elementu prowadzi do zachowań w rodzaju okna otwierającego się i natychmiast zamykającego. Rozsądny układ to jedno wywołanie po zakończeniu nawigacji i jedno po wstawieniu treści pobranej z serwera, a nie przy każdej zmianie stanu.
Kiedy to jest właściwy wybór
Warto nazwać scenariusze wprost, bo ta biblioteka celuje w konkretną sytuację i poza nią przegrywa z alternatywami.
Najlepiej sprawdza się tam, gdzie nie ma warstwy komponentowej i nie zamierzasz jej wprowadzać. Aplikacja renderowana po stronie serwera, panel administracyjny dopisany do istniejącego systemu, strona w języku szablonów. Dostajesz działające komponenty bez dokładania frameworka i bez procesu budowania po stronie klienta.
Drugi dobry scenariusz to prototyp albo projekt na krótki termin, w którym liczy się tempo, a nie architektura. Wklejasz znaczniki, dodajesz skrypt i masz działający interfejs w godzinę, bez konfigurowania procesu budowania i bez decyzji, które trzeba by potem uzasadniać.
Gorzej wypada w dużej aplikacji komponentowej, gdzie sterowanie atrybutami kłóci się z resztą kodu. W takim projekcie stan interfejsu żyje w komponentach, a biblioteka trzyma go we własnych strukturach powiązanych z drzewem dokumentu, więc te dwa światy trzeba ręcznie synchronizować. To działa, ale jest źródłem błędów, których nie byłoby przy rozwiązaniu pisanym pod ten sam model.
Trzecia sytuacja, w której warto poszukać dalej, to projekt z twardymi wymaganiami dostępności, o czym niżej. Dotyczy to zwłaszcza sektora publicznego i branż regulowanych, gdzie zgodność podlega audytowi.
Licencja, czyli rzecz do sprawdzenia przed wdrożeniem
Tu wymagane jest sprostowanie, bo powtarzane wszędzie określenie „licencja MIT" jest niepełne.
Projekt stosuje licencjonowanie podwójne: użytkownik może wybrać MIT albo osobną licencję nazwaną przez autorów fair use. Ta druga dokłada ograniczenia, których w MIT nie ma, przede wszystkim zakaz tworzenia produktu konkurencyjnego wobec samej biblioteki oraz wymóg zachowania not licencyjnych przy redystrybucji.
Praktyczne znaczenie dla większości zespołów jest niewielkie, bo nikt nie buduje konkurencyjnej biblioteki komponentów, a wariant MIT pozostaje dostępny. Warto natomiast wiedzieć o jednej konsekwencji technicznej: plik licencji jest niestandardowy, więc automatyczne narzędzia skanujące zależności oznaczają go jako nierozpoznany. W organizacji, w której takie skanowanie jest częścią procesu, oznacza to dodatkowe pytanie od działu prawnego, a nie automatyczne przejście.
Rekomendacja jest więc prosta: jeśli Twoja firma wymaga zatwierdzania licencji zależności, zgłoś to zawczasu i wskaż, że wariant MIT jest jednym z dopuszczalnych wyborów. Odkrycie tego na etapie audytu przed wdrożeniem jest znacznie mniej wygodne.
Czym jest Preline UI?
Preline UI to otwarta biblioteka komponentów zbudowana na Tailwind CSS, oferująca ponad 640 gotowych elementów interfejsu w wariancie darmowym. Jest jedną z najbogatszych darmowych bibliotek dla Tailwinda i daje zarówno rzeczy proste, jak przyciski i formularze, jak i złożone, jak karuzele, kreatory krokowe czy wybieraki dat.
Wyróżnia ją wyjątkowo obszerna dokumentacja z setkami przykładów, przez co sprawdza się u osób szukających gotowych rozwiązań zamiast pisania od zera. Wszystkie komponenty są responsywne, obsługują tryb ciemny i są przygotowane pod kątem dostępności.
Dlaczego Preline UI?
- Ponad 640 komponentów - jeden z największych darmowych zbiorów
- Dobra dokumentacja - setki przykładów razem z kodem
- Czysty JavaScript, React i Vue - obsługa popularnych rozwiązań
- Tryb ciemny - wbudowany, bez dokładania czegokolwiek
- Kopiuj i wklej - kod bierzesz wprost z dokumentacji
- Podwójna licencja - MIT albo licencja własna producenta, darmowa w zastosowaniach komercyjnych
Preline UI vs Inne Biblioteki
| Cecha | Preline UI | Flowbite | daisyUI | Tailwind Plus |
|---|---|---|---|---|
| Cena | darmowy; Pro 249 USD, Team 459 USD, Enterprise po wycenie | darmowy plus wariant Pro | darmowy | 299 USD dla jednej osoby, 979 USD dla zespołu do 25 osób |
| Komponenty | 640+ w wariancie darmowym | 68 rodzajów komponentów | 68 komponentów | 500+ bloków interfejsu |
| Bloki i szablony | 204 bloki i 5 szablonów za darmo, 780+ bloków i 21 szablonów w Pro | 400+ sekcji w Flowbite Blocks | brak | szablony w React i Next.js |
| Frameworki | HTML, React, Vue | HTML plus nakładki dla frameworków | wyłącznie CSS | HTML, React, Vue |
| Dokumentacja | doskonała | bardzo dobra | dobra | świetna |
| Dark mode | wbudowany | wbudowany | wbudowany | wbudowany |
| Licencja | podwójna, MIT albo fair use | MIT | MIT | komercyjna, płatna |
Kiedy wybrać Preline UI?
Wybierz Preline UI, gdy:
- Potrzebujesz dużej liczby darmowych komponentów
- Cenisz dobrą dokumentację z przykładami
- Budujesz stronę HTML, React lub Vue
- Potrzebujesz zaawansowanych komponentów (stepper, tree view)
- Chcesz szybko prototypować
Rozważ alternatywy, gdy:
- Potrzebujesz wsparcia dla Angular/Svelte (Flowbite)
- Wolisz CSS-only bez JavaScript (DaisyUI)
- Budget pozwala na płatne rozwiązanie (Tailwind Plus)
Instalacja
NPM/Yarn
# npm
npm install preline
# yarn
yarn add preline
# pnpm
pnpm add prelineKonfiguracja Tailwind CSS
Od czwartej wersji Tailwinda konfiguracja mieszka w arkuszu stylów, a nie w pliku tailwind.config.js:
/* main.css */
@import "tailwindcss";
/* Preline UI */
@source "./node_modules/preline/dist/*.js";
@import "./node_modules/preline/variants.css";Dodanie skryptu JavaScript
<!-- Przed </body> -->
<script src="./node_modules/preline/dist/preline.js"></script>
<!-- Lub z CDN -->
<script src="https://cdn.jsdelivr.net/npm/preline@4.2.0/dist/preline.js"></script>Konfiguracja dla React i Next.js
// app/layout.tsx lub pages/_app.tsx
'use client'
import { useEffect } from 'react'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
useEffect(() => {
import('preline')
}, [])
return (
<html lang="pl">
<body>{children}</body>
</html>
)
}Konfiguracja dla Vue
// main.js
import 'preline'
// Lub w komponencie
import { onMounted } from 'vue'
onMounted(() => {
import('preline')
})Komponenty - Accordion
Accordion pozwala na rozwijanie i zwijanie sekcji treści.
<!-- Podstawowy Accordion -->
<div class="hs-accordion-group">
<div class="hs-accordion active" id="hs-basic-heading-one">
<button class="hs-accordion-toggle hs-accordion-active:text-blue-600 py-3 inline-flex items-center gap-x-3 w-full font-semibold text-start text-gray-800 hover:text-gray-500 focus:outline-none focus:text-gray-500 rounded-lg disabled:opacity-50 disabled:pointer-events-none dark:hs-accordion-active:text-blue-500 dark:text-neutral-200 dark:hover:text-neutral-400 dark:focus:text-neutral-400" aria-expanded="true" aria-controls="hs-basic-collapse-one">
<svg class="hs-accordion-active:hidden block size-3.5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14"></path>
<path d="M12 5v14"></path>
</svg>
<svg class="hs-accordion-active:block hidden size-3.5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14"></path>
</svg>
Accordion #1
</button>
<div id="hs-basic-collapse-one" class="hs-accordion-content w-full overflow-hidden transition-[height] duration-300" role="region" aria-labelledby="hs-basic-heading-one">
<p class="text-gray-800 dark:text-neutral-200">
This is the first accordion body. It is shown by default.
</p>
</div>
</div>
<div class="hs-accordion" id="hs-basic-heading-two">
<button class="hs-accordion-toggle hs-accordion-active:text-blue-600 py-3 inline-flex items-center gap-x-3 w-full font-semibold text-start text-gray-800 hover:text-gray-500 focus:outline-none focus:text-gray-500 rounded-lg disabled:opacity-50 disabled:pointer-events-none dark:hs-accordion-active:text-blue-500 dark:text-neutral-200 dark:hover:text-neutral-400 dark:focus:text-neutral-400" aria-expanded="false" aria-controls="hs-basic-collapse-two">
<svg class="hs-accordion-active:hidden block size-3.5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14"></path>
<path d="M12 5v14"></path>
</svg>
<svg class="hs-accordion-active:block hidden size-3.5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14"></path>
</svg>
Accordion #2
</button>
<div id="hs-basic-collapse-two" class="hs-accordion-content hidden w-full overflow-hidden transition-[height] duration-300" role="region" aria-labelledby="hs-basic-heading-two">
<p class="text-gray-800 dark:text-neutral-200">
This is the second accordion body.
</p>
</div>
</div>
</div>Accordion z ikonami po prawej stronie
<div class="hs-accordion-group">
<div class="hs-accordion active bg-white border -mt-px first:rounded-t-lg last:rounded-b-lg dark:bg-neutral-800 dark:border-neutral-700" id="hs-bordered-heading-one">
<button class="hs-accordion-toggle hs-accordion-active:text-blue-600 inline-flex items-center justify-between gap-x-3 w-full font-semibold text-start text-gray-800 py-4 px-5 hover:text-gray-500 disabled:opacity-50 disabled:pointer-events-none dark:hs-accordion-active:text-blue-500 dark:text-neutral-200 dark:hover:text-neutral-400" aria-expanded="true" aria-controls="hs-bordered-collapse-one">
Accordion #1
<svg class="hs-accordion-active:hidden block size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m6 9 6 6 6-6"></path>
</svg>
<svg class="hs-accordion-active:block hidden size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m18 15-6-6-6 6"></path>
</svg>
</button>
<div id="hs-bordered-collapse-one" class="hs-accordion-content w-full overflow-hidden transition-[height] duration-300" role="region" aria-labelledby="hs-bordered-heading-one">
<div class="pb-4 px-5">
<p class="text-gray-800 dark:text-neutral-200">
Content for accordion #1
</p>
</div>
</div>
</div>
</div>Komponenty - Tabs
<!-- Podstawowe Tabs -->
<div class="border-b border-gray-200 dark:border-neutral-700">
<nav class="flex gap-x-1" aria-label="Tabs" role="tablist" aria-orientation="horizontal">
<button type="button" class="hs-tab-active:font-semibold hs-tab-active:border-blue-600 hs-tab-active:text-blue-600 py-4 px-1 inline-flex items-center gap-x-2 border-b-2 border-transparent text-sm whitespace-nowrap text-gray-500 hover:text-blue-600 focus:outline-none focus:text-blue-600 disabled:opacity-50 disabled:pointer-events-none dark:text-neutral-400 dark:hover:text-blue-500 active" id="tabs-with-underline-item-1" aria-selected="true" data-hs-tab="#tabs-with-underline-1" aria-controls="tabs-with-underline-1" role="tab">
Tab 1
</button>
<button type="button" class="hs-tab-active:font-semibold hs-tab-active:border-blue-600 hs-tab-active:text-blue-600 py-4 px-1 inline-flex items-center gap-x-2 border-b-2 border-transparent text-sm whitespace-nowrap text-gray-500 hover:text-blue-600 focus:outline-none focus:text-blue-600 disabled:opacity-50 disabled:pointer-events-none dark:text-neutral-400 dark:hover:text-blue-500" id="tabs-with-underline-item-2" aria-selected="false" data-hs-tab="#tabs-with-underline-2" aria-controls="tabs-with-underline-2" role="tab">
Tab 2
</button>
<button type="button" class="hs-tab-active:font-semibold hs-tab-active:border-blue-600 hs-tab-active:text-blue-600 py-4 px-1 inline-flex items-center gap-x-2 border-b-2 border-transparent text-sm whitespace-nowrap text-gray-500 hover:text-blue-600 focus:outline-none focus:text-blue-600 disabled:opacity-50 disabled:pointer-events-none dark:text-neutral-400 dark:hover:text-blue-500" id="tabs-with-underline-item-3" aria-selected="false" data-hs-tab="#tabs-with-underline-3" aria-controls="tabs-with-underline-3" role="tab">
Tab 3
</button>
</nav>
</div>
<div class="mt-3">
<div id="tabs-with-underline-1" role="tabpanel" aria-labelledby="tabs-with-underline-item-1">
<p class="text-gray-500 dark:text-neutral-400">
This is the <em class="font-semibold text-gray-800 dark:text-neutral-200">first</em> tab content.
</p>
</div>
<div id="tabs-with-underline-2" class="hidden" role="tabpanel" aria-labelledby="tabs-with-underline-item-2">
<p class="text-gray-500 dark:text-neutral-400">
This is the <em class="font-semibold text-gray-800 dark:text-neutral-200">second</em> tab content.
</p>
</div>
<div id="tabs-with-underline-3" class="hidden" role="tabpanel" aria-labelledby="tabs-with-underline-item-3">
<p class="text-gray-500 dark:text-neutral-400">
This is the <em class="font-semibold text-gray-800 dark:text-neutral-200">third</em> tab content.
</p>
</div>
</div>Tabs jako Pills (przyciski)
<nav class="flex gap-x-1" aria-label="Tabs" role="tablist" aria-orientation="horizontal">
<button type="button" class="hs-tab-active:bg-blue-600 hs-tab-active:text-white py-3 px-4 inline-flex items-center gap-x-2 bg-transparent text-sm font-medium text-center text-gray-500 rounded-lg hover:text-blue-600 focus:outline-none focus:text-blue-600 disabled:opacity-50 disabled:pointer-events-none dark:text-neutral-400 dark:hover:text-blue-500 active" id="pills-item-1" aria-selected="true" data-hs-tab="#pills-1" aria-controls="pills-1" role="tab">
Tab 1
</button>
<button type="button" class="hs-tab-active:bg-blue-600 hs-tab-active:text-white py-3 px-4 inline-flex items-center gap-x-2 bg-transparent text-sm font-medium text-center text-gray-500 rounded-lg hover:text-blue-600 focus:outline-none focus:text-blue-600 disabled:opacity-50 disabled:pointer-events-none dark:text-neutral-400 dark:hover:text-blue-500" id="pills-item-2" aria-selected="false" data-hs-tab="#pills-2" aria-controls="pills-2" role="tab">
Tab 2
</button>
<button type="button" class="hs-tab-active:bg-blue-600 hs-tab-active:text-white py-3 px-4 inline-flex items-center gap-x-2 bg-transparent text-sm font-medium text-center text-gray-500 rounded-lg hover:text-blue-600 focus:outline-none focus:text-blue-600 disabled:opacity-50 disabled:pointer-events-none dark:text-neutral-400 dark:hover:text-blue-500" id="pills-item-3" aria-selected="false" data-hs-tab="#pills-3" aria-controls="pills-3" role="tab">
Tab 3
</button>
</nav>Komponenty - Modal
<!-- Trigger Button -->
<button type="button" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none" aria-haspopup="dialog" aria-expanded="false" aria-controls="hs-basic-modal" data-hs-overlay="#hs-basic-modal">
Open modal
</button>
<!-- Modal -->
<div id="hs-basic-modal" class="hs-overlay hidden size-full fixed top-0 start-0 z-[80] overflow-x-hidden overflow-y-auto pointer-events-none" role="dialog" tabindex="-1" aria-labelledby="hs-basic-modal-label">
<div class="hs-overlay-open:mt-7 hs-overlay-open:opacity-100 hs-overlay-open:duration-500 mt-0 opacity-0 ease-out transition-all sm:max-w-lg sm:w-full m-3 sm:mx-auto">
<div class="flex flex-col bg-white border shadow-sm rounded-xl pointer-events-auto dark:bg-neutral-800 dark:border-neutral-700 dark:shadow-neutral-700/70">
<div class="flex justify-between items-center py-3 px-4 border-b dark:border-neutral-700">
<h3 id="hs-basic-modal-label" class="font-bold text-gray-800 dark:text-white">
Modal title
</h3>
<button type="button" class="size-8 inline-flex justify-center items-center gap-x-2 rounded-full border border-transparent bg-gray-100 text-gray-800 hover:bg-gray-200 focus:outline-none focus:bg-gray-200 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:text-neutral-400 dark:focus:bg-neutral-600" aria-label="Close" data-hs-overlay="#hs-basic-modal">
<span class="sr-only">Close</span>
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6 6 18"></path>
<path d="m6 6 12 12"></path>
</svg>
</button>
</div>
<div class="p-4 overflow-y-auto">
<p class="text-gray-800 dark:text-neutral-400">
This is a modal body. You can add any content here.
</p>
</div>
<div class="flex justify-end items-center gap-x-2 py-3 px-4 border-t dark:border-neutral-700">
<button type="button" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-sm hover:bg-gray-50 focus:outline-none focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700" data-hs-overlay="#hs-basic-modal">
Close
</button>
<button type="button" class="py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none">
Save changes
</button>
</div>
</div>
</div>
</div>Modal Sizes
<!-- Small Modal -->
<div id="hs-small-modal" class="hs-overlay hidden">
<div class="hs-overlay-open:mt-7 hs-overlay-open:opacity-100 mt-0 opacity-0 ease-out transition-all sm:max-w-sm sm:w-full m-3 sm:mx-auto">
<!-- content -->
</div>
</div>
<!-- Large Modal -->
<div id="hs-large-modal" class="hs-overlay hidden">
<div class="hs-overlay-open:mt-7 hs-overlay-open:opacity-100 mt-0 opacity-0 ease-out transition-all lg:max-w-4xl lg:w-full m-3 lg:mx-auto">
<!-- content -->
</div>
</div>
<!-- Full Screen Modal -->
<div id="hs-full-screen-modal" class="hs-overlay hidden">
<div class="hs-overlay-open:mt-7 hs-overlay-open:opacity-100 mt-0 opacity-0 ease-out transition-all max-w-full w-full m-3 mx-auto h-[calc(100%-3.5rem)]">
<!-- content -->
</div>
</div>Komponenty - Dropdown
<div class="hs-dropdown relative inline-flex">
<button id="hs-dropdown-default" type="button" class="hs-dropdown-toggle py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-sm hover:bg-gray-50 focus:outline-none focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700" aria-haspopup="menu" aria-expanded="false" aria-label="Dropdown">
Actions
<svg class="hs-dropdown-open:rotate-180 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m6 9 6 6 6-6"/>
</svg>
</button>
<div class="hs-dropdown-menu transition-[opacity,margin] duration hs-dropdown-open:opacity-100 opacity-0 hidden min-w-60 bg-white shadow-md rounded-lg mt-2 dark:bg-neutral-800 dark:border dark:border-neutral-700" role="menu" aria-orientation="vertical" aria-labelledby="hs-dropdown-default">
<div class="p-1 space-y-0.5">
<a class="flex items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm text-gray-800 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700" href="#">
Newsletter
</a>
<a class="flex items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm text-gray-800 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700" href="#">
Purchases
</a>
<a class="flex items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm text-gray-800 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700" href="#">
Downloads
</a>
<a class="flex items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm text-gray-800 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700" href="#">
Team Account
</a>
</div>
</div>
</div>Dropdown z ikonami
<div class="hs-dropdown relative inline-flex">
<button id="hs-dropdown-with-icons" type="button" class="hs-dropdown-toggle py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-sm hover:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700">
With icons
<svg class="hs-dropdown-open:rotate-180 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m6 9 6 6 6-6"/>
</svg>
</button>
<div class="hs-dropdown-menu transition-[opacity,margin] duration hs-dropdown-open:opacity-100 opacity-0 hidden min-w-60 bg-white shadow-md rounded-lg mt-2 divide-y divide-gray-200 dark:bg-neutral-800 dark:border dark:border-neutral-700 dark:divide-neutral-700">
<div class="p-1 space-y-0.5">
<a class="flex items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm text-gray-800 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-neutral-300" href="#">
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" x2="12" y1="15" y2="3"/>
</svg>
Downloads
</a>
<a class="flex items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm text-gray-800 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-neutral-300" href="#">
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M22 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
Team Account
</a>
</div>
<div class="p-1 space-y-0.5">
<a class="flex items-center gap-x-3.5 py-2 px-3 rounded-lg text-sm text-red-600 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 dark:text-red-500 dark:hover:bg-neutral-700" href="#">
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18"/>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
<line x1="10" x2="10" y1="11" y2="17"/>
<line x1="14" x2="14" y1="11" y2="17"/>
</svg>
Delete
</a>
</div>
</div>
</div>Komponenty - Tooltip
<!-- Basic Tooltip -->
<div class="hs-tooltip inline-block">
<button type="button" class="hs-tooltip-toggle py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-sm hover:bg-gray-50 focus:outline-none focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
Hover me
</button>
<span class="hs-tooltip-content hs-tooltip-shown:opacity-100 hs-tooltip-shown:visible opacity-0 transition-opacity inline-block absolute invisible z-10 py-1 px-2 bg-gray-900 text-xs font-medium text-white rounded shadow-sm dark:bg-neutral-700" role="tooltip">
Tooltip content
</span>
</div>
<!-- Tooltip z różnymi pozycjami -->
<div class="hs-tooltip inline-block" data-hs-tooltip-placement="top">
<button type="button" class="hs-tooltip-toggle ...">Top</button>
<span class="hs-tooltip-content ..." role="tooltip">Top tooltip</span>
</div>
<div class="hs-tooltip inline-block" data-hs-tooltip-placement="bottom">
<button type="button" class="hs-tooltip-toggle ...">Bottom</button>
<span class="hs-tooltip-content ..." role="tooltip">Bottom tooltip</span>
</div>
<div class="hs-tooltip inline-block" data-hs-tooltip-placement="left">
<button type="button" class="hs-tooltip-toggle ...">Left</button>
<span class="hs-tooltip-content ..." role="tooltip">Left tooltip</span>
</div>
<div class="hs-tooltip inline-block" data-hs-tooltip-placement="right">
<button type="button" class="hs-tooltip-toggle ...">Right</button>
<span class="hs-tooltip-content ..." role="tooltip">Right tooltip</span>
</div>Komponenty - Carousel
<div data-hs-carousel='{
"loadingClasses": "opacity-0",
"isAutoPlay": true
}' class="relative">
<div class="hs-carousel relative overflow-hidden w-full min-h-96 bg-white rounded-lg">
<div class="hs-carousel-body absolute top-0 bottom-0 start-0 flex flex-nowrap transition-transform duration-700 opacity-0">
<div class="hs-carousel-slide">
<div class="flex justify-center h-full bg-gray-100 p-6 dark:bg-neutral-900">
<span class="self-center text-4xl text-gray-800 transition duration-700 dark:text-white">First slide</span>
</div>
</div>
<div class="hs-carousel-slide">
<div class="flex justify-center h-full bg-gray-200 p-6 dark:bg-neutral-800">
<span class="self-center text-4xl text-gray-800 transition duration-700 dark:text-white">Second slide</span>
</div>
</div>
<div class="hs-carousel-slide">
<div class="flex justify-center h-full bg-gray-300 p-6 dark:bg-neutral-700">
<span class="self-center text-4xl text-gray-800 transition duration-700 dark:text-white">Third slide</span>
</div>
</div>
</div>
</div>
<!-- Prev/Next buttons -->
<button type="button" class="hs-carousel-prev hs-carousel-disabled:opacity-50 disabled:pointer-events-none absolute inset-y-0 start-0 inline-flex justify-center items-center w-[46px] h-full text-gray-800 hover:bg-gray-800/10 focus:outline-none focus:bg-gray-800/10 rounded-s-lg dark:text-white dark:hover:bg-white/10 dark:focus:bg-white/10">
<span class="text-2xl" aria-hidden="true">
<svg class="shrink-0 size-5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m15 18-6-6 6-6"></path>
</svg>
</span>
<span class="sr-only">Previous</span>
</button>
<button type="button" class="hs-carousel-next hs-carousel-disabled:opacity-50 disabled:pointer-events-none absolute inset-y-0 end-0 inline-flex justify-center items-center w-[46px] h-full text-gray-800 hover:bg-gray-800/10 focus:outline-none focus:bg-gray-800/10 rounded-e-lg dark:text-white dark:hover:bg-white/10 dark:focus:bg-white/10">
<span class="sr-only">Next</span>
<span class="text-2xl" aria-hidden="true">
<svg class="shrink-0 size-5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m9 18 6-6-6-6"></path>
</svg>
</span>
</button>
<!-- Pagination -->
<div class="hs-carousel-pagination flex justify-center absolute bottom-3 start-0 end-0 space-x-2">
<span class="hs-carousel-active:bg-blue-700 hs-carousel-active:border-blue-700 size-3 border border-gray-400 rounded-full cursor-pointer dark:border-neutral-600 dark:hs-carousel-active:bg-blue-500 dark:hs-carousel-active:border-blue-500"></span>
<span class="hs-carousel-active:bg-blue-700 hs-carousel-active:border-blue-700 size-3 border border-gray-400 rounded-full cursor-pointer dark:border-neutral-600 dark:hs-carousel-active:bg-blue-500 dark:hs-carousel-active:border-blue-500"></span>
<span class="hs-carousel-active:bg-blue-700 hs-carousel-active:border-blue-700 size-3 border border-gray-400 rounded-full cursor-pointer dark:border-neutral-600 dark:hs-carousel-active:bg-blue-500 dark:hs-carousel-active:border-blue-500"></span>
</div>
</div>Komponenty - Stepper
<!-- Basic Stepper -->
<ul class="relative flex flex-row gap-x-2">
<!-- Completed Step -->
<li class="shrink basis-0 flex-1 group">
<div class="min-w-7 min-h-7 w-full inline-flex items-center text-xs align-middle">
<span class="size-7 flex justify-center items-center shrink-0 bg-blue-600 font-medium text-white rounded-full dark:bg-blue-500">
<svg class="shrink-0 size-3" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</span>
<div class="ms-2 w-full h-px flex-1 bg-blue-600 group-last:hidden dark:bg-blue-500"></div>
</div>
<div class="mt-3">
<span class="block text-sm font-medium text-gray-800 dark:text-white">
Step
</span>
</div>
</li>
<!-- Current Step -->
<li class="shrink basis-0 flex-1 group">
<div class="min-w-7 min-h-7 w-full inline-flex items-center text-xs align-middle">
<span class="size-7 flex justify-center items-center shrink-0 bg-blue-600 font-medium text-white rounded-full dark:bg-blue-500">
2
</span>
<div class="ms-2 w-full h-px flex-1 bg-gray-200 group-last:hidden dark:bg-neutral-700"></div>
</div>
<div class="mt-3">
<span class="block text-sm font-medium text-blue-600 dark:text-blue-500">
Step
</span>
</div>
</li>
<!-- Future Step -->
<li class="shrink basis-0 flex-1 group">
<div class="min-w-7 min-h-7 w-full inline-flex items-center text-xs align-middle">
<span class="size-7 flex justify-center items-center shrink-0 bg-gray-100 font-medium text-gray-800 rounded-full dark:bg-neutral-700 dark:text-white">
3
</span>
<div class="ms-2 w-full h-px flex-1 bg-gray-200 group-last:hidden dark:bg-neutral-700"></div>
</div>
<div class="mt-3">
<span class="block text-sm font-medium text-gray-800 dark:text-white">
Step
</span>
</div>
</li>
</ul>Vertical Stepper
<ul class="relative flex flex-col gap-2">
<li class="flex items-center gap-x-2 shrink basis-0 flex-1 group">
<span class="min-w-7 min-h-7 inline-flex justify-center items-center text-xs align-middle">
<span class="size-7 flex justify-center items-center shrink-0 bg-blue-600 font-medium text-white rounded-full dark:bg-blue-500">
1
</span>
</span>
<span class="text-sm font-medium text-gray-800 dark:text-white">
First step completed
</span>
</li>
<li class="flex items-center gap-x-2 shrink basis-0 flex-1 group">
<span class="min-w-7 min-h-7 inline-flex justify-center items-center text-xs align-middle">
<span class="size-7 flex justify-center items-center shrink-0 bg-blue-600 font-medium text-white rounded-full dark:bg-blue-500">
2
</span>
</span>
<span class="text-sm font-medium text-blue-600 dark:text-blue-500">
Current step
</span>
</li>
<li class="flex items-center gap-x-2 shrink basis-0 flex-1 group">
<span class="min-w-7 min-h-7 inline-flex justify-center items-center text-xs align-middle">
<span class="size-7 flex justify-center items-center shrink-0 bg-gray-100 font-medium text-gray-800 rounded-full dark:bg-neutral-700 dark:text-white">
3
</span>
</span>
<span class="text-sm font-medium text-gray-800 dark:text-white">
Final step
</span>
</li>
</ul>Komponenty - Form Elements
Input
<!-- Basic Input -->
<div class="max-w-sm">
<label for="input-label" class="block text-sm font-medium mb-2 dark:text-white">Email</label>
<input type="email" id="input-label" class="py-3 px-4 block w-full border-gray-200 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600" placeholder="you@example.com">
</div>
<!-- Input with helper text -->
<div class="max-w-sm">
<label for="input-label-with-helper-text" class="block text-sm font-medium mb-2 dark:text-white">Email</label>
<input type="email" id="input-label-with-helper-text" class="py-3 px-4 block w-full border-gray-200 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600" placeholder="you@example.com" aria-describedby="hs-input-helper-text">
<p class="text-sm text-gray-500 mt-2" id="hs-input-helper-text">We'll never share your details.</p>
</div>
<!-- Input with validation -->
<div class="max-w-sm">
<label for="with-validation" class="block text-sm font-medium mb-2 dark:text-white">Email</label>
<input type="email" id="with-validation" class="py-3 px-4 block w-full border-red-500 rounded-lg text-sm focus:border-red-500 focus:ring-red-500 dark:bg-neutral-900 dark:border-red-600 dark:text-neutral-400" aria-describedby="email-error">
<p class="text-sm text-red-600 mt-2" id="email-error">Please enter a valid email address.</p>
</div>
<!-- Input with success -->
<div class="max-w-sm">
<label for="with-success" class="block text-sm font-medium mb-2 dark:text-white">Email</label>
<input type="email" id="with-success" class="py-3 px-4 block w-full border-teal-500 rounded-lg text-sm focus:border-teal-500 focus:ring-teal-500 dark:bg-neutral-900 dark:border-teal-600 dark:text-neutral-400" aria-describedby="email-success">
<p class="text-sm text-teal-600 mt-2" id="email-success">Looks good!</p>
</div>Select
<div class="max-w-sm">
<label for="countries" class="block text-sm font-medium mb-2 dark:text-white">Country</label>
<select id="countries" class="py-3 px-4 pe-9 block w-full border-gray-200 rounded-lg text-sm focus:border-blue-500 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-900 dark:border-neutral-700 dark:text-neutral-400 dark:placeholder-neutral-500 dark:focus:ring-neutral-600">
<option selected>Open this select menu</option>
<option value="US">United States</option>
<option value="CA">Canada</option>
<option value="FR">France</option>
</select>
</div>Checkbox
<div class="flex">
<input type="checkbox" class="shrink-0 mt-0.5 border-gray-200 rounded text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="hs-default-checkbox">
<label for="hs-default-checkbox" class="text-sm text-gray-500 ms-3 dark:text-neutral-400">Default checkbox</label>
</div>Radio
<div class="flex gap-x-6">
<div class="flex">
<input type="radio" name="radio-group" class="shrink-0 mt-0.5 border-gray-200 rounded-full text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="radio-1" checked>
<label for="radio-1" class="text-sm text-gray-500 ms-2 dark:text-neutral-400">Default</label>
</div>
<div class="flex">
<input type="radio" name="radio-group" class="shrink-0 mt-0.5 border-gray-200 rounded-full text-blue-600 focus:ring-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-800" id="radio-2">
<label for="radio-2" class="text-sm text-gray-500 ms-2 dark:text-neutral-400">Checked</label>
</div>
</div>Toggle Switch
<input type="checkbox" id="hs-basic-usage" class="relative w-[3.25rem] h-7 p-px bg-gray-100 border-transparent text-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:ring-blue-600 disabled:opacity-50 disabled:pointer-events-none checked:bg-none checked:text-blue-600 checked:border-blue-600 focus:checked:border-blue-600 dark:bg-neutral-800 dark:border-neutral-700 dark:checked:bg-blue-500 dark:checked:border-blue-500 dark:focus:ring-offset-gray-600
before:inline-block before:size-6 before:bg-white checked:before:bg-white before:translate-x-0 checked:before:translate-x-full before:rounded-full before:shadow before:transform before:ring-0 before:transition before:ease-in-out before:duration-200 dark:before:bg-neutral-400 dark:checked:before:bg-white">
<label for="hs-basic-usage" class="text-sm text-gray-500 ms-3 dark:text-neutral-400">Toggle switch</label>Komponenty - Buttons
<!-- Solid Buttons -->
<button type="button" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none">
Primary
</button>
<button type="button" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-gray-500 text-white hover:bg-gray-600 focus:outline-none focus:bg-gray-600 disabled:opacity-50 disabled:pointer-events-none">
Secondary
</button>
<button type="button" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-teal-500 text-white hover:bg-teal-600 focus:outline-none focus:bg-teal-600 disabled:opacity-50 disabled:pointer-events-none">
Success
</button>
<button type="button" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-red-500 text-white hover:bg-red-600 focus:outline-none focus:bg-red-600 disabled:opacity-50 disabled:pointer-events-none">
Danger
</button>
<button type="button" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-yellow-500 text-white hover:bg-yellow-600 focus:outline-none focus:bg-yellow-600 disabled:opacity-50 disabled:pointer-events-none">
Warning
</button>
<!-- Outline Buttons -->
<button type="button" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-blue-600 text-blue-600 hover:border-blue-500 hover:text-blue-500 focus:outline-none focus:border-blue-500 focus:text-blue-500 disabled:opacity-50 disabled:pointer-events-none dark:border-blue-500 dark:text-blue-500 dark:hover:text-blue-400 dark:hover:border-blue-400">
Outline Primary
</button>
<!-- Ghost Buttons -->
<button type="button" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent text-blue-600 hover:bg-blue-100 hover:text-blue-800 focus:outline-none focus:bg-blue-100 focus:text-blue-800 disabled:opacity-50 disabled:pointer-events-none dark:text-blue-500 dark:hover:bg-blue-800/30 dark:hover:text-blue-400 dark:focus:bg-blue-800/30 dark:focus:text-blue-400">
Ghost
</button>
<!-- Soft Buttons -->
<button type="button" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-100 text-blue-800 hover:bg-blue-200 focus:outline-none focus:bg-blue-200 disabled:opacity-50 disabled:pointer-events-none dark:text-blue-400 dark:bg-blue-800/30 dark:hover:bg-blue-800/20 dark:focus:bg-blue-800/20">
Soft
</button>
<!-- White Button -->
<button type="button" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-sm hover:bg-gray-50 focus:outline-none focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-800 dark:border-neutral-700 dark:text-white dark:hover:bg-neutral-700 dark:focus:bg-neutral-700">
White
</button>
<!-- Button with icon -->
<button type="button" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:bg-blue-700 disabled:opacity-50 disabled:pointer-events-none">
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14"></path>
<path d="M12 5v14"></path>
</svg>
Add item
</button>
<!-- Button sizes -->
<button type="button" class="py-2 px-3 inline-flex items-center gap-x-2 text-xs font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700">
Small
</button>
<button type="button" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700">
Default
</button>
<button type="button" class="py-3 px-4 inline-flex items-center gap-x-2 text-base font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700">
Large
</button>Komponenty - Alerts
<!-- Info Alert -->
<div class="bg-blue-100 border border-blue-200 text-sm text-blue-800 rounded-lg p-4 dark:bg-blue-800/10 dark:border-blue-900 dark:text-blue-500" role="alert" tabindex="-1" aria-labelledby="hs-with-description-label">
<div class="flex">
<div class="shrink-0">
<svg class="shrink-0 size-4 mt-0.5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 16v-4"></path>
<path d="M12 8h.01"></path>
</svg>
</div>
<div class="ms-4">
<h3 id="hs-with-description-label" class="text-sm font-semibold">
Info
</h3>
<div class="mt-1 text-sm text-blue-700 dark:text-blue-400">
A simple info alert with an example link.
</div>
</div>
</div>
</div>
<!-- Success Alert -->
<div class="bg-teal-100 border border-teal-200 text-sm text-teal-800 rounded-lg p-4 dark:bg-teal-800/10 dark:border-teal-900 dark:text-teal-500" role="alert" tabindex="-1">
<span class="font-bold">Success</span> Your purchase was successful.
</div>
<!-- Warning Alert -->
<div class="bg-yellow-100 border border-yellow-200 text-sm text-yellow-800 rounded-lg p-4 dark:bg-yellow-800/10 dark:border-yellow-900 dark:text-yellow-500" role="alert" tabindex="-1">
<span class="font-bold">Warning</span> Please check your input.
</div>
<!-- Error Alert -->
<div class="bg-red-100 border border-red-200 text-sm text-red-800 rounded-lg p-4 dark:bg-red-800/10 dark:border-red-900 dark:text-red-500" role="alert" tabindex="-1">
<span class="font-bold">Error</span> Something went wrong.
</div>
<!-- Dismissible Alert -->
<div id="dismiss-alert" class="hs-removing:translate-x-5 hs-removing:opacity-0 transition duration-300 bg-teal-50 border border-teal-200 text-sm text-teal-800 rounded-lg p-4 dark:bg-teal-800/10 dark:border-teal-900 dark:text-teal-500" role="alert" tabindex="-1">
<div class="flex">
<div class="shrink-0">
<svg class="shrink-0 size-4 mt-0.5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"></path>
<path d="m9 12 2 2 4-4"></path>
</svg>
</div>
<div class="ms-2">
<div class="text-sm font-medium">
Successfully uploaded.
</div>
</div>
<div class="ps-3 ms-auto">
<div class="-mx-1.5 -my-1.5">
<button type="button" class="inline-flex bg-teal-50 rounded-lg p-1.5 text-teal-500 hover:bg-teal-100 focus:outline-none focus:bg-teal-100 dark:bg-transparent dark:hover:bg-teal-800/50 dark:focus:bg-teal-800/50" data-hs-remove-element="#dismiss-alert">
<span class="sr-only">Dismiss</span>
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6 6 18"></path>
<path d="m6 6 12 12"></path>
</svg>
</button>
</div>
</div>
</div>
</div>Dark Mode
Preline UI wspiera dark mode przez wariant dark Tailwinda. Żeby przełączać go ręcznie zamiast opierać się na ustawieniu systemu, nadpisz ten wariant własnym selektorem:
/* main.css */
@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));<!-- Przełącznik dark mode -->
<button type="button" class="hs-dark-mode-active:hidden block hs-dark-mode group flex items-center text-gray-600 hover:text-blue-600 font-medium dark:text-neutral-400 dark:hover:text-neutral-500" data-hs-theme-click-value="dark">
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"></path>
</svg>
</button>
<button type="button" class="hs-dark-mode-active:block hidden hs-dark-mode group flex items-center text-gray-600 hover:text-blue-600 font-medium dark:text-neutral-400 dark:hover:text-neutral-500" data-hs-theme-click-value="light">
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="4"></circle>
<path d="M12 2v2"></path>
<path d="M12 20v2"></path>
<path d="m4.93 4.93 1.41 1.41"></path>
<path d="m17.66 17.66 1.41 1.41"></path>
<path d="M2 12h2"></path>
<path d="M20 12h2"></path>
<path d="m6.34 17.66-1.41 1.41"></path>
<path d="m19.07 4.93-1.41 1.41"></path>
</svg>
</button>Integracja z React/Next.js
// components/Modal.tsx
'use client'
import { useEffect } from 'react'
interface ModalProps {
id: string
title: string
children: React.ReactNode
isOpen: boolean
onClose: () => void
}
export function Modal({ id, title, children, isOpen, onClose }: ModalProps) {
useEffect(() => {
// Reinitialize Preline after mounting
import('preline').then((module) => {
if (typeof window !== 'undefined' && (window as any).HSOverlay) {
(window as any).HSOverlay.autoInit()
}
})
}, [])
return (
<div
id={id}
className={`hs-overlay ${isOpen ? 'open' : 'hidden'} size-full fixed top-0 start-0 z-[80] overflow-x-hidden overflow-y-auto pointer-events-none`}
role="dialog"
tabIndex={-1}
aria-labelledby={`${id}-label`}
>
<div className="hs-overlay-open:mt-7 hs-overlay-open:opacity-100 hs-overlay-open:duration-500 mt-0 opacity-0 ease-out transition-all sm:max-w-lg sm:w-full m-3 sm:mx-auto">
<div className="flex flex-col bg-white border shadow-sm rounded-xl pointer-events-auto dark:bg-neutral-800 dark:border-neutral-700 dark:shadow-neutral-700/70">
<div className="flex justify-between items-center py-3 px-4 border-b dark:border-neutral-700">
<h3 id={`${id}-label`} className="font-bold text-gray-800 dark:text-white">
{title}
</h3>
<button
type="button"
className="size-8 inline-flex justify-center items-center gap-x-2 rounded-full border border-transparent bg-gray-100 text-gray-800 hover:bg-gray-200 focus:outline-none focus:bg-gray-200 disabled:opacity-50 disabled:pointer-events-none dark:bg-neutral-700 dark:hover:bg-neutral-600 dark:text-neutral-400 dark:focus:bg-neutral-600"
aria-label="Close"
onClick={onClose}
>
<span className="sr-only">Close</span>
<svg className="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 6 6 18"></path>
<path d="m6 6 12 12"></path>
</svg>
</button>
</div>
<div className="p-4 overflow-y-auto">
{children}
</div>
</div>
</div>
</div>
)
}Cennik
Open Source (darmowy)
- 640+ komponentów
- 204 bloki i 5 szablonów premium
- Licencja podwójna: MIT albo fair use
- Pełna dokumentacja
- Dark mode
- Responsive
Preline Pro (od 249 USD)
Płatność jest jednorazowa, dostęp bezterminowy, a warianty są trzy: Pro za 249 USD dla jednego dewelopera, Team za 459 USD dla maksymalnie piętnastu osób i Enterprise po wycenie, jako jedyny dopuszczający redystrybucję. Wszystkie dają to samo:
- 780+ bloków
- 21 szablonów premium, łącznie 207 podstron
- Dashboardy administracyjne, strony docelowe i dema sklepów
- Wsparcie ogólne dotyczące konta, licencji i produktu, bez wsparcia technicznego
- Bez plików Figma; system projektowy w Figmie jest udostępniany osobno i za darmo
FAQ - najczęściej zadawane pytania
Czy Preline UI wymaga JavaScript?
Dla statycznych komponentów (buttons, alerts, cards) - nie. Dla interaktywnych (accordion, tabs, modal, dropdown) - tak.
Czy Preline UI działa z Next.js App Router?
Tak! Musisz zaimportować Preline w useEffect w głównym layout/komponentach.
Jak dodać własne style do komponentów?
Możesz nadpisywać klasy Tailwind bezpośrednio w HTML - Preline używa standardowych klas Tailwind.
Jaka jest różnica między Preline UI a Flowbite?
Preline udostępnia za darmo ponad 640 komponentów, Flowbite ma około 68 rodzajów komponentów i bogatsze nakładki dla frameworków. Obie liczby liczą co innego, więc traktuj je jako wskazówkę, a nie miarę: porównaj listę elementów, których faktycznie potrzebujesz.
Czy mogę używać Preline komercyjnie?
Tak. Licencjonowanie jest podwójne, a wariant MIT pozwala na komercyjne użycie bez opłat.
Preline UI a alternatywy
| Cecha | Preline UI | Flowbite | daisyUI | shadcn/ui |
|---|---|---|---|---|
| Sposób sterowania | atrybuty w znacznikach | atrybuty albo komponenty | wyłącznie klasy CSS | komponenty w Twoim kodzie |
| Zależność od frameworka | żadna | żadna, z nakładkami | żadna | React |
| Warstwa JavaScript | własna | własna | brak | biblioteka zachowań |
| Licencja | podwójna, MIT albo fair use | MIT | MIT | MIT |
| Aktualizacje komponentów | z pakietem | z pakietem | z pakietem | ręcznie, kod jest Twój |
Wybór między dwoma pierwszymi jest najtrudniejszy, bo są do siebie najbardziej podobne: obie stawiają na sterowanie atrybutami, obie mają warstwę darmową i płatną, obie działają poza frameworkami. Rozstrzygać warto na podstawie tego, który zestaw ma komponenty, których faktycznie potrzebujesz, i który wygląd bliżej Ci odpowiada, bo pod względem technicznym różnice są niewielkie.
Jeśli natomiast Twój projekt stoi na Reakcie i nie ma powodu, żeby unikać zależności, warto rozważyć podejście z kopiowanym kodem komponentu. Dostajesz wtedy pełną kontrolę i typowanie, a tracisz automatyczne aktualizacje.
Poza tabelą zostaje jeszcze jedna kategoria, którą łatwo z powyższymi pomylić, bo liczby wyglądają podobnie. Zestawy bloków, jak TailGrids, nie dają przycisku ani listy rozwijanej, tylko całą sekcję strony: nagłówek z tytułem, opisem i obrazkiem, gotowy do wklejenia. Kod kopiujesz do siebie, więc aktualizacje nie przychodzą, a logiki przełączania czy obsługi klawiatury tam nie ma i nie ma jej być. To warstwa nad komponentami, a nie zamiast nich, i przy stronie sprzedażowej oszczędza inny rodzaj pracy niż zestaw taki jak ten.
Typowe błędy
Pierwszy to pominięcie ponownej inicjalizacji po zmianie widoku, opisane wyżej. To najczęstsza przyczyna zgłoszeń w rodzaju „komponent nie działa", a rozwiązanie mieści się w jednej linii kodu.
Drugi to brak skanowania plików biblioteki przez Tailwind. Klasy używane przez komponenty muszą zostać znalezione przez skaner, więc katalog pakietu trzeba uwzględnić w konfiguracji. Objawem jest interfejs bez stylów, wyglądający na uszkodzoną instalację. Warto przy tym pamiętać, że od czwartej wersji Tailwinda konfiguracja przeniosła się do arkusza stylów, więc poradniki opisujące dopisanie ścieżki do pliku konfiguracyjnego dotyczą starszego układu i w nowym projekcie nie zadziałają.
Trzeci to zakładanie, że dostępność jest załatwiona. Komponenty niosą atrybuty ról, ale zakres obsługi klawiatury bywa nierówny. Przy elementach kluczowych dla Twojego interfejsu warto przejść je tabulatorem i sprawdzić, czy fokus zachowuje się poprawnie, zamiast przyjmować to na słowo. Test zajmuje kilka minut i polega na czterech pytaniach: czy fokus wchodzi do otwartego okna, czy nie wychodzi poza nie tabulatorem, czy klawisz ucieczki zamyka i czy po zamknięciu wraca na element wywołujący. Jeśli któryś warunek nie jest spełniony, wiesz, ile pracy dołożyć, zanim uznasz komponent za gotowy.
Przy projekcie, w którym dostępność jest wymogiem formalnym, a nie dobrą praktyką, sensowniejszym punktem wyjścia bywa warstwa zachowań pisana pod tym kątem, na przykład Headless UI, z wyglądem złożonym samodzielnie na klasach Tailwinda. Kosztuje to więcej pracy na starcie i zwraca się przy audycie.
Czwarty to mieszanie tej biblioteki z inną warstwą zachowań w jednym projekcie. Dwa systemy podpinające obsługę zdarzeń do tych samych elementów potrafią wchodzić sobie w drogę, zwłaszcza przy oknach modalnych i pułapce fokusa. Objaw bywa nieoczywisty: okno otwiera się poprawnie, ale klawisz ucieczki zamyka je dwukrotnie albo fokus wraca w niewłaściwe miejsce, bo obie biblioteki próbują nim zarządzać jednocześnie.
Piąty to kopiowanie znaczników bez usuwania tego, czego nie używasz. Przykłady z dokumentacji są napisane szeroko, z wariantami i klasami na wypadek różnych sytuacji, więc po wklejeniu zostaje sporo martwego kodu, który zostanie w projekcie na zawsze, jeśli nie przejrzysz go od razu.
Szósty, widoczny dopiero po kilku miesiącach, to brak własnej warstwy pośredniej. Skoro komponenty są znacznikami, a nie kodem, to trzydzieści przycisków w projekcie to trzydzieści niezależnych kopii tych samych klas. Zmiana wyglądu przycisku oznacza wtedy trzydzieści edycji zamiast jednej. Rozwiązaniem jest wydzielenie własnych komponentów opakowujących te znaczniki zaraz na początku, nawet jeśli w danym momencie wydaje się to zbędne, bo koszt zrobienia tego później rośnie z każdym tygodniem.
Kod źródłowy i pełną listę komponentów znajdziesz w repozytorium Preline, a dokumentację na stronie projektu.