CS UI
A Python UI framework built on Flet, providing a rich set of pre-styled, ready-to-use components with enhanced defaults and a clean package structure.
Features
- Batteries included —
from ui import *re-exports everything from Flet plus all CS UI components - Design tokens + Material 3 theming — no raw color / radius / font-size literals inside
components. A single brand seed (
color_scheme_seed) derives the whole M3 color scheme, so light/dark and brand-color switching are framework-level operations, not per-widget recoloring - Inheritance-based — static components subclass Flet native controls (e.g.,
Button(ft.Button),Text(ft.Text)) - Smart defaults — components come with sensible styling defaults (colors, sizes, border-radius, etc.) for rapid prototyping
- Categorized modules — components organized by function: chart, display, feedback, input, layout, navigation
- Declarative routing — root-view routing via
ft.Router(routes, manage_views=False)+page.render - Working page overlays — under the root-view path,
page.overlay/page.show_dialogactually render, so theMultiSelectdropdown, theDateInputcalendar, and theDateTimeInputcalendar-plus-time-wheel panel can truly float above the content (a collapsed field is all the layout height the widget takes; opening it never pushes the content below) - Two component paradigms — static components subclass native Flet controls; interactive ones are
@ft.componentfunctions whose state is declared with hooks (use_state/use_memo/use_effect) - Charts — Bar, Line, Area, and Scatter chart wrappers via
flet-charts, with both numeric and categorical x-axes - Desktop-grade timeline —
Timelinesupports three alignments (rail on the left / rail on the right / alternating around a centered rail); the timestamp can sit in its own column on the opposite side (the default, VS Code style), above the title, inline, or be hidden. Dots come with semantic colors, three variants (filled / outlined / plain) and three sizes;doneturns into a check mark automatically andpendingappends an "in progress" hollow ring. Rows are clickable end to end — hover highlight and click ripple are painted by Material'sink, so nothing is re-rendered from Python and long lists stay smooth
Requirements
- Python >= 3.12
- flet[all] ~= 1.0.4
- flet-code-editor
- flet-charts
- flet-video / flet-audio / flet-webview
Installation
uv pip install cs-ui
Or install from source in editable mode:
git clone https://github.com/icstos/cs-ui.git
cd cs-ui
pip install -e .
Quick Start
import flet as ft
from ui import (
Button,
Card,
Checkbox,
Column,
Container,
Divider,
Input,
Router,
Route,
Row,
Text,
)
@ft.component
def HomePage() -> ft.Control:
name = ft.use_ref(lambda: Input(label="Name", value="Shawn", width=260)).current
agree = ft.use_ref(lambda: Checkbox(label="I have read the terms")).current
# Pages return plain controls and draw their own top bar — see the notes below
return Container(
expand=True,
content=Column(
spacing=0,
controls=[
Container(
height=56,
bgcolor=ft.Colors.WHITE,
padding=ft.Padding.symmetric(horizontal=14),
content=Row(
vertical_alignment=ft.CrossAxisAlignment.CENTER,
controls=[Text("CS UI Demo", size=17, weight=ft.FontWeight.W_600)],
),
),
Card(
elevation=4,
content=Container(
padding=24,
border_radius=16,
content=Column(
controls=[
Text("CS UI declarative demo", size=24, weight=ft.FontWeight.BOLD),
Text("Built on flet 1.0.0.", size=14, color="#6b7280"),
Divider(),
name.ui(),
Row(
spacing=20,
controls=[agree.ui(), ft.Switch(label="A switch")],
),
Button("Click me", on_click=lambda _: print("clicked!")),
],
spacing=16,
),
),
),
],
),
)
@ft.component
def App() -> ft.Control:
return Router([Route(index=True, component=HomePage)], manage_views=False)
def main(page: ft.Page) -> None:
page.title = "CS UI Demo"
page.render(App)
if __name__ == "__main__":
ft.run(main)
Render path: use
page.render+Router(manage_views=False), notpage.render_views.Two pitfalls we hit for real:
page.render_views(the view stack behindRouter(manage_views=True)) covers the entire page overlay layer — anything appended topage.overlayrenders zero pixels, andpage.show_dialogis just as dead. It does not raise:len(page.overlay)is correct and callbacks still fire. FloatingMultiSelectpanels, toasts and dialogs all silently break.ft.Viewcannot be used as an ordinary control. Under the root-view path, putting aViewinto the widget tree makes Flutter throwBad state: No elementon repeat and the whole page turns into a grey box. Likewiseft.AppBaris anAdaptiveControl: it only fitsView.appbarand cannot go intoColumn.controls— draw the top bar withContainer+Rowinstead.Migration notes:
ft.app(main)→ft.run(main);page.add(...)→page.render(Component);page.go(route)→page.navigate(route); hand-maintainedpage.views→ft.Router(routes, manage_views=False). The bundledui.Buttontakes its text ascontent(notlabel).Components come in two flavors: stateless controls (
Button/Table/Rating…) subclass Flet controls and are usable immediately; stateful components (Input/Checkbox/Switch/SelectBox/MultiSelect…) are@ft.observabledata objects that must be constructed and then rendered via.ui()— putting the object itself in the widget tree yields a blank or grey box.If the host app has to keep the view stack (
manage_views=True),MultiSelectautomatically degrades to in-flow expansion (the panel takes layout height and pushes content down); passfloat_panel=Falseto opt out of the overlay explicitly.
Package Structure
src/ui/
├── __init__.py # re-exports all of flet + every CS UI component (617 names)
├── app.py # legacy-style App entry point
├── ft_init.py # Flet initialization
├── theme.py # theme / palette
├── cli.py # CLI entry point
├── chart/ # Charts (via flet-charts)
│ ├── _data.py # shared data parsing + palette (numeric / categorical axes)
│ ├── bar_chart.py # BarChart
│ ├── line_chart.py # LineChart
│ ├── rea_chart.py # AreaChart
│ └── scatter_chart.py # ScatterChart
├── components/ # Reusable generic controls
│ └── icon_button.py # IconButton
├── core/ # Core utilities
│ ├── config.py # configuration
│ ├── constants.py # StyleType / FeedbackStyle / ButtonShape …
│ ├── float_layer.py # page overlay: overlay_usable / use_float_layer
│ ├── language.py # i18n
│ ├── logger.py # logging
│ ├── snackbar.py # content guard for SnackBar-based components
│ ├── styles.py # shared style helpers
│ ├── theme.py # ThemeProvider / ThemeSwitcher / use_theme
│ └── tokens.py # design tokens: SPACE / RADIUS / FONT_SIZE / semantic colors
├── data/ # Static assets (fonts, images)
├── display/ # Display components
│ ├── image.py # Image
│ ├── image_gridview.py # ImageGridView
│ ├── list_tile.py # ListTile
│ ├── log_container.py # LogContainer
│ ├── text.py # Text / Header_1..5 / Quote / Link / Code / Markdown / Json
│ └── media/ # Media
│ ├── audio.py # Audio / AudioPlayer
│ ├── pdf.py # Pdf
│ └── video.py # Video
├── feedback/ # Feedback & overlays
│ ├── alert_dialog.py # AlertDialog
│ ├── loading.py # Loading
│ ├── message.py # Message
│ ├── progress_bar.py # ProgressBar
│ └── toast.py # Toast / toast_success / toast_error …
├── input/ # Form inputs
│ ├── button.py # Button
│ ├── checkbox.py # Checkbox / CheckboxGroup
│ ├── chip.py # Chip
│ ├── color_picker.py # ColorPicker
│ ├── date_input.py # DateInput
│ ├── datetime_input.py # DateTimeInput
│ ├── file_picker.py # FilePicker / DirPicker
│ ├── image_picker.py # ImagePicker
│ ├── input.py # Input (data_type: str/int/float/file/dir)
│ ├── multi_select.py # MultiSelect
│ ├── radio.py # RadioGroup
│ ├── rating.py # Rating
│ ├── search_bar.py # SearchBar
│ ├── segmented_button.py # SegmentedButton
│ ├── select_box.py # SelectBox
│ ├── slider.py # Slider
│ └── switch.py # Switch
├── layout/ # Layout & containers
│ ├── card.py # Card
│ ├── column.py # Column
│ ├── container.py # Container
│ ├── divider.py # Divider
│ ├── expander.py # Expander
│ ├── grid_view.py # GridView
│ ├── list_view.py # ListView
│ ├── page.py # PageLayout
│ ├── row.py # Row
│ ├── stack.py # Stack
│ ├── table.py # Table (with paging)
│ ├── tabs.py # Tabs / Tab / TabBar / TabBarView
│ ├── time_line.py # Timeline / TimelineItem
│ └── view.py # View
├── navigation/ # Navigation
│ ├── app_bar.py # AppBar
│ ├── bread_crumb.py # BreadCrumb / Crumb
│ └── paging.py # Paging / PagingState
└── utils/ # Utilities
├── code_editor.py # Code
├── code_view.py # CodeView
└── componts.py # Helper components
Component Overview
| Category | Components |
|---|---|
| Core | ThemeProvider, ThemeSwitcher, use_theme, theme_state |
| Chart | BarChart, LineChart, AreaChart, ScatterChart |
| Display | Image, ImageGridView, ListTile, LogContainer, Text / Header_1..5 / Quote / Link / Code / Markdown / Json |
| Display / Media | Audio, AudioPlayer, Video, Pdf |
| Feedback | AlertDialog, Loading, Message, ProgressBar, Toast |
| Input | Button, Checkbox / CheckboxGroup, Chip, ColorPicker, DateInput, DateTimeInput, FilePicker / DirPicker, ImagePicker, Input, MultiSelect, RadioGroup, Rating, SearchBar, SegmentedButton, SelectBox, Slider, Switch |
| Layout | Card, Column, Container, Divider, Expander, GridView, ListView, PageLayout, Row, Stack, Table, Tabs, Timeline, View |
| Navigation | AppBar, BreadCrumb, Paging |
Two paradigms:
- Static components (
Button/Table/Rating/Timeline/ charts …) subclass Flet controls and are ready to use. - Interactive components (
Input/Checkbox/Switch/SelectBox/MultiSelect/Pdf…) are@ft.componentfunctions; their state is declared with hooks and the return value goes straight into the tree.
Design Tokens and Theming
Components never contain raw color / radius / font-size literals — everything comes from
ui.core.tokens. Switching theme and brand color is therefore a framework-level
operation rather than per-widget recoloring.
Apply a theme
Wrap the root view in ThemeProvider; build returns your router:
import flet as ft
from ui import ThemeProvider
def App() -> ft.Control:
return ThemeProvider(
build=lambda: ft.Router(ROUTES, not_found=NotFoundPage, manage_views=False)
)
def main(page: ft.Page):
page.render(App)
ThemeProvider builds the subtree with use_memo(build, []) — switching the theme does not
rebuild the widget tree, so values already typed into a form survive the switch
(verified on a real device: after switching to dark, the use_state value is intact).
Light/dark and brand color
from ui import use_theme, ThemeSwitcher, PRIMARY_SEEDS
@ft.component
def TopBar() -> ft.Control:
state = use_theme() # subscribe (must be called inside a component)
ft.Button("toggle", on_click=lambda e: state.toggle_mode())
ft.Button("recolor", on_click=lambda e: state.set_primary(PRIMARY_SEEDS["violet"]))
ThemeSwitcher is a ready-made top-bar control (brand palette + light/dark switch).
ThemeState exposes three distinct behaviors:
| Method | Behavior |
|---|---|
toggle_mode() |
always jump to the opposite of what is currently visible (for a ☀/🌙 button, so a click always shows a change) |
cycle_mode() |
system → light → dark → system (for a dropdown) |
set_mode(name) |
go straight to a mode; "system" follows the platform brightness |
set_primary(hex) |
swap the brand seed; the whole M3 scheme is re-derived |
Tokens
Theme-aware tokens are ft.Colors role names resolved by Flutter at paint time, so a theme
switch costs zero Python re-renders:
ft.Colors.PRIMARY, ft.Colors.SURFACE_CONTAINER_LOWEST, ft.Colors.ON_SURFACE_VARIANT
Framework-resolved tokens cover the semantic roles M3 does not ship (ui.core.tokens):
from ui import SPACE, RADIUS, FONT_SIZE, GAP, PAD, elevation, overlay, hairline
SPACE["4"] # 16 — a 4pt scale: (0,1,2,3,4,5,6,8,10,12,16,20,24,32,40)
RADIUS["lg"] # 8
FONT_SIZE["body"] # 13
elevation(2) # five shadow levels, tinted with the 'shadow' role so it follows light/dark
overlay(0.06) # 'onsurface,0.06' — a translucent scrim that adapts automatically
hairline() # 1px divider
Semantic colors come in three tiers — one value cannot serve every use:
| Function | Use | Light | Dark |
|---|---|---|---|
semantic("success") |
solid fill (badge background, status dot, progress segment) | #047857 |
#34D399 |
on_semantic("success") |
foreground on that fill | #FFFFFF |
#04231A |
semantic_text("success") |
text / icons / hairlines on the page | #047857 |
#34D399 |
Why: measured WCAG contrast, the previous #10B981 is only 2.54:1 on white and #F59E0B
only 2.15:1 — white text on them is equally unreadable at 2.54 / 2.15. Each tier now clears
4.5:1, guarded by assertions in scripts/_theme_logic.py.
Available roles: success / warning / danger / info / neutral.
One important trap
In flet 1.0.4, reading a field of an observable does not subscribe to it. Subscription only
happens in three places: the component argument is an Observable, use_state, or use_context.
state = theme_state()
mode = state.mode # ❌ just a read; nothing re-renders
state = use_theme() # ✅ use_context(theme_context); flet attaches the subscription
Relatedly, theme-aware role names and Python-computed colors have very different costs: role
names are resolved by Flutter (free on theme switch), whereas a hex computed by
semantic("success") only updates if the component itself subscribed to the theme.
Importing
All components can be imported directly from ui:
from ui import Button, Card, Column, Container, Divider, Row, Text, TextField
Since ui re-exports Flet, you can also access Flet types directly:
from ui import ft # the flet module
# or
from ui import Page, Colors, Icons, MainAxisAlignment, CrossAxisAlignment
Examples
Run the demo to see every component in action (11 category pages plus a 404 page):
python examples/demo.py
| Route | Contents |
|---|---|
/ |
Home navigation (enter each category) |
/general |
Text / buttons / icons / chips |
/layout |
Containers / lists / tables / timeline |
/navigation |
Breadcrumb / tabs / paging |
/form |
Inputs / selectors / slider / rating |
/upload |
Files / directories / save / images |
/feedback |
Toast / message / dialog / progress |
/display |
Logs / code / images |
/charts |
Line / area / bar / scatter charts |
/media |
Audio / video / PDF |
/about |
Migration cheat-sheet and design notes |
Additional test files:
examples/test_home.py— minimal home pageexamples/test_form.py— form componentsexamples/test_feedback.py— feedback (dialog, snackbar, loading, progress)examples/test_chart.py— chart componentsexamples/test_line_chart.py— line chartexamples/test_button.py— button variantsexamples/test_minimal.py/test_simple.py— minimal examplesexamples/my_control.py— custom control example
MCP server (ui-mcp)
An MCP server that gives AI agents version-accurate knowledge of this component
library — same idea as flet-mcp, but for
ui. The index is built by runtime introspection (@ft.control flattens the
constructor and @ft.observable creates proxy classes, so a static scan would miss
the real signatures) and shipped as JSON; the server never imports ui.
pip install -e ".[mcp]"
ui-mcp # stdio server (default subcommand)
ui-mcp build # rebuild the index after changing src/ui
ui-mcp check # is the index older than the sources?
ui-mcp guide # list the curated "gotchas" topics
Client config:
{ "mcpServers": { "ui": { "command": "ui-mcp" } } }
12 tools in five groups (UI_MCP_ENABLE_{API,ICONS,EXAMPLES,DOCS,GUIDE}):
get_api (primary verifier, walks base classes), list_components,
search_components (Chinese + English), enum tools, find_icon,
search_examples / get_example (real demo code, with per-component call sites),
search_docs / get_doc, and get_guidelines — the paradigm rules that no API
lookup can tell you (frozen trees, page.render, #AARRGGBB colors, design tokens).
Adding a component? Add one line to
src/ui_mcp/aliases.pyso Chinese queries find it, then runui-mcp build.
Metadata
Release files for cs-ui 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| cs_ui-0.1.0.tar.gz | 338.2 kB | Without provenance | Details for cs_ui-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| cs_ui-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for cs_ui-0.1.0-py3-none-any.whl |
Total release size: 720.9 kB
Release files / cs_ui-0.1.0.tar.gz
| Download URL | cs_ui-0.1.0.tar.gz |
|---|---|
| Size | 338.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1b798c12302cc5ad0014c9650f759321c8548190a5f0059f077f0e727aa0cd7d
|
|
BLAKE2b-256 checksum How to use checksums |
2644a336aca921ee495287df33f20c87f093b6a256e00569ca2f3da2d6b4789c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Release files / cs_ui-0.1.0-py3-none-any.whl
| Download URL | cs_ui-0.1.0-py3-none-any.whl |
|---|---|
| Size | 382.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c02ff2816ed48cb11fe12f8ca5956b57264115d7f88534464a20cdf968cb669c
|
|
BLAKE2b-256 checksum How to use checksums |
c197c8457e1f78674913f7408a23aa3b6473056070c772cb2aa468eaf4e5af61
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Security for cs-ui 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.