Buttons, sheets, and themes
Native controls, useful defaults, and scoped themes. Import from renku/ui; no additional packages or theme provider required.
On this page
Try the theme
accent
neutral
danger
Sample actions activated 0 times. These controls demonstrate appearance and do not save data.
Always light
Always dark
Sheet is a controlled modal bottom sheet. Render it inside a client component with a unique stable id, a visible title, open, and onOpenChange. Update your state when onOpenChange is called. Children stay mounted while closed; removing the component releases its listeners and scroll lock. The initial server markup is closed, including when open starts true, and opens after client activation.
"use client";
import {state} from "renku/client";
import {Button, Sheet} from "renku/ui";
export default function Example() {
const open = state(false);
return @{
<div>
<Button aria-controls="workspace-sheet" aria-expanded={open.get()}
onClick={() => open.set(true)}>Menu</Button>
<Sheet id="workspace-sheet" title="Workspace" open={open.get()}
onOpenChange={(value) => open.set(value)}>
<nav aria-label="Workspace"><a href="/settings">Settings</a></nav>
</Sheet>
</div>
};
}The optional snapPoint is a fraction of the panel height greater than zero and at most one. It defaults to one; a smaller value adds a partial stop alongside full expansion and dismissal. Choose enough visible height for the title and close button. The panel is content-sized up to 90% of the viewport and 640px wide. Pass xstyle to customize it, preserving its scrolling geometry. Change the initial snap point while closed.
A native dialog provides background inertness, focus containment, and focus return. Escape, backdrop clicks, the visible Close button, and swiping down dismiss it. Localize the button with closeLabel. Native CSS scroll snapping handles touch gestures; the backdrop follows a view timeline. Content scrolls only at full expansion. Reduced motion skips smooth opening and closing. Visual viewport changes resize the sheet when the software keyboard opens.
This first version supports bottom placement and one optional partial stop. Multiple custom stops, alternate edges, decorative stack animations, and non-dismissible workflows are not part of this API. Native scroll snapping, view timelines, dialog, scrollend, and VisualViewport are required. Test your own content with touch input, zoom, and software keyboards on target devices.
Design reference: Devon Govett’s Scrolling is All You Need.
Button renders a button and defaults to type="button", solid appearance, accent tone, and medium size. Use type="submit" for forms. ButtonLink renders an anchor and preserves normal browser navigation. Static controls work without JavaScript; event handlers belong in a client component.
"use client";
import { state } from "renku/client";
import { Button, ButtonLink } from "renku/ui";
export default function Actions() {
const count = state(0);
return @{
<section>
<Button onClick={() => count.set(count.get() + 1)}>Add one</Button>
<Button variant="outline" tone="neutral" disabled>Unavailable</Button>
<ButtonLink href="/guides" variant="ghost">Read the guides</ButtonLink>
<output>{count.get()}</output>
</section>
};
}Apply appearance.system to your document for the operating system preference, or appearance.light / appearance.dark for an explicit choice. Color tokens use light-dark(); the browser handles system changes. A subtree can choose its own scheme. Apply surface colors where that subtree paints a background or text.
The playground changes its own scope. For a persisted document preference, validate light, dark, or system on the server and render the corresponding appearance style on html. Keep preference-dependent HTML out of shared cache entries. A global preference controller and storage policy are not included.
import * as stylex from "renku/stylex";
import { Button, appearance } from "renku/ui";
import { accent, surface } from "renku/ui/tokens.stylex";
const violet = stylex.createTheme(accent, {
solid: "light-dark(#6d28d9, #c4b5fd)",
solidHover: "light-dark(#5b21b6, #ddd6fe)",
solidActive: "light-dark(#4c1d95, #a78bfa)",
onSolid: "light-dark(#ffffff, #2e1065)",
text: "light-dark(#6d28d9, #c4b5fd)",
hover: "light-dark(#f5f3ff, #2e1065)",
active: "light-dark(#ede9fe, #4c1d95)",
border: "light-dark(#7c3aed, #a78bfa)",
});
const styles = stylex.create({
scope: { backgroundColor: surface.canvas, color: surface.text, padding: 24 },
});
export default function Palette() @{
<section {...stylex.props(appearance.system, violet, styles.scope)}>
<Button>System appearance</Button>
<div {...stylex.props(appearance.dark, styles.scope)}>
<Button variant="outline">Always dark</Button>
</div>
</section>
}The public token groups are surface, accent, neutral, danger, and control. Use createTheme for a scoped brand or control shape. Themes for the same group replace one another; unspecified members use the group's defaults. Supply a complete tone palette when changing a brand. Independent groups compose.
For a local change, pass compiled StyleX values as xstyle. The order is base, size, tone, variant, then xstyle. The exported buttonStyles recipe applies the same appearance to a custom native element. Default and hover declarations are separate conditions, so overriding one does not automatically override the other.
import * as stylex from "renku/stylex";
import { Button, buttonStyles } from "renku/ui";
const styles = stylex.create({
wide: { minInlineSize: "12rem" },
});
export default function CustomButton() @{
<section>
<Button xstyle={styles.wide}>Save changes</Button>
<a href="/guides" {...stylex.props(buttonStyles({ variant: "outline" }))}>
Custom native link
</a>
</section>
}Use visible, meaningful labels or aria-label / aria-labelledby for icon-only content. Keyboard activation, disabled controls, and forms use native behavior. Focus uses a visible outline. Reduced motion and forced colors are supported. Recheck contrast for your own themes and overrides, especially transparent buttons on custom surfaces.
Supported variants are solid, outline, and ghost; tones are accent, neutral, and danger; sizes are sm, md, and lg. The controls have minimum sizes and grow with their content. Explicit native attributes include form association and submitter overrides, ARIA labels and toggle state, and link targets and downloads. There is no arbitrary prop spreading, polymorphic tag, or onPress API.
Native disabled state is available now. Pending behavior, automatic announcements, an IconButton, and client SVG support are separate future additions. Client actions remain disabled until their existing client root activates; a Button does not add its own activation root.
You can now choose native controls, scope a theme, and identify the browser capabilities your sheet requires.