/* Copyright 2026 Marimo. All rights reserved. */ import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect, useId } from "react"; import { useForm } from "react-hook-form"; import type { z } from "zod"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { useAppConfig } from "@/core/config/config"; import { getAppWidths } from "@/core/config/widths"; import { useRequestClient } from "@/core/network/requests"; import { useDebouncedCallback } from "@/hooks/useDebounce"; import { arrayToggle } from "@/utils/arrays"; import { type AppConfig, AppConfigSchema, AppTitleSchema, } from "../../core/config/config-schema"; import { Checkbox } from "../ui/checkbox"; import { Input } from "../ui/input"; import { Kbd } from "../ui/kbd"; import { ExternalLink } from "../ui/links"; import { NativeSelect } from "../ui/native-select"; import { toast } from "../ui/use-toast"; import { SettingDescription, SettingTitle, SQL_OUTPUT_SELECT_OPTIONS, } from "./common"; const FORM_DEBOUNCE = 100; // ms; export const AppConfigForm: React.FC = () => { const [config, setConfig] = useAppConfig(); const { saveAppConfig } = useRequestClient(); const htmlCheckboxId = useId(); const ipynbCheckboxId = useId(); // Create form const form = useForm({ resolver: zodResolver( AppConfigSchema as unknown as z.ZodType, ), defaultValues: config, }); const onSubmit = async (values: AppConfig) => { await saveAppConfig({ config: values }) .then(() => { setConfig(values); }) .catch(() => { setConfig(values); }); }; const debouncedSubmit = useDebouncedCallback((v: AppConfig) => { onSubmit(v); }, FORM_DEBOUNCE); // When width is changed, dispatch a resize event so widgets know to resize useEffect(() => { window.dispatchEvent(new Event("resize")); }, [config.width]); return (
Notebook Settings Configure how your notebook or application looks and behaves.
( Width field.onChange(e.target.value)} value={field.value} disabled={field.disabled} className="inline-flex mr-2" > {getAppWidths().map((option) => ( ))} )} /> (
App title { field.onChange(e.target.value); if ( AppTitleSchema.safeParse(e.target.value).success ) { document.title = e.target.value; } }} /> The application title is put in the title tag in the HTML code and typically displayed in the title bar of the browser window.
)} />
(
Custom CSS { field.onChange(e.target.value); if ( AppTitleSchema.safeParse(e.target.value).success ) { document.title = e.target.value; } }} /> A filepath to a custom css file to be injected into the notebook.
)} /> (
HTML Head { field.onChange(e.target.value); }} /> A filepath to an HTML file to be injected into the{" "} {""} section of the notebook. Use this to add analytics, custom fonts, meta tags, or external scripts.
)} />
(
SQL Output Type { field.onChange(e.target.value); toast({ title: "Kernel Restart Required", description: "This change requires a kernel restart to take effect.", }); }} value={field.value} disabled={field.disabled} className="inline-flex mr-2" > {SQL_OUTPUT_SELECT_OPTIONS.map((option) => ( ))} The Python type returned by a SQL cell. For best performance with large datasets, we recommend using{" "} native. See the{" "} SQL guide {" "} for more information.
)} />
(
{ field.onChange(arrayToggle(field.value, "html")); }} /> HTML
{ field.onChange(arrayToggle(field.value, "ipynb")); }} /> IPYNB
When enabled, marimo will periodically save this notebook in your selected formats (HTML, IPYNB) to a folder named{" "} __marimo__ next to your notebook file.
)} />
); }; const SettingSection = ({ title, children, }: { title: string; children: React.ReactNode; }) => { return (

{title}

{children}
); };