fhdaisy provides Python components for using DaisyUI in FastHTML applications. For example, Btn('Click me', cls='-primary') creates <button class="btn btn-primary">Click me</button>. Component names follow DaisyUI’s CSS classes: card becomes Card, and alert becomes Alert. Modifiers can omit the component prefix, as in -primary for btn-primary.
fhdaisy.xtras provides helpers for repeated structures such as accordions and forms. You can also write your own helpers.
Install latest from pypi
DaisyUI adds component classes to Tailwind CSS. Tailwind utilities such as bg-blue-500 and p-4 each control a styling property. DaisyUI classes such as btn, card, and modal define styles for whole components. For example, class="btn btn-primary" styles a button without listing its individual Tailwind utilities.
DaisyUI provides CSS-based components with themes, responsive design, and accessibility features. It does not require a JavaScript framework. FastHTML can use these components in server-rendered HTML.
from fasthtml.common import *
from fasthtml.jupyter import *
from fhdaisy import *
DaisyUI buttons use the btn class. Additional classes prefixed with btn- modify their appearance:
c = Button('Hey there', cls='btn btn-primary')
print(c)
<button class="btn btn-primary">Hey there</button>
Use mk_previewer() to preview components in Jupyter, Solveit, and similar environments:
Pass the button to the previewer:
<iframe srcdoc=" <!doctype html>
<html>
<head>
<title>FastHTML page</title>
<link rel="canonical" href="https://testserver/_8HrMjHAiTWG4vTUItzgUEw">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script> <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script>
function sendmsg() {
window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
}
window.onload = function() {
sendmsg();
document.body.addEventListener('htmx:afterSettle', sendmsg);
document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
};</script> </head>
<body>
<div class="max-w-lg ">
<button class="btn btn-primary">Hey there</button> </div>
</body>
</html>
" style="width: 100%; height: auto; border: none;" onload="{
let frame = this;
window.addEventListener('message', function(e) {
if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
}, false);
}" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe>
fhdaisy components create HTML elements with the corresponding DaisyUI base class. Btn creates a <button> with class btn. The HTML tag follows DaisyUI’s documentation. For example, Alert creates a <div> and Input creates an <input>.
In the cls parameter, a leading - expands to the component’s class prefix. For Btn, -primary, -outline, and -sm expand to btn-primary, btn-outline, and btn-sm.
The previous button can therefore be written as:
c = Btn('Hey there', cls='-primary')
print(c)
<button class="btn btn-primary ">Hey there</button>
This renders identically to the previous manual version.
<iframe srcdoc=" <!doctype html>
<html>
<head>
<title>FastHTML page</title>
<link rel="canonical" href="https://testserver/_bZU0JtfGTNmXfFUY9ImULQ">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script> <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script>
function sendmsg() {
window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
}
window.onload = function() {
sendmsg();
document.body.addEventListener('htmx:afterSettle', sendmsg);
document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
};</script> </head>
<body>
<div class="max-w-lg ">
<button class="btn btn-primary ">Hey there</button> </div>
</body>
</html>
" style="width: 100%; height: auto; border: none;" onload="{
let frame = this;
window.addEventListener('message', function(e) {
if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
}, false);
}" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe>
DaisyUI input classes follow the same pattern:
input supplies the base input styling.
input-bordered adds a border.
input-primary uses the primary colour theme.
Using the full class names:
p( Input(placeholder='Enter name', cls='input input-bordered') )
<iframe srcdoc=" <!doctype html>
<html>
<head>
<title>FastHTML page</title>
<link rel="canonical" href="https://testserver/__MZIf7NKRaWxTqIhVIh4pw">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script> <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script>
function sendmsg() {
window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
}
window.onload = function() {
sendmsg();
document.body.addEventListener('htmx:afterSettle', sendmsg);
document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
};</script> </head>
<body>
<div class="max-w-lg ">
<input placeholder="Enter name" class="input input input-bordered ">
</div>
</body>
</html>
" style="width: 100%; height: auto; border: none;" onload="{
let frame = this;
window.addEventListener('message', function(e) {
if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
}, false);
}" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe>
Input supplies the base input class. Use -bordered as shorthand for input-bordered:
p( Input(placeholder='Enter name', cls='-bordered') )
<iframe srcdoc=" <!doctype html>
<html>
<head>
<title>FastHTML page</title>
<link rel="canonical" href="https://testserver/_-ATZYyaFTBeliHkLxG0avw">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script> <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script>
function sendmsg() {
window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
}
window.onload = function() {
sendmsg();
document.body.addEventListener('htmx:afterSettle', sendmsg);
document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
};</script> </head>
<body>
<div class="max-w-lg ">
<input placeholder="Enter name" class="input input-bordered ">
</div>
</body>
</html>
" style="width: 100%; height: auto; border: none;" onload="{
let frame = this;
window.addEventListener('message', function(e) {
if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
}, false);
}" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe>
print( Alert('Success! Your changes have been saved', cls='-success -soft') )
<div class="alert alert-success alert-soft ">Success! Your changes have been saved</div>
DaisyUI creates alerts using the alert class on a <div> element. Here’s the traditional DaisyUI approach:
p( Div('This is an important message!', cls='alert alert-info') )
<iframe srcdoc=" <!doctype html>
<html>
<head>
<title>FastHTML page</title>
<link rel="canonical" href="https://testserver/_6of-JsHRRPed2hWquMXjrQ">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script> <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script>
function sendmsg() {
window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
}
window.onload = function() {
sendmsg();
document.body.addEventListener('htmx:afterSettle', sendmsg);
document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
};</script> </head>
<body>
<div class="max-w-lg ">
<div class="alert alert-info">This is an important message!</div>
</div>
</body>
</html>
" style="width: 100%; height: auto; border: none;" onload="{
let frame = this;
window.addEventListener('message', function(e) {
if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
}, false);
}" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe>
Alert also creates a <div>. Its name comes from the CSS class, rather than an HTML tag. HTML has no <alert> element:
p( Alert('This is an important message!', cls='-info') )
<iframe srcdoc=" <!doctype html>
<html>
<head>
<title>FastHTML page</title>
<link rel="canonical" href="https://testserver/_glfh0ZPLQ42hJ7jwI6th6w">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script> <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script>
function sendmsg() {
window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
}
window.onload = function() {
sendmsg();
document.body.addEventListener('htmx:afterSettle', sendmsg);
document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
};</script> </head>
<body>
<div class="max-w-lg ">
<div class="alert alert-info ">This is an important message!</div>
</div>
</body>
</html>
" style="width: 100%; height: auto; border: none;" onload="{
let frame = this;
window.addEventListener('message', function(e) {
if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
}, false);
}" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe>
A DaisyUI card contains nested elements:
- A
<div> with class card as the container
- An optional
<figure> for images
- A
<div> with class card-body for the main content
- Within the card body:
- An optional
<h2> with class card-title
- Content paragraphs
- A
<div> with class card-actions for buttons
fhdaisy provides Card for the container. The part names card-body, card-title, and card-actions correspond to CardBody, CardTitle, and CardActions:
p ( Card(
Figure(Img(src='https://picsum.photos/seed/fd/400/225')),
CardBody(
CardTitle('Card title'),
P('This is a sample card with some content'),
CardActions(cls='justify-end')( Btn('Buy Now', cls='-primary') )
) , cls='w-96 bg-base-100 shadow-sm'
) )
<iframe srcdoc=" <!doctype html>
<html>
<head>
<title>FastHTML page</title>
<link rel="canonical" href="https://testserver/_2aQWwIExTh_TWhBa-h3ugQ">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script> <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script>
function sendmsg() {
window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
}
window.onload = function() {
sendmsg();
document.body.addEventListener('htmx:afterSettle', sendmsg);
document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
};</script> </head>
<body>
<div class="max-w-lg ">
<div class="card w-96 bg-base-100 shadow-sm ">
<figure><img src="https://picsum.photos/seed/fd/400/225"></figure> <div class="card-body ">
<h2 class="card-title ">Card title</h2>
<p>This is a sample card with some content</p>
<div class="card-actions justify-end ">
<button class="btn btn-primary ">Buy Now</button> </div>
</div>
</div>
</div>
</body>
</html>
" style="width: 100%; height: auto; border: none;" onload="{
let frame = this;
window.addEventListener('message', function(e) {
if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
}, false);
}" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe>
from fhdaisy.xtras import *
import fasthtml.components as fh
An accordion item contains a radio input, a title, and content inside a Collapse component:
p ( Collapse(
fh.Input(type='radio', name='acc1', checked="checked"),
CollapseTitle('Click to expand', cls='font-semibold'),
CollapseContent('This is the hidden content', cls='text-sm'),
cls='-arrow border border-base-300'
) )
<iframe srcdoc=" <!doctype html>
<html>
<head>
<title>FastHTML page</title>
<link rel="canonical" href="https://testserver/_yHDYllsARciNQyUeXjfHlQ">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script> <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script>
function sendmsg() {
window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
}
window.onload = function() {
sendmsg();
document.body.addEventListener('htmx:afterSettle', sendmsg);
document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
};</script> </head>
<body>
<div class="max-w-lg ">
<div class="collapse collapse-arrow border border-base-300 ">
<input type="radio" name="acc1" checked="checked">
<div class="collapse-title font-semibold ">Click to expand</div>
<div class="collapse-content text-sm ">This is the hidden content</div>
</div>
</div>
</body>
</html>
" style="width: 100%; height: auto; border: none;" onload="{
let frame = this;
window.addEventListener('message', function(e) {
if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
}, false);
}" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe>
fhdaisy.xtras provides mk_accordion_item to construct those parts:
p (mk_accordion_item('Click to expand', 'This is the hidden content',
name='acc1', checked=True, cls='-arrow border border-base-300', titlecls='font-semibold'))
<iframe srcdoc=" <!doctype html>
<html>
<head>
<title>FastHTML page</title>
<link rel="canonical" href="https://testserver/_3fcwTxUAQCeZVt-zevFrRA">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script> <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script>
function sendmsg() {
window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
}
window.onload = function() {
sendmsg();
document.body.addEventListener('htmx:afterSettle', sendmsg);
document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
};</script> </head>
<body>
<div class="max-w-lg ">
<div class="collapse collapse-arrow border border-base-300 ">
<input type="radio" name="acc1" checked>
<div class="collapse-title font-semibold ">Click to expand</div>
<div class="collapse-content ">This is the hidden content</div>
</div>
</div>
</body>
</html>
" style="width: 100%; height: auto; border: none;" onload="{
let frame = this;
window.addEventListener('message', function(e) {
if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
}, false);
}" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe>
mk_accordion builds an accordion from a sequence of items. All xtras helpers use the mk_ prefix to distinguish them from components that map directly to DaisyUI classes:
accitems = [
('How do I create an account?', 'Click the "Sign Up" button in the top right corner.'),
('I forgot my password', 'Click on "Forgot Password" on the login page.'),
('How do I update my profile?', 'Go to "My Account" settings and select "Edit Profile".')
]
p( mk_accordion(*accitems,
titlecls='font-semibold', contentcls='text-sm',
itemcls='-arrow border border-base-300',
cls='-vertical min-w-md') )
<iframe srcdoc=" <!doctype html>
<html>
<head>
<title>FastHTML page</title>
<link rel="canonical" href="https://testserver/_x0MiTJGhTUqNTftdvrTVBQ">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script> <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script>
function sendmsg() {
window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
}
window.onload = function() {
sendmsg();
document.body.addEventListener('htmx:afterSettle', sendmsg);
document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
};</script> </head>
<body>
<div class="max-w-lg ">
<div class="join join-vertical min-w-md ">
<div class="collapse collapse-arrow border border-base-300 ">
<input type="radio" name="acc-132586820290048" checked>
<div class="collapse-title font-semibold ">How do I create an account?</div>
<div class="collapse-content text-sm ">Click the "Sign Up" button in the top right corner.</div>
</div>
<div class="collapse collapse-arrow border border-base-300 ">
<input type="radio" name="acc-132586820290048">
<div class="collapse-title font-semibold ">I forgot my password</div>
<div class="collapse-content text-sm ">Click on "Forgot Password" on the login page.</div>
</div>
<div class="collapse collapse-arrow border border-base-300 ">
<input type="radio" name="acc-132586820290048">
<div class="collapse-title font-semibold ">How do I update my profile?</div>
<div class="collapse-content text-sm ">Go to "My Account" settings and select "Edit Profile".</div>
</div>
</div>
</div>
</body>
</html>
" style="width: 100%; height: auto; border: none;" onload="{
let frame = this;
window.addEventListener('message', function(e) {
if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
}, false);
}" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe>
Write custom helpers for repeated structures that fhdaisy.xtras does not yet cover. Use the mk_ prefix for these helpers too.
A DaisyUI rating consists of masked input elements, one for each star or other shape. Written individually, the inputs look like this:
p( Rating(
Mask(cls='-star-2 bg-orange-400', checked=True, name='rating-demo'),
Mask(cls='-star-2 bg-orange-400', checked=True, name='rating-demo'),
Mask(cls='-star-2 bg-orange-400', checked=True, name='rating-demo'),
Mask(cls='-star-2 bg-orange-400', checked=False, name='rating-demo'),
Mask(cls='-star-2 bg-orange-400', checked=False, name='rating-demo'),
cls='-sm'
) )
A mk_rating helper can generate the inputs from the number of stars and the selected count:
def mk_rating(n, checked, nm=None, cls='', itemcls=''):
return Rating(*[Mask(cls=itemcls, checked=(i<checked), name=nm) for i in range(n)], cls=cls)
This creates the same five-star rating with three stars selected:
p( mk_rating(5, 3, nm='rating-demo', cls='-sm', itemcls='-star-2 bg-orange-400') )
<iframe srcdoc=" <!doctype html>
<html>
<head>
<title>FastHTML page</title>
<link rel="canonical" href="https://testserver/_LNo66vGTTmWXZ5MMpF6RaA">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script> <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script>
function sendmsg() {
window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
}
window.onload = function() {
sendmsg();
document.body.addEventListener('htmx:afterSettle', sendmsg);
document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
};</script> </head>
<body>
<div class="max-w-lg ">
<div class="rating rating-sm ">
<input type="radio" checked name="rating-demo" class="mask mask-star-2 bg-orange-400 input">
<input type="radio" checked name="rating-demo" class="mask mask-star-2 bg-orange-400 input">
<input type="radio" checked name="rating-demo" class="mask mask-star-2 bg-orange-400 input">
<input type="radio" name="rating-demo" class="mask mask-star-2 bg-orange-400 input">
<input type="radio" name="rating-demo" class="mask mask-star-2 bg-orange-400 input">
</div>
</div>
</body>
</html>
" style="width: 100%; height: auto; border: none;" onload="{
let frame = this;
window.addEventListener('message', function(e) {
if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
}, false);
}" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe>
For other repeated structures, put the component construction in a mk_ function. Make the parts that vary between uses its parameters.
This example combines several components. Sonnet-3.5 generated it in Solveit:
c = Div(
Card(
Figure(Img(src="https://picsum.photos/seed/42/400/250")),
CardBody(
H2("Mountain Adventure", cls="card-title"),
Flex(
Badge("New", cls="-primary"),
Badge("Featured", cls="-secondary -outline"),
Badge("Travel", cls="-accent -soft"),
cls="gap-2 mb-3"),
P("Discover breathtaking mountain trails and scenic vistas on this unforgettable journey."),
Flex(
Avatar(
Div(Img(src="https://picsum.photos/80/80", cls="rounded-full"), cls="w-10"),
cls="-online"),
Div(
Div("Alex Chen", cls="font-semibold"),
Div("2 hours ago", cls="text-sm opacity-50")),
cls="items-center gap-3 my-4"),
mk_rating(5, 3, nm='rating-demo', cls='-sm', itemcls='-star-2 bg-orange-400'),
Progress(value="75", max="100", cls="-primary -sm mt-3"),
CardActions(
Btn("Learn More", cls="-primary"),
Btn("Bookmark"),
cls="justify-end mt-4")
),
cls="w-96 bg-base-100 shadow-xl"),
cls="min-h-screen bg-base-200 flex items-center justify-center p-8"
)
<iframe srcdoc=" <!doctype html>
<html>
<head>
<title>FastHTML page</title>
<link rel="canonical" href="https://testserver/_ugpV4FVDQICiyX5-VNmVCQ">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js@1.0.12/fasthtml.js"></script><script src="https://cdn.jsdelivr.net/gh/answerdotai/surreal@main/surreal.js"></script><script src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline@main/script.js"></script> <link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" type="text/css">
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script>
function sendmsg() {
window.parent.postMessage({height: document.documentElement.offsetHeight}, '*');
}
window.onload = function() {
sendmsg();
document.body.addEventListener('htmx:afterSettle', sendmsg);
document.body.addEventListener('htmx:wsAfterMessage', sendmsg);
};</script> </head>
<body>
<div class="max-w-lg ">
<div class="min-h-screen bg-base-200 flex items-center justify-center p-8">
<div class="card w-96 bg-base-100 shadow-xl ">
<figure><img src="https://picsum.photos/seed/42/400/250"></figure> <div class="card-body ">
<h2 class="card-title">Mountain Adventure</h2>
<div class="flex gap-2 mb-3 ">
<span class="badge badge-primary ">New</span><span class="badge badge-secondary badge-outline ">Featured</span><span class="badge badge-accent badge-soft ">Travel</span> </div>
<p>Discover breathtaking mountain trails and scenic vistas on this unforgettable journey.</p>
<div class="flex items-center gap-3 my-4 ">
<div class="avatar avatar-online ">
<div class="w-10">
<img src="https://picsum.photos/80/80" class="rounded-full"> </div>
</div>
<div>
<div class="font-semibold">Alex Chen</div>
<div class="text-sm opacity-50">2 hours ago</div>
</div>
</div>
<div class="rating rating-sm ">
<input type="radio" checked name="rating-demo" class="mask mask-star-2 bg-orange-400 input">
<input type="radio" checked name="rating-demo" class="mask mask-star-2 bg-orange-400 input">
<input type="radio" checked name="rating-demo" class="mask mask-star-2 bg-orange-400 input">
<input type="radio" name="rating-demo" class="mask mask-star-2 bg-orange-400 input">
<input type="radio" name="rating-demo" class="mask mask-star-2 bg-orange-400 input">
</div>
<progress value="75" max="100" class="progress progress-primary progress-sm mt-3 "></progress> <div class="card-actions justify-end mt-4 ">
<button class="btn btn-primary ">Learn More</button><button class="btn ">Bookmark</button> </div>
</div>
</div>
</div>
</div>
</body>
</html>
" style="width: 100%; height: auto; border: none;" onload="{
let frame = this;
window.addEventListener('message', function(e) {
if (e.source !== frame.contentWindow) return; // Only proceed if the message is from this iframe
if (e.data.height) frame.style.height = (e.data.height+1) + 'px';
}, false);
}" allow="accelerometer; autoplay; camera; clipboard-read; clipboard-write; display-capture; encrypted-media; fullscreen; gamepad; geolocation; gyroscope; hid; identity-credentials-get; idle-detection; magnetometer; microphone; midi; payment; picture-in-picture; publickey-credentials-get; screen-wake-lock; serial; usb; web-share; xr-spatial-tracking"></iframe>
Try components interactively in Jupyter or Solveit. Create a previewer with mk_previewer() and compare different combinations and modifiers. Start with Btn or Alert and modifiers such as -primary, -outline, and -lg. Then try multi-part components such as cards and modals.
The markdown version of this documentation makes useful context for an LLM. Click “commonmark” on the documentation page to obtain it. Provide it when asking an assistant to prototype an interface or convert existing HTML designs to fhdaisy.
See the DaisyUI component documentation for all components and modifiers. See the FastHTML documentation for building complete applications.
Contributions of useful mk_ helpers are welcome in the fhdaisy repository. Use that prefix for helpers that construct repeated component structures.